JVM垃圾回收機制

範圍:要回收哪些區域

在JVM五種內存模型中,有三個是不需要進行垃圾回收的:程序計數器、JVM棧、本地方法棧。因爲它們的生命週期是和線程同步的,隨着線程的銷燬,它們佔用的內存會自動釋放,所以只有方法區和堆需要進行GC。

前提:如何判斷對象已死

所有的垃圾收集算法都面臨同一個問題,那就是找出應用程序不可到達的內存塊,將其釋放,這裏面得不可到達主要是指應用程序已經沒有內存塊的引用了, 在Java中,某個對象對應用程序是可到達的是指:這個對象被根(根主要是指類的靜態變量,或者活躍在所有線程棧的對象的引用)引用或者對象被另一個可到達的對象引用。

引用計數算法
引用計數是最簡單直接的一種方式,這種方式在每一個對象中增加一個引用的計數,這個計數代表當前程序有多少個引用引用了此對象,如果此對象的引用計數變爲0,那麼此對象就可以作爲垃圾收集器的目標對象來收集。
優點:簡單,直接,不需要暫停整個應用
缺點:1.需要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操作;2.不能處理循環引用的問題
因此這種方法是垃圾收集的早期策略,現在很少使用。Sun的JVM並沒有採用引用計數算法來進行垃圾回收,是基於根搜索算法的。

根搜索算法
通過一系列的名爲“GC Root”的對象作爲起點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Root沒有任何引用鏈相連時,則該對象不可達,該對象是不可使用的,垃圾收集器將回收其所佔的內存。

在java語言中,可作爲GCRoot的對象包括以下幾種對象:
a. java虛擬機棧(棧幀中的本地變量表)中的引用的對象。
b.方法區中的類靜態屬性引用的對象。
c.方法區中的常量引用的對象。
d.本地方法棧中JNI本地方法的引用對象。

判斷無用的類:
(1).該類的所有實例都已經被回收,即java堆中不存在該類的實例對象。
(2).加載該類的類加載器已經被回收。
(3).該類所對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射機制訪問該類的方法。

四種引用
GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用主要有四種:
1.強引用(Strong Reference)
強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。

23軟引用(Soft Reference)
如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
下面舉個例子,假如有一個應用需要讀取大量的本地圖片,如果每次讀取圖片都從硬盤讀取,則會嚴重影響性能,但是如果全部加載到內存當中,又有可能造成內存溢出,此時使用軟引用可以解決這個問題。
設計思路是:用一個HashMap來保存圖片的路徑和相應圖片對象關聯的軟引用之間的映射關係,在內存不足時,JVM會自動回收這些緩存圖片對象所佔用的空間,從而有效地避免了內存溢出的問題。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

3.弱引用(Weak Reference)
弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

4.虛引用(Phantom Reference)
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用於檢測對象是否已經從內存中刪除,跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。

ReferenceQueue queue = new ReferenceQueue ();  
PhantomReference pr = new PhantomReference (object, queue);  

策略:JVM中的垃圾收集策略

標記-清除算法
這裏寫圖片描述

標記清除收集器停止所有的工作,從根掃描每個活躍的對象,然後標記掃描過的對象,標記完成以後,清除那些沒有被標記的對象。

優點:
1 解決循環引用的問題
2 不需要編譯器的配合,從而就不執行額外的指令

缺點:
1. 每個活躍的對象都要進行掃描,收集暫停的時間比較長。
2.標記-清除算法採用從根集合進行掃描,對存活的對象對象標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收,如上圖所示。
標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活對象比較多的情況下極爲高效,但由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片

複製算法
這裏寫圖片描述
複製收集器將內存分爲兩塊一樣大小空間,某一個時刻,只有一個空間處於活躍的狀態,當活躍的空間滿的時候,GC就會將活躍的對象複製到未使用的空間中去,原來不活躍的空間就變爲了活躍的空間。

優點:
1 只掃描可以到達的對象,不需要掃描所有的對象,從而減少了應用暫停的時間

缺點:
1.需要額外的空間消耗,某一個時刻,總是有一塊內存處於未使用狀態
2.複製對象需要一定的開銷

複製算法採用從根集合掃描,並將存活對象複製到一塊新的,沒有使用過的空間中,這種算法當空間存活的對象比較少時,極爲高效,但是帶來的成本是需要一塊內存交換空間用於進行對象的移動。

標記-整理算法
這裏寫圖片描述
標記整理收集器汲取了標記清除和複製收集器的優點,它分兩個階段執行,在第一個階段,首先掃描所有活躍的對象,並標記所有活躍的對象,第二個階段首先清除未標記的對象,然後將活躍的的對象複製到堆得底部
該算法極大的減少了內存碎片,並且不需要像複製算法一樣需要兩倍的空間。
標記-整理算法採用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象佔用的空間後,會將所有的存活對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。

分代回收算法
垃圾分代回收算法(GenerationalCollecting)基於對對象生命週期分析後得出的垃圾回收算法。
因爲我們前面有介紹,內存主要被分爲三塊,新生代、舊生代、持久代。三代的特點不同,造就了他們所用的GC算法不同,新生代適合那些生命週期較短,頻繁創建及銷燬的對象,舊生代適合生命週期相對較長的對象,持久代在Sun HotSpot中就是指方法區(有些JVM中根本就沒有持久代這中說法)。首先介紹下新生代、舊生代、持久代的概念及特點。
這裏寫圖片描述

Young(年輕代、新生代):JVM specification中的 Heap的一部份年輕代分三個區。一個Eden區,兩個Survivor區。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的對象,將被複制舊生代。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。

新生代使用複製算法和標記-清除垃圾收集算法:新生代中98%的對象是朝生夕死的短生命週期對象,所以不需要將新生代劃分爲容量大小相等的兩部分內存,而是將新生代分爲Eden區,Survivor from(Survivor 0)和Survivor to(Survivor1)三部分,其佔新生代內存容量默認比例分別爲8:1:1,其中Survivor from和Survivor to總有一個區域是空白,只有Eden和其中一個Survivor總共90%的新生代容量用於爲新創建的對象分配內存,只有10%的Survivor內存浪費,當新生代內存空間不足需要進行垃圾回收時,仍然存活的對象被複制到空白的Survivor內存區域中,Eden和非空白的Survivor進行標記-清理回收,兩個Survivor區域是輪換的。
如果空白Survivor空間無法存放下仍然存活的對象時,使用內存分配擔保機制,直接將新生代依然存活的對象複製到年老代內存中,同時對於創建大對象時,如果新生代中無足夠的連續內存時,也直接在年老代中分配內存空間。
Java虛擬機對新生代的垃圾回收稱爲Minor GC,次數比較頻繁,每次回收時間也比較短。
使用java虛擬機-Xmn參數可以指定新生代內存大小。

Tenured(年老代、舊生代):JVMspecification中的 Heap的一部份年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。
年老代中的對象一般都是長生命週期對象,對象的存活率比較高,因此在年老代中使用標記-整理垃圾回收算法
Java虛擬機對年老代的垃圾回收稱爲MajorGC/Full GC,次數相對比較少,每次回收的時間也比較長。
java虛擬機-Xms參數可以指定最小內存大小,-Xmx參數可以指定最大內存大小,這兩個參數分別減去Xmn參數指定的新生代內存大小,可以計算出年老代最小和最大內存容量。

Perm(持久代、永久代): JVM specification中的 Method area 用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。
java虛擬機內存中的方法區在SunHotSpot虛擬機中被稱爲永久代,是被各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。永久代垃圾回收比較少,效率也比較低,但是也必須進行垃圾回收,否則會永久代內存不夠用時仍然會拋出OutOfMemoryError異常。
永久代也使用標記-整理算法進行垃圾回收,java虛擬機參數-XX:PermSize和**-XX:MaxPermSiz**e可以設置永久代的初始大小和最大容量。

垃圾回收過程
上面我們看了JVM的內存分區管理,現在我們來看JVM的垃圾回收工作是怎樣運作的。
首先當啓動J2EE應用服務器時,JVM隨之啓動,並將JDK的類和接口,應用服務器運行時需要的類和接口以及J2EE應用的類和接口定義文件也及編譯後的Class文件或JAR包中的Class文件裝載到JVM的永久存儲區。在伊甸園中創建JVM,應用服務器運行時必須的JAVA對象,創建J2EE應用啓動時必須創建的JAVA對象;J2EE應用啓動完畢,可對外提供服務。
JVM在伊甸園區根據用戶的每次請求創建相應的JAVA對象,當伊甸園的空間不足以用來創建新JAVA對象的時候,JVM的垃圾回收器執行對伊甸園區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象(如果該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸爲沒有存在的必要,依此類推),並將那些被其他對象所引用的JAVA對象移動到倖存者0區。
如果倖存者0區有足夠空間存放則直接放到倖存者0區;如果倖存者0區沒有足夠空間存放,則JVM的垃圾回收器執行對倖存者0區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象,並將那些被其他對象所引用的JAVA對象移動到倖存者1區。
如果倖存者1區有足夠空間存放則直接放到倖存者1區;如果倖存者1區沒有足夠空間存放,則JVM的垃圾回收器執行對倖存者1區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象,並將那些被其他對象所引用的JAVA對象移動到養老區。
如果養老區有足夠空間存放則直接放到養老區;如果養老區沒有足夠空間存放,則JVM的垃圾回收器執行對養老區區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象,並保留那些被其他對象所引用的JAVA對象。
如果到最後養老區,倖存者1區,倖存者0區和伊甸園區都沒有空間的話,則JVM會報告“JVM堆空間溢出(java.lang.OutOfMemoryError: Java heap space)”,也即是在堆空間沒有空間來創建對象。
這就是JVM的內存分區管理,相比不分區來說;一般情況下,垃圾回收的速度要快很多;因爲在沒有必要的時候不用掃描整片內存而節省了大量時間。

對象的空間分配和晉升
(1)對象優先在Eden上分配
(2)大對象直接進入老年代
虛擬機提供了-XX:PretenureSizeThreshold參數,大於這個參數值的對象將直接分配到老年代中。因爲新生代採用的是標記-複製策略,在Eden中分配大對象將會導致Eden區和兩個Survivor區之間大量的內存拷貝。
(3)長期存活的對象將進入老年代
對象在Survivor區中每熬過一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認爲15歲)時,就會晉升到老年代中。

觸發:何時開始GC

Minor GC(新生代回收)的觸發條件比較簡單,Eden空間不足就開始進行Minor GC回收新生代
而Full GC(老年代回收,一般伴隨一次MinorGC)則有幾種觸發條件:
1.老年代空間不足
2.PermSpace空間不足
3.統計得到的MinorGC晉升到老年代的平均大小大於老年代的剩餘空間

這裏注意一點:PermSpace並不等同於方法區,只不過是HotspotJVM用PermSpace來實現方法區而已,有些虛擬機沒有PermSpace而用其他機制來實現方法區。

實現:JVM中的回收器類型

串行回收器(Serial Collector)
單線程執行回收操作,回收期間暫停所有應用線程的執行,client模式下的默認回收器

1.年輕代的回收算法(Minor Collection):把Eden區的存活對象移到To區,To區裝不下直接移到年老代,把From區的移到To區,To區裝不下直接移到年老代,From區裏面年齡很大的升級到年老代。 回收結束之後,Eden和From區都爲空,此時把From和To的功能互換,From變To,To變From,每一輪迴收之前To都是空的。設計的選型爲複製。

2.年老代的回收算法(Full Collection):年老代的回收分爲三個步驟,標記(Mark)、清除(Sweep)、合併(Compact)。標記階段把所有存活的對象標記出來,清除階段釋放所有死亡的對象,合併階段把所有活着的對象合併到年老代的前部分,把空閒的片段都留到後面。設計的選型爲合併,減少內存的碎片。

並行回收器(Parallel Collector)
使用多個線程同時進行垃圾回收,多核環境裏面可以充分的利用CPU資源,減少回收時間,增加JVM生產率,Server模式下的默認回收器。與串行回收器相同,回收期間暫停所有應用線程的執行。

1.年輕代的回收算法(Minor Collection):使用多個線程回收垃圾,每一個線程的算法與串行回收器相同。

2.年老代的回收算法(Full Collection):年老代依然是單線程的,與串行回收器相同。

並行合併收集器(Parallel Compacting Collection)
年輕代和年老代的回收都是用多線程處理。與並行回收器相比,年老代的回收時間更短,從而減少了暫停時間間隔(Pause time)。

1.年輕代的回收算法(Minor Collection):與並行回收器(ParallelCollector)相同

2.年老代的回收算法(Full Collection) :年老代分爲三個步驟,標記、統計、合併。這裏用到分的思想,把年老代劃分爲很多個固定大小的區(region)。標記階段,把所有存活的對象劃分爲N組(應該與回收線程數相同),每一個線程獨立的負責自己那一組,標記存活對象的位置以及所在區(Region)的存活率信息,標記爲並行的。統計階段,統計每一個區(Region)的存活率,原則上靠前面的存活率較高,從前到後,找到值得合併的開始位置(絕大多數對象都存活的區不值得合併),統計階段是串行的(單線程)。合併階段,依據統計階段的信息,多線程並行的把存活的對象從一個區(Region)複製到另外一個區(Region)。

併發標記清除回收器(Concurrent Mark-Sweep Collector)
又名低延時收集器(Low-latencyCollector),通過各種手段使得應用程序被掛起的時間最短。基本與應用程序併發地執行回收操作,沒有合併和複製操作。

1.年輕代的回收算法(Minor Collection):與並行回收器(ParallelCollector)相同

2.年老代的回收算法(Full Collection) :分爲四個步驟,初始標記(Initial Mark)、併發標記(ConcurrentMark)、再次標記(Remark)、以及併發清理(Concurrent Sweep)。特別注意,沒有合併操作,所以會有碎片。
初始化階段: 暫停應用線程,找出所有存活的對象,耗時比較短,回收器使用單線程。
併發標記階段: 回收器標記操作與應用併發運行,回收器使用單線程標記存活對象。
再次標記:併發標記階段由於應用程序也在運行,這個過程中可能新增或者修改對象。所以再次暫停應用線程,找出所有修改的對象,使用多線程標記。
併發清理:回收器清理操作與應用併發運行,回收器使用單線程清理死亡對象。

感謝

本文非原創,經作者同意後轉載!
感謝作者冰河winner

http://blog.csdn.net/u012152619/article/details/46981643
JVM垃圾回收機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章