深入理解JVM之記憶體管理

541732025發表於2014-03-27


1,方法區:存放類的資訊(名稱、修飾符等)、類中的靜態變數、類中final型常量、類中的Field資訊,類中的方法資訊。方法區是全域性共享的,在一定條件下也會被GC,當方法區要使用的記憶體超過其允許的大小時,會丟擲OutOfMemory。
JVM中這塊區域對應Permanet Generation,又稱為持久代。預設最小為16MB,最大為64MB,可透過-XX:PermSize及-XX:MaxPermSize來指定最小值和最大值
2,堆:存放物件例項及陣列值,大部分new建立的物件的記憶體都在此分配,這裡也是GC最頻繁的地方。32位系統最大為2G,64位則沒限制,大小可透過-Xmx和-Xms來指定,為了避免在執行時對heap的調整,通常將-Xmx和-Xms的值設成一樣。

為了讓記憶體回收更加高效,JDK1.2開始採用分代回收,不同的區域採用不同的回收演算法。
1,新生代:大多數new物件都在此分配記憶體,新生代由Eden、兩個存活區(survivor space)構成,可透過-Xmn來指定新生代的大小,還可以透過-XX:SurvivorRatio(在Parallel Scavenge中是透過-XX:InitialSurvivorRatio設定)來調整Eden、存活區的大小比例,預設是8:1
2,老年代:用於存放新生代中經過多次垃圾回收任然存活的物件,如快取的物件,新建的物件也有可能在老年代上直接分配,這種情況分兩種,一種是大物件,直接在老年代分配,可以透過-XX:PretenureSizeThreshold指定大小閾值,但此引數在新生代採用Parallel Scavenge GC時無效Parallel Scavenge GC會根據執行狀況決定什麼物件直接在老年代分配;另一種在老年代直接分配的是大陣列,其陣列中無引用外部物件。老年代的大小為-Xmx減去-Xmn
3,本地方法棧:用於支援native方法的執行,儲存每個native方法呼叫的狀態,在Hotspot中本地方法棧和JVM方法棧是同一個。
4,PC暫存器和JVM方法棧:每個執行緒都會建立PC暫存器和JVM方法棧,PC暫存器佔用的可能是CPU暫存器或作業系統記憶體,JVM方法棧佔用作業系統記憶體,JVM方法棧為執行緒私有,其記憶體分配高效,當方法執行完畢時,其對應的棧幀所佔用的記憶體也會被自動釋放。當JVM方法棧空間不足時,會丟擲StackOverflowError,可以透過-Xss來指定方法棧的大小,如果不出現無窮遞迴,棧的深度不會太大,一般配置1M夠矣。

記憶體分配:
主要在堆上分配,堆為所有執行緒共享,因此在堆上分配記憶體時需要加鎖,這導致建立物件開銷比較大,當堆空間不足時,會觸發GC,如果GC後任然不足,則拋OutOfMemory。
為了提升記憶體分配效率,JVM會為新建立的執行緒在新生代的Eden上分配一塊獨立的空間,稱為TLAB(Thread Local Allocation Buffer),其大小由JVM根據執行情況計算而來,可透過-XX:TLABWasteTargetPrecentlaishezhi TLAB可佔用Eden的百分比,預設為1%。在TLAB上分配不需加鎖,因此效率高,JVM在給執行緒中的物件分配記憶體時會盡量在TLAB上分配,如果物件過大或TLAB空間用完,則在堆上分配。
除了在堆上分配及在TLAB上分配,還有一種基於逃逸分析直接在棧上分配的情況。

記憶體回收
收集器
1,引用計數收集器:採用計數器來判斷物件是否被引用,當計數器為0時,說明此物件已經不再被引用,可以被回收。引用計數對於迴圈引用的場景沒辦法回收,所以在Sun JDK實現GC時也未採用這種方式。
2,跟蹤收集器:全域性記錄資料的引用狀態,基於一定的條件觸發(定時、空間不足)執行時需要從根集合來掃描物件的引用關係。主要有複製、標記-清除、標記-壓縮三種實現演算法。
複製:從根集合掃描出存活的物件,並將它們複製到一塊新的空間。當回收區域存活物件較少時,複製演算法比較高效,其成本是開闢一塊空記憶體以及物件的移動。
標記-清除:從根集合掃描出存活的物件,並對它們進行標記,之後清空未標記的物件。標記-清除動作不需要物件移動,僅對不存活的物件處理,在空間中存活物件較多的情況下較為高效,但會產生記憶體碎片。
標記-壓縮:前面動作和標記-清除一樣,但是清除不存活的物件後,會將存活物件都往空閒的空間移動,並更新引用指標。成本相對較高,但避免了記憶體碎片。
根集合物件:當前執行緒棧上引用的物件、常量、靜態變數、傳到本地方法中,還沒有被本地方法釋放的物件。

JDK中的GC收集器:不同的區域採用不同的GC收集器

新生代可用GC:
新生代中的物件通常存活時間較短,因此採用複製演算法。上面提到,複製時將存活物件移動到一塊新的區域,這個區域就是新生代中的其中一個survior space。對新生代的回收又叫Minor GC。
1,序列GC(Serial GC)
當新生代不夠空間來建立物件時,就會觸發Minor GC。如果Minor GC僅從根集合物件中掃描存活物件,則當老年代中的物件引用了新生代的物件時會出問題,但老年代通常比較大,為了提高效能,也不可能每次Minor GC都會去掃描整個老年代,Sun JDK採用的是remember set方式來解決此問題。JVM在進行物件引用賦值時,會檢查賦值的物件是否在老年代,並檢查該物件是否有引用指向新生代,如果條件滿足,則在remember set做個標記。因此,對於Minor GC而言,完整的根集合為Sun JDK認為的根集合物件加上remember set中標記的物件。
為了避免在掃描過程中引用關係發生變化,採用暫停應用的方式,JDK在編譯程式碼時會為每段方法注入SafePoint,通常SafePoint位於方法中迴圈的結束
點一級方法執行完畢的點。在暫停應用時需要等待所有的使用者執行緒進入SafePoint,然後將記憶體頁設定為不可讀狀態,從而實現暫停使用者執行緒的執行。
物件引用關係:除了預設的強引用,還有軟引用、弱引用、虛引用
A a = new A(),這就是強引用,這種物件只有主動釋放引用後才會被GC
軟引用,採用SoftReference來實現,這種物件會在JVM記憶體不足時被回收,因此軟引用很適合用於實現快取,另外當GC認為掃描到的軟引用物件不經常使用時,也會被回收。
弱引用:採用WeakReference來實現,這種物件在沒有強引用後,會被回收。

在Minor GC後存活的物件並不是直接進入老年代,只有經歷過幾次Minor GC後任然存貨的物件才會進入老年代。這個在Minor GC中存活的次數在序列、ParNew方式時可透過-XX:MaxTenuringThreshhold設定,在Parallel Scavenge則由hotspot根據執行狀況來句定。當存活區已滿,剩下的存活物件則直接進入老年代。
Serial GC在整個掃描、複製過程中均採用單執行緒,適用於單CPU、新生代空間較小、及對暫停時間要求

2,並行回收GC(Parallel Scavenge)
上面已經提過,在Parallel Scavenge中,Eden,survivor的比例透過-XX:InitialSurvivorRatio來配置,預設也為8,不過,在jdk6以後,也能透過-XX:SurvivorRatio來配置了。
在啟動時Eden,survivor的比例按照配置劃分,但是在執行一段時間以後,並行回收GC會根據Minor GC的頻率,消耗的時間來動態調整比例,可以透過-XX:-UseAdaptiveSizePolicy來固定比例
在PS GC中不是透過-XX:PretenureSizeThreshold來決定物件是否在老年代直接分配的,而是當分配記憶體時,如果Eden空間不夠,而且物件大小也大於等於Eden的一半,則直接在老年代分配。
PS GC也是採用複製演算法回收垃圾,但區別於Serial GC的地方在於,其掃描和複製時均採用多執行緒方式來進行,在多CPU機器上效率更高,適合對暫停時間要求較短的應用上。PS GC也是C2級別上預設採用的GC方式。

3,並行GC(ParNew)
ParNew在SurvivorRatio的方式上和序列GC一樣。ParNew與Ps GC的區別在於ParNew必須配合老年代使用CMS GC,因為CMS GC在對老年代回收時,有些過程是併發進行的,如此時發生Minor GC,需要進行相應的處理,而PS GC是沒有做這些處理的,也正是這個原因,ParNew不可與並行的老年代GC同時使用。
在配置老年代使用CMS GC的情況下,新生代預設採用ParNew
同樣,當Eden空間不足時,會觸發Minor GC

綜上:新生代各GC器的區別在於:
1,
-XX:PretenureSizeThreshold,關於大物件在老年代分配,Serial是根據此值判斷,而Parallel Scavenge實在分配記憶體時判斷,如果Eden不夠,且物件大於Eden的一半,則直接在老年代分配
2,關於晉升老年代的條件,Serial、ParNew是設定
-XX:MaxTenuringThreshhold,熬過了一定次數的GC則晉升老年代,如果survivor已滿,則直接晉升老年代。而Parallel Scavenge則是根據執行狀況來決定。
3,ParNew的特點是必須搭配老年代的CMS GC使用
綜上,主要區別還是與老年代有關

Minor GC的觸發方式
1,Eden上分配記憶體時空間不足,觸發Minor GC
2,System.gc顯示呼叫也可以觸發Minor GC

老年代、持久代可用GC
序列、並行、併發
主要講講併發CMS GC:其它幾種回收都是stop the world,造成應用暫停,所以提供了CMS GC,它的大部分動作都能與應用併發執行。CMS GC採用Mark-Sweep
CMS分4個步驟:
1,初始標記:stop the world,也是從根集合出發,掃描
2,併發標記:並行執行,標記上一步存活物件的引用的物件
3,再次標記:stop the world,因為上一步中可能會有新物件建立,或者物件引用改變,所以要對這些物件進行掃描
4,併發收集

CMS GC觸發方式有2種
1,如果老年代使用CMS,則可設定CMSInitiatingOccupancyFraction百分比,當老年代空間使用達到某個值時,觸發
2,還一種是JVM自動觸發,基於之前GC的頻率以及老年代的增長趨勢來決定

Full GC
除CMS GC之外,當老年代、持久代發生GC時,其實是對新生代、老年代、持久代都進行GC,因此又叫Full GC。
以下情況會觸發Full GC:
1,老年代空間不足(新生代晉升、直接老年代建立大物件),會觸發Full GC,若GC後空間還不夠,則拋OutOfMemory:java heap space
2,Permanent Generation空間滿,該區域存放class資訊,當空間滿,則觸發Full GC,如果GC後任然不夠,則拋OutOfMemory:PermGen space
3,CMS GC時出現promotion failed和concurrent mode failure。在執行Minor GC(對應ParNew)時,晉升老年代的物件過多;或者執行CMS GC時同時有物件要放入老年代,而老年代空間有不足,這兩種CMS情況會觸發Full GC
4,統計得到Minor GC晉升到老年代的平均大小大於老年代空間,在進行Minor GC時,如果之前統計得到Minor GC晉升到老年代的平均大小大於老年代剩餘空間,則觸發Full GC。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28912557/viewspace-1130874/,如需轉載,請註明出處,否則將追究法律責任。

相關文章