對於C/C++開發者來說,他們在內存管理方面具有至高的權利,但是也承擔著巨大的維護責任。而對於Java程序員來說,有了JVM(Java虛擬機)管理機制的幫助,再也不用擔心內存洩漏和內存溢出問題了。因此,這篇文章我將深入探討一下JVM,它的內部結構以及運行原理。
當GC要回收某個對象的時候,它是如何判斷該對象已死(即不可能再被使用),當一個對象不再被使用時,那麼這個對象就是可以被回收的。
引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象(不是引用)都有一個引用計數。當一個對象被創建時,且將該對象分配給一個變量,該變量計數設置為1。當任何其它變量被賦值為這個對象的引用時,計數加1(a = b,則b引用的對象+1),但當一個對象的某個引用超過了生命周期或者被設置為一個新值時,對象的引用計數減1。任何引用計數為0的對象可以被當作垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數減1。
引用計數算法實現簡單,很好理解,判斷效率也很高,大部分情況下是一個很不錯的算法。但值得注意的是,主流的Java虛擬機並沒有采用引用計數算法,其主要的原因就是它很難解決對象之間的相互循環引用。很簡單的一個實例:
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
class MyObject{
public Object object = null;
}
代碼中的對象object1與對象object2相互引用,這樣的情況在引用計數算法下永遠都不會被回收,但實際情況下這樣的相互指引沒有任何實際意義。
通過一系列的稱為“GC Roots”的對象作為起點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
但注意,JVM中並不是判斷對象不可達就立即回收,被判定為不可達的對象要成為可回收對象必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成為可回收對象的可能性,則基本上就真的成為可回收對象了。
藍色:仍然存活的對象
白色:判定可回收的對象<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMiBpZD0="二垃圾收集算法">二、垃圾收集算法
在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裡面涉及到一個問題是:如何高效地進行垃圾回收。由於Java虛擬機規范並沒有對如何實現垃圾收集器做出明確的規定,因此各個廠商的虛擬機可以采用不同的方式來實現垃圾收集器,所以在此只討論幾種常見的垃圾收集算法的核心思想。
最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,它的標記過程其實在前一節講述對象標記判定時已經介紹過了。之所以說它是最基礎的收集算法,是因為後續的收集算法都是基於這種思路並對其不足進行改進而得到的。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
為了解決效率問題,一種稱為“復制”(Copying)的收集算法出現了,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半,未免太高了一點。現在的商業虛擬機都采用這種收集算法來回收新生代。
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
當前商業虛擬機的垃圾收集都采用“分代收集”(Generational Collection)算法,這種算法並沒有什麼新的思想,只是根據對象存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。
如果說收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現。 Java虛擬機規范中沒有規定垃圾收集器如何實現,所以不同的廠商,不同版本的虛擬機提供的垃圾收集器是有很大區別的。這裡討論的收集器基於Sun HotSpot虛擬機1.6版 Update 22。
Serial收集器:新生代收集器,使用停止復制算法,使用一個線程進行GC,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內存回收(這也是虛擬機在Client模式下運行的默認值)
ParNew收集器:新生代收集器,使用停止復制算法,Serial收集器的多線程版,用多個線程進行GC,其它工作線程暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。
Parallel Scavenge 收集器:新生代收集器,使用停止復制算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間,比如:JVM運行100分鐘,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行後台運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合用戶交互,提高用戶體驗)。使用-XX:+UseParallelGC開關控制使用 Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間占總時間的比例,默認99,即 1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數只對Parallel Scavenge有效)
Serial Old收集器:老年代收集器,單線程收集器,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,只留幸存 的對象,壓縮是將移動對象,將空間填滿保證內存分為2塊,一塊全是對象,一塊空閒)算法,使用單線程進行GC,其它工作線程暫停(注意,在老年代中進行標 記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
Parallel Old收集器:老年代收集器,多線程,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這裡的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復制到預先准備好的區域,而不是像Sweep(清 理)那樣清理廢棄的對象)算法,在Parallel Old執行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現後(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現Parallel Scavenge收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力於獲取最短回收停頓時間,使用標記清除算法,多線程,優點是並發收集(用戶線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內存回收,優先使用ParNew+CMS(原因見後面),當用戶線程內存不足時,采用備用方案Serial Old收集。
G1收集器:在JDK1.7中正式發布,與現狀的新生代、老年代概念有很大不同,目前使用較少,不做介紹。CMS收集的方法是:先3次標記,再1次清除,3次標記中前兩次是初始標記和重新標記(此時仍然需要停止(stop the world)), 初始標記(Initial Remark)是標記GC Roots能關聯到的對象(即有引用的對象),停頓時間很短;並發標記(Concurrent remark)是執行GC Roots查找引用的過程,不需要用戶線程停頓;重新標記(Remark)是在初始標記和並發標記期間,有標記變動的那部分仍需要標記,所以加上這一部分 標記的過程,停頓時間比並發標記小得多,但比初始標記稍長。在完成標記之後,就開始並發清除,不需要用戶線程停頓。
所以在CMS清理過程中,只有初始標記和重新標記需要短暫停頓,並發標記和並發清除都不需要暫停用戶線程,因此效率很高,很適合高交互的場合。
CMS也有缺點,它需要消耗額外的CPU和內存資源,在CPU和內存資源緊張,CPU較少時,會加重系統負擔(CMS默認啟動線程數為(CPU數量+3)/4)。
另外,在並發收集過程中,用戶線程仍然在運行,仍然產生內存垃圾,所以可能產生“浮動垃圾”,本次無法清理,只能下一次Full GC才清理,因此在GC期間,需要預留足夠的內存給用戶線程使用。所以使用CMS的收集器並不是老年代滿了才觸發Full GC,而是在使用了一大半(默認68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction來設置)的時候就要進行Full GC,如果用戶線程消耗內存不是特別大,可以適當調高-XX:CMSInitiatingOccupancyFraction以降低GC次數,提高性能,如果預留的用戶線程內存不夠,則會觸發Concurrent Mode Failure,此時,將觸發備用方案:使用Serial Old 收集器進行收集,但這樣停頓時間就長了,因此-XX:CMSInitiatingOccupancyFraction不宜設的過大。
還有,CMS采用的是標記清除算法,會導致內存碎片的產生,可以使用-XX:+UseCMSCompactAtFullCollection來設置是否在Full GC之後進行碎片整理,用-XX:CMSFullGCsBeforeCompaction來設置在執行多少次不壓縮的Full GC之後,來一次帶壓縮的Full GC。
(1) Young(年輕代):主要是用來存放新生的對象。對象被創建時,內存的分配首先發生在年輕代(大對象可以直接被創建在年老代),大部分的對象在創建後很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消亡的),這個GC機制被稱為Minor GC或叫Young GC。注意,Minor GC並不代表年輕代內存不足,它事實上只表示在Eden區上的GC。
年輕代上的內存分配是這樣的,年輕代可以分為3個區域:Eden區(用來表示內存首次分配的區域)和兩個存活區(Survivor 0 、Survivor 1)。
絕大多數剛創建的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,因此在其上分配內存極快;
當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩余的對象復制到一個存活區Survivor0(此時,Survivor1 是空白的,兩個Survivor總有一個是空白的);
此後,每次Eden區滿了,就執行一次Minor GC,並將剩余的對象都添加到Survivor0;
當Survivor0 也滿的時候,將其中仍然活著的對象直接復制到Survivor1,以後Eden區執行Minor GC後,就將剩余的對象添加Survivor1(此時,Survivor0是空白的);
當兩個存活區切換了幾次(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代)之後,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被復制到老年代。
(2)Old(年老代):主要存放應用程序中生命周期長的內存對象。
對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次 Young GC 後存活了下來),則會被復制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時, 將執行Major GC,也叫 Full GC。
可以使用-XX:+UseAdaptiveSizePolicy開關來控制是否采用動態控制策略,如果動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。
如果對象比較大(比如長字符串或大數組),Young空間不足,則大對象會直接分配到老年代上(大對象可能觸發提前GC,應少用,更應避免使用短命的大對象)。用-XX:PretenureSizeThreshold 來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。
可能存在年老代對象引用新生代對象的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這裡。Young GC時,只要查這裡即可,不用再去查全部老年代,因此性能大大提高。
(3)Permanent(永久代):是指內存的永久保存區域,也就是方法區,主要存放Class和Meta的信息,Class在被 Load的時候被放入PermGen space區域. 它和和存放Instance的Heap區域不同,GC(Garbage Collection)不會在主程序運行期對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤。
永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:
類的所有實例都已經被回收; 加載類的ClassLoader已經被回收; 類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)。永久代的回收並不是必須的,可以通過參數來設置是否對類進行回收。HotSpot提供-Xnoclassgc進行控制。
使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看類加載和卸載信息-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持。
堆設置
-Xms :初始堆大小
-Xmx :最大堆大小
-XX:NewSize=n :設置年輕代大小
-XX:NewRatio=n: 設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
-XX:SurvivorRatio=n :年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
-XX:MaxPermSize=n :設置持久代大小
收集器設置
-XX:+UseSerialGC :設置串行收集器
-XX:+UseParallelGC :設置並行收集器
-XX:+UseParalledlOldGC :設置並行年老代收集器
-XX:+UseConcMarkSweepGC :設置並發收集器
垃圾回收統計信息
-XX:+PrintHeapAtGC GC的heap詳情
-XX:+PrintGCDetails GC詳情
-XX:+PrintGCTimeStamps 打印GC時間信息
-XX:+PrintTenuringDistribution 打印年齡信息等
-XX:+HandlePromotionFailure 老年代分配擔保(true or false)
並行收集器設置
-XX:ParallelGCThreads=n :設置並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n :設置並行收集最大暫停時間
-XX:GCTimeRatio=n :設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)
並發收集器設置
-XX:+CMSIncrementalMode :設置為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n :設置並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。