1. 程式人生 > >java之JVM學習--簡單了解GC算法

java之JVM學習--簡單了解GC算法

救贖 osgi -xms 查看類 nor blog 虛擬機棧 頻繁 lur

JVM內存組成結構:

技術分享圖片

(1)堆

所有通過new創建的對象都是在堆中分配內存,其大小可以通過-Xmx和-Xms來控制,堆被劃分為新生代和舊生代,新生代又被進一步劃分為Eden和Survivor區。Survivor被劃分為from space 和 to space組成,結構圖如下:

技術分享圖片

(2)棧

每個線程 執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包含局部變量區和操作數棧。用於存放此次方法調用過程中的臨時變量,參數和中間結果

(3)本地方法棧

用於支持native方法的執行。存儲了每個native方法調用的狀態

(4)方法區

存放了要加載的類信息,靜態變量,final類型的常量,屬性和方法信息。JVM用持久代(permanet generation)來存放方法區,可通過-XX:PermSize和 -XX:MaxPermSize來指定最小值和最大值。

(5)程序計數器

每個線程私有,當前線程執行的字節碼的行數。

JAVA堆內存分配機制

java內存分配和回收概括地說:就是分代分配,分代回收。對象將根據存貨的時間被分為:young generation, old generation,permanent generation。

技術分享圖片

yong generation:對象被創建時,內存的分配首先發生在年輕代(大對象可以直接創建在old generation),大部分的對象在創建後很快不再使用,因此很快變得不可達,被young generation 的GC機制清理掉(IBM的研究表示,98%的對象都是很快消亡的),這個GC機制被稱為Minor GC或者 Young GC;Minor GC並不代表內存不足。

young generation分為 3個區域, eden區,兩個 survivor區(from survivor, to survivor),內存分配過程如下所示:

技術分享圖片

1.絕大多數對象剛創建被分配在 eden區,其中的大多數對象很快就會消亡,eden區域是連續的內存空間,在其上分配內存極快。

2.最初一次,當eden區滿的時候,執行 minor GC,將消亡的對象清理掉,並將eden,survivor 1剩余的對象復制到到一個存活區 Survivor 0(此時Survivor 1是空白的,兩個Survivor總有一個是空白的)

3.下次eden滿了,在執行一次 minor GC,將消亡的對象清理掉,存活的對象復制到survivor1中,清空eden區。將survivor 0 中消亡的對象清理掉,將其中可以晉級的對象晉級到old區,將存活的對象也復制到survivor 1中,清空survivor 0

4.當被兩個存活期 來回復制了幾次之後,(用-XX:maxTenuringThreshold 控制,大於該值進入old generation,但是這只是個最大值,並不代表一定是這個值,因為:為了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。)仍然存活的對象,將被復制到old generation。

old generation:對象如果在young generation 存活了足夠長的時間而沒有被清理掉,則會被復制到old generation,old generation 的空間一般比young generation大得多,發生的GC次數也比年輕代少,當年老代內存不足時,將執行 Major GC,也叫 Full GC;

可以使用-XX:+UseAdaptiveSizePolicy開關來控制是否采用動態控制策略,如果動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。

如果對象比較大,young generation空間不足,則大對象會直接分配到old generation(大對象可能提前觸發GC,應盡少使用大對象,更少用短命的大對象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。

可能存在年老代對象引用新生代對象的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這裏。Young GC時,只要查這裏即可,不用再去查全部老年代,因此性能大大提高。

JAVA 不同代 GC 機制

young generation:發生的GC是minor GC,使用停止-復制算法進行清理,將新生代內存分為:較大的eden,和兩個相等survivor,每次進行清理時,把eden 和一個survivor中存活的對象 復制到另一個survivor中,如果存活的對象超過了survivor內存,則需要通過空間分配擔保機制將一部分對象復制到old generation,然後清理掉eden,和剛才的survivor。可以通過 -XX:SurvivorRation參數來調整eden和survivor區的內存容量比值。默認是8,eden:survivor:survivor = 8:1:1。

old generation:發生major GC。存儲的對象比young generation多得多,且存在很多大對象,對old generation進行內存清理,如果使用 停止-復制算法,相當低效,一般使用 標記-整理算法,標記出仍然存活的對象(存在引用),將所有存活的對象向一端移動,以保證內存的連續。

在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩余空間大小,如果大於,則直接觸發一次Full GC,否則,就查看是否設置了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍內存分配失敗;如果不允許,則仍然進行Full GC(這代表著如果設置-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多內存,所以,最好不要這樣做)。

old generation GC之 標記-清除法:標記出所有需要回收的對象(可達性分析),標記完成後統一清理掉所有被標記的對象。

技術分享圖片

該算法有兩個問題:

(1)下頻率問題:標記和清除過程效率都不高

(2)空間問題:標記清除後會產生大量的不連續的內存碎片,空間碎片太多會導致在運行過程需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集。

old generation GC之 標記整理算法:標記過程和標記清除算法一樣,但是後續步驟不再對可回收對象直接清理,而是讓所有存活對象都向一端移動,然後清理掉邊界以外的內存。

技術分享圖片

permanent generation:永久代的垃圾收集分為兩類:廢棄的常量和不再被使用的類。

廢棄常量:比如說我們在常量池中用intern()添個字符串常量“a”,但是現在的系統沒有任何一個String對象叫“a”,所以這個常量就是廢棄了。

不再被使用的類:①該類的所有的實例都已經被回收,Java堆中不存在該類的任何實例。

②加載類的ClassLoader已經被回收

③沒有該類的java.lang.Class對象被引用,即不能通過反射訪問該類信息。

滿足了上述三個條件只是滿足了類回收的基本條件,是否回收不用的類需要看設置的-Xnoclassgc參數進行控制,還可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看類的加載和卸載信息。

在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

算法分析:

空間分配擔保:

在執行Minor GC前, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活對象, 由於新生代使用復制收集算法, 為了提升內存利用率, 只使用了其中一個Survivor作為輪換備份, 因此當出現大量對象在Minor GC後仍然存活的情況時, 就需要老年代進行分配擔保, 讓Survivor無法容納的對象直接進入老年代, 但前提是老年代需要有足夠的空間容納這些存活對象. 但存活對象的大小在實際完成GC前是無法明確知道的, 因此Minor GC前, VM會先首先檢查老年代連續空間是否大於新生代對象總大小或歷次晉升的平均大小, 如果條件成立, 則進行Minor GC, 否則進行Full GC(讓老年代騰出更多空間).然而取歷次晉升的對象的平均大小也是有一定風險的, 如果某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然可能導致擔保失敗(Handle Promotion Failure, 老年代也無法存放這些對象了), 此時就只好在失敗後重新發起一次Full GC(讓老年代騰出更多空間).

GC回收對象確立:

引用計數:如果有引用這個對象的,對象計數器+1,引用失效,計數器-1,絕大多數情況下,這個算法高效簡單,但是不能解決對象之間的循環引用的關系,所以沒有被主流語言采用。

可達性算法:通過一系列的稱為 GC Roots 的對象作為起點, 然後向下搜索; 搜索所走過的路徑稱為引用鏈/Reference Chain, 當一個對象到 GC Roots 沒有任何引用鏈相連時, 即該對象不可達, 也就說明此對象是不可用的, 如下圖: Object5、6、7 雖然互有關聯, 但它們到GC Roots是不可達的, 因此也會被判定為可回收的對象:

技術分享圖片

在Java, 可作為GC Roots的對象包括:

  1. 方法區: 類靜態屬性引用的對象;
  2. 方法區: 常量引用的對象;
  3. 虛擬機棧(本地變量表)中引用的對象.
  4. 本地方法棧JNI(Native方法)中引用的對象。

可達性算法如果對象到GC Roots不可達也不是說這個對象會立即被回收,是需要經過一個兩次標記過程的,第一次:是在可達性分析後發現沒有與GC Roots相連接的引用鏈。第二次是判斷是否需要執行finalize(),這個方法也是對象唯一能夠進行自我救贖的機會了,但是不推薦使用,因為這個方法運行代價高,不確定性大,不能保證不同對象的執行順序。如果不需要執行該方法,直接就進行回收,如果需要執行該方法,那麽該對象會被放在一個F-Queue裏。雖然會被執行,但是不一定保證能夠執行成功,因為有可能會在這個方法執行過程中出現死循環等意外情況,所以虛擬機並不一定會等待這個方法執行結束才進行回收。如果在第二次標記的時候,該對象沒有成功的自我拯救,那麽就真的被回收了。

常用JVM配置參數

-XX:CMSInitiatingPermOccupancyFraction當永久區占用率達到這一百分比時,啟動CMS回收
-XX:CMSInitiatingOccupancyFraction設置CMS收集器在老年代空間被使用多少後觸發
-XX:+CMSClassUnloadingEnabled允許對類元數據進行回收
-XX:CMSFullGCsBeforeCompaction設定進行多少次CMS垃圾回收後,進行一次內存壓縮
-XX:NewRatio:新生代和老年代的比
-XX:ParallelCMSThreads設定CMS的線程數量
-XX:ParallelGCThreads設置用於垃圾回收的線程數
-XX:SurvivorRatio設置eden區大小和survivior區大小的比例
-XX:+UseParNewGC在新生代使用並行收集器
-XX:+UseParallelGC 新生代使用並行回收收集器
-XX:+UseParallelOldGC老年代使用並行回收收集器
-XX:+UseSerialGC在新生代和老年代使用串行收集器
-XX:+UseConcMarkSweepGC新生代使用並行收集器,老年代使用CMS+串行收集器
-XX:+UseCMSCompactAtFullCollection設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片的整理
-XX:+UseCMSInitiatingOccupancyOnly表示只在到達閥值的時候,才進行CMS回收

(+不能省)
-Xms:設置堆的最小空間大小。
-Xmx:設置堆的最大空間大小。
-XX:NewSize設置新生代最小空間大小。
-XX:MaxNewSize設置新生代最大空間大小。
-XX:PermSize設置永久代最小空間大小。
-XX:MaxPermSize設置永久代最大空間大小。
-Xss:設置每個線程的堆棧大小

java之JVM學習--簡單了解GC算法