Java虛擬機器那些事(1)

來源:互聯網
上載者:User

標籤:部落格   ant   ram   通知   ada   java8   object   如何   編譯   

對於Java程式員來說,虛擬機器和多線程方面的知識是必不可少的。這裡就來聊一聊Java虛擬機器的一些基礎和概念,主要內容來源自《深入理解Java虛擬機器》這本書。

首先為什麼要有虛擬機器呢?因為對象的建立和銷毀是一個很頻繁的操作,由程式員來維護,一方面成本有點高,增加開發成本;另一方面,如果操作不當,發生了記憶體泄露,要自己去調試代碼,找出原因。所以虛擬機器的這種機制的誕生可以說是程式員的福音,解放了生產力。但凡事有利也有弊,虛擬機器的引入使得Java在效能方面跟C++比還是有一定差距,像網路遊戲或者資料庫這種對效能要求比較高的應用,都會選擇用C/C++來開發。同時,雖然我們有了虛擬機器,但還是要對它的運行機制和效能調優有所瞭解,不然萬一發生java記憶體泄露,就會無從下手了。

Java記憶體地區

圖中的五大資料區域可以分為兩類:1.由所有線程共用的資料區域 2. 線程隔離的資料區

第一類:方法區,堆

第二類:虛擬機器棧,本地方法棧,程式計數器

(圖片源自部落格http://blog.sina.com.cn/s/blog_ed30769e0102v233.html)

程式計數器(Program Counter Register)

可以把它看作是當前線程所執行的位元組碼的行號指標。我們通過改變這個計數器的值來選取下一個需要執行的位元組碼指令。每個線程都有一個獨立的程式計數器。如果線程正在執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是native方法,這個計數器值為空白。此記憶體地區是唯一一個在java虛擬機器規範中沒有規定任何OutOfMemoryError情況的地區

Java虛擬機器棧(Java Virtual Machine Stacks)

它描述的是Java方法執行的記憶體模型:每個方法被執行都會建立一個棧幀(Stack Frame)用於儲存局部變數表、運算元棧、動態連結、方法出口等資訊。

局部變數表存放了編譯期可知的各種基礎資料型別 (Elementary Data Type)、對象引用和returnAddress類型(指向一條位元組碼指令的地址)。局部變數表所需的記憶體空間是在編譯期間完成分配的。

這個地區規定了兩種異常:

1.如果線程請求的棧深度大於虛擬機器所允許的深度,將拋出StackOverflowError異常。

2.記憶體動態擴充時,若無法申請到足夠的記憶體時會拋出OutOfMemoryError異常。

本地方法棧(Native Method Stacks)

 此方法區與Java虛擬機器棧類似,區別在於它是為虛擬機器使用到的Native方法服務。有的虛擬機器(如Sun HotSpot虛擬機器)直接把它們合二為一了。

與虛擬機器棧一樣,它也會拋出 StackOverflowError和OutOfMemoryError異常。

Java堆(Java Heap)

所有線程共用的一塊記憶體地區,在虛擬機器啟動時建立。此記憶體地區的唯一目的就是存放對象執行個體。

可以通過-Xmx和-Xms的設定來控制堆記憶體大小。

如果堆無法擴充時,將會拋出OutOfMemoryError異常。

方法區(Method Area)

它也是各個線程共用的記憶體地區,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料。

Java虛擬機器對這個地區的限制非常寬鬆。垃圾收集行為在這個地區是比較少出現的;這個地區的記憶體回收目標主要是針對常量池的回收和對類型的卸載

當方法區無法滿足記憶體配置需求時,將拋出OutOfMemoryError異常。

運行時常量池(Runtime Constant Pool)

它是方法區的一部分; 用於存放編譯期間產生的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的運行時常量池中。

運行期間也可能將新的常量放入池中,比如說使用String類的intern()方法。

無法申請到記憶體時會拋出OutOfMemoryError異常。

直接記憶體(Direct Memory)

直接記憶體並不是虛擬機器運行時資料區的一部分,也不是虛擬機器規範中的記憶體地區,但這部分記憶體被頻繁地使用,也可能會導致OutOfMemoryError異常出現。

在JDK1.4中加入了NIO,引入了一種基於通道與緩衝區的I/O方式,它可以使用Native函數庫直接分配堆外記憶體,然後通過儲存在堆裡面的DirectByteBuffer對象作為這塊記憶體的引用操作。

對象訪問

Object obj = new Object();

虛擬機器棧:會在本地變數表中儲存一個對象引用

堆:儲存了Object類型所有執行個體資料值

方法區:儲存物件的類資訊

不同的虛擬機器的對象訪問方式會有所不同,主流的訪問方式有兩種:使用控制代碼和直接指標。

控制代碼訪問方式

堆會劃分出一塊記憶體來作為控制代碼池,引用中儲存的就是對象的控制代碼地址,而控制代碼中包含了對象執行個體資料和類型資料各自的具體地址資訊

好處:引用中儲存的是穩定的控制代碼地址,在對象被移動時只會改變控制代碼中的執行個體資料指標,而引用本身不需要修改。

直接指標訪問方式( Sun HotSpot採用的方式)

堆對象的布局中就必須考慮如何放置訪問類型資料的相關資訊,引用中直接儲存的就是對象地址。

好處:速度更快,節省了一次指標定位的時間開銷。

OutOfMemory異常

堆溢出:通過-XX:+HeapDumpOnOutOfMemoryError可以在出現記憶體溢出時Dump出當前的記憶體堆轉儲快照以便事後進行分析。

虛擬機器棧和本地方法棧溢出:通過-Xss設定棧容量。一般棧深度可以到達1000-2000。棧容量過大,多線程時容易耗盡記憶體,因為單個線程耗記憶體比較多。

運行時常量池溢出:常見的PermGen issue。Java8對方法區做了調整,所以這個問題不會再出現了。

方法區溢出:往往出現於動態產生大量Class的應用中。

本機直接記憶體溢出:可通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與堆的最大值一樣。

對象回收

如何確定對象已經死去,可以採取回收了呢?

引用計數法(Python)

非常直觀的一種方法,增加一處引用時加一,減少一處引用時減一。

但它的一個主要缺陷是很難解決對象之間的循環相依性問題

根搜尋演算法(Java,C#,Lisp)

基本思路:通過一系列GC roots對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個對象到GC roots沒有任何引用鏈相連時,則證明此對象是不可達的,作為回收對象。

GC roots對象包括下面幾種:

1.虛擬機器棧(棧幀中的本地變數表)中的引用的對象

2.方法區中的類靜態屬性引用的對象

3.方法區中的常量引用的對象

4.本地方法棧中JNI的引用的對象

在JDK1.2之後,Java擴充了引用的概念,分為四種:

1.強引用(strong reference):一般引用,有引用,不會回收對象。

2.軟引用(soft reference):系統將要發生溢出時,會把它們引用的對象列入回收範圍進行一次回收。若記憶體還是不夠,才拋異常。

3.弱引用(weak reference):被引用的對象只能活到下一次垃圾收集發生之前。

4.虛引用(phantom reference):一個對象是否有虛引用,完全不會對其存留時間構成影響,也無法通過虛引用取得一個對象執行個體。設定此引用得目標就是為了能在這個對象被回收時收到一個系統通知。

宣告一個對象死亡,至少要經過兩次標記過程

如果一個對象在根搜尋後,沒有關聯得引用鏈,它將會被第一次標記並且進行一次篩選,篩選得條件是此對象是否有必要執行finalize()方法。

如果沒有覆蓋此方法或者方法已被虛擬機器調用過,則視為“沒有必要執行”。

如果需要執行finalize()方法,則會被放置到一個F-Quenue隊列,由低優先順序得Finalizer線程去執行。執行此方法,但並不保證運行結束(為了運行效率考慮)。

稍後會對F-Queue進行第二次小規模得標記,只要在finalize方法中與引用鏈上得任意一個對象產生關聯就會在此次標記時被移除“即將回收”的集合。

 回收方法區

在堆中,尤其是新生代中,一次記憶體回收可以回收70%~95%的空間,而方法區的垃圾收集效率遠低於此。

方法區的記憶體回收主要內容:廢棄常量和無用的類

類需要滿足下面3個條件才能算是“無用的類”,可以進行回收:

1.該類所有的執行個體已經被回收

2.載入該類的ClassLoader已經被回收

3.該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

是否對類進行回收,由參數-Xnoclassgc進行控制

垃圾收集演算法標記-清除演算法

演算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

缺點:

1.效率問題,標記和清除過程的效率都不高

2.空間問題,會產生大量不連續的記憶體片段。如果發現不能分配記憶體給大對象時,不得不再觸發一次記憶體回收

複製演算法

將可使用的記憶體按容量分為兩塊,每次只使用其中的一塊。

經過實驗發現,新生代中的對象98%是朝生夕死的,所以記憶體可以分配為一塊較大的Eden空間和兩塊較小的Survivor空間。每次使用Eden和其中一塊Survivor空間;回收記憶體時,將還存活的對象拷貝到另外一塊Survivor空間上,然後清理掉另外兩塊的空間。

Sun HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1。當Survivor空間不夠時,需要依賴老年代進行分配擔保,若有擔保直接分配進老年代。

標記-整理演算法

複製演算法在對象存活率較高時就要執行較多的複製操作,效率將會變低。所以老年代一般不能直接選用這種演算法。

標記-整理演算法在標記後,讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的記憶體。

垃圾收集器Serial收集器

曆史最悠久的收集器,在JDK1.3.1之前是虛擬機器新生代收集唯一的選擇。單線程的收集器。

優點:簡單高效

缺點:收集時必須暫停其他所有的背景工作執行緒(stop the world)。

使用情境:虛擬機器運行在Client模式下預設新生代收集器

ParNew收集器

它是Serial收集器的多線程版本。預設開啟的收集線程數與CPU的數量相同。JDK1.4引入。

除了Serial收集器外,只有它能與CMS收集器配合工作。

使用情境:運行在Server模式下的虛擬機器首選的新生代收集器

Parallel Scavenge收集器

目標:達到一個可控制的輸送量。所謂輸送量就是CPU用於運行使用者代碼的時間與CPU總消耗時間的比值。適合在後台運算而不需要太多互動的任務。

-XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間

-XX:GCTimeRatio用來設定輸送量大小,預設99

-XX:+UserAdaptiveSizePolicy, 開啟GC自適應的調節策略

使用情境:新生代收集器

Serial Old收集器

它是Serial收集器的老年代版本,使用標記整理演算法。

使用情境:Client模式下的老年代。

Parallel Old收集器

它是Parallel Scavenge收集器的老年代版本。JDK1.6引入。

注重吞吐率的場合,可以考慮採用Parallel Scavenge加Parallel Old收集器。

CMS收集器

它是一種以擷取最短回收停頓時間為目標的收集器,並發執行。標記清除演算法:初始標記,並發標記,重新標記,並發清除。其中初始標記和重新標記仍需stop the world。

缺點:

1.對CPU資源非常敏感。預設回收線程數是(CPU數量+3)/ 4。

2.無法處理浮動垃圾,可能出現“concurrent mode failure”失敗而導致另一次full gc的產生。要是在運行收集期間,預留的記憶體無法滿足程式需要,就會出現“concurrent mode failure”失敗,這時會臨時採用serial old收集器來進行老年代的垃圾收集,這樣停頓時間會變長。

3.收集結束時會產生大量片段,容易因無法分配大對象,而觸發full gc。可以啟用參數-XX:+UseCMSCompactAtFullCollection來享受full gc後來一次磁碟重組。另外有-XX:CMSFullGCsBeforeCompaction這個參數來設定執行了多少次不壓縮的full gc後,進行一次帶壓縮的。

 G1收集器

目標是替代CMS收集器。可以分代收集,不會產生片段。有分區(region)的概念,優先回收垃圾最多的地區。通過remenbered set來避免全堆掃描。

收集可分為四個步驟:初始標記,並發標記,最終標記,篩選回收。

記憶體配置與回收策略對象優先在Eden分配

當Eden區沒有足夠空間時,會觸發一次minor gc。虛擬機器提供-XX:+PrintGCDetails這個參數來列印記憶體回收日誌以及進程退出時當前記憶體各地區的分配情況。

大對象直接進入老年代

這樣做的目的時避免Eden區裡面發生大量的記憶體拷貝。可通過-XX:PretenureSizeThreshold參數設定多大的對象進入老年代。

長期存活的對象將進入老年代

熬過15次minor gc後,到達15歲,就會被晉陞到老年代。關於這個年齡,可以通過-XX:MaxTenuringThreshold來設定。

動態對象年齡判定

為了適應不同程式的記憶體情況,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代。

空間分配擔保

在發生minor gc時,虛擬機器會檢測之前每次晉陞到老年代的平均大小是否大於老年代的剩餘空間大小。

如果大於,則改為進行一次 full gc。

如果小於,先查看HandlePromotionFailure設定(JDK1.6預設開啟)是否允許擔保失敗。如果允許,進行minor gc;如果不允許,還是要 full gc。

如果最後發生擔保失敗,還是要重新發起一次full gc。

Java虛擬機器那些事(1)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.