JVM總結之GC

哪些內存須要回收

在Java堆中存放着幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要知道哪些對象還「存活着」,哪些對象已經」死去「。java

引用計數算法

引用計數法的實現:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1,當引用失效時,計數器就減1,只要計數器爲0的對象就是不可能被使用的。算法

這個算法實現簡單,效率也很高,可是當存活對象中,存在相互引用的時候,這算法就解決不了。因此Java中的GC並無採用引用計數法來管理內存。(後面例子分析會根據GC日誌看出相互引用的對象被回收了)數組

可達性分析算法

以GC Roots對象做爲起始點,從這些節點依次向下搜索,若是當前對象到GC Roots沒有任何的路徑相連(對象不可達)時,那麼,當前對象沒有引用。安全

在Java中,如下對象可做爲GC Roots:工具

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

當這些對象不可達的時候,也並非就能夠宣告這個對象就死亡了。還有對象的最後一次自我救贖——finalize。優化

finalize是一個方法名,Java容許使用finalize方法在垃圾收集器將對象從內存中清除出去以前作必要的清理工做。在這個操做中若是對象被從新引用,對象就能夠活過來了。spa

判斷對象是否有必要執行該方法主要有如下兩個依據:線程

  • 對象有沒有覆蓋finalize方法;
  • 對象已覆蓋finalize方法,檢查finalize方法是否被虛擬機調用過,若是已被調用,就不須要再次執行。

若是該對象有必要執行finalize方法,則該被對象將被放置到F-Queue中,由虛擬機的單獨線程Finalizer執行。日誌

注意:finalize方法只能被執行一次,若是面臨下一次回收,finalize方法將不會被執行。code

什麼時候回收

young gc

對於 young gc,觸發條件彷佛要簡單不少,當 eden 區的內存不夠時,就會觸發young gc。

full gc

1. old gen 空間不足

當建立一個大對象、大數組時,eden 區不足以分配這麼大的空間,會嘗試在old gen 中分配,若是這時 old gen 空間也不足時,會觸發 full gc,爲了不上述致使的 full gc,調優時應儘可能讓對象在 young gc 時就可以被回收,還有不要建立過大的對象和數組。

2. 統計獲得的 young gc 晉升到 old gen的對象平均總大小大於old gen 的剩餘空間

當準備觸發一次 young gc時,會判斷此次 young gc 是否安全,這裏所謂的安全是當前老年代的剩餘空間能夠容納以前 young gc 晉升對象的平均大小,或者能夠容納 young gen 的所有對象,若是結果是不安全的,就不會執行此次 young gc,轉而執行一次 full gc

3. perm gen 空間不足

若是有perm gen的話,當系統中要加載的類、反射的類和調用的方法較多,並且perm gen沒有足夠空間時,也會觸發一次 full gc

4. ygc出現 promotion failure

promotion failure 發生在 young gc 階段,即 cms 的 ParNewGC,當對象的gc年齡達到閾值時,或者 eden 的 to 區放不下時,會把該對象複製到 old gen,若是 old gen 空間不足時,會發生 promotion failure,並接下去觸發full gc

如何回收

標記-清除算法

標記清除算法分爲」標記「和」清除「兩個階段:首先標記處全部須要回收的對象,在標記完成後統一回收全部被標記的對象。

它的不足有兩個:

  1. 效率問題,標記和清除兩個過程的效率都不高。
  2. 空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後的程序在運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

複製算法

爲了解決效率問題,」複製「算法出現了,將可用的內存劃分爲兩塊,每次只使用其中一塊, 當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。這樣就不用考慮內存碎片等複雜狀況。

如今的商業虛擬機都採用這這種收集算法來回收新生代。根據研究代表,新生代中98%的對象都是」朝生夕死「的,因此新生代中將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間(from和to),每次使用Eden和其中一塊Survivor,當回收時,將Eden和剛纔用過的Survivor中還存活的對象一次性的複製到另一個Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。

Hotspot虛擬機默認使用Eden和Survivor的大小比例是8:1,也就是Eden佔8,form和to各佔1。

當Survivor空間不夠的時候,須要依賴老年代進行分配擔保。

在發生Minor GC以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間,若是大於,則這次Minor GC是安全的;若是小於,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小,若是大於,則嘗試進行一次Minor GC,但此次Minor GC依然是有風險的;若是小於或者HandlePromotionFailure=false,則改成進行一次Full GC。

標記-整理算法

標記整理算法的「標記」過程和標記-清除算法一致,只是後面並非直接對可回收對象進行整理,而是讓全部存活的對象都向一段移動,而後直接清理掉端邊界之外的內存。

分代收集算法

當前商業虛擬機的垃圾收集都採用」分代收集「算法,其主要思想是將Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適合的收集算法。

在新生代採用複製算法,上面已經講過了。而老年代由於對象存活率高,沒有額外空間爲它進行分配擔保,就必須使用」標記清理「或者」標記整理「算法來進行回收。

例子分析(查看GC日誌)

GC日誌是一個很重要的工具,它準確記錄了每一次的GC的執行時間和執行結果,經過分析GC日誌能夠優化堆設置和GC設置,或者改進應用程序的對象分配模式。

JVM的GC日誌的主要參數包括以下幾個:

  • -XX:+PrintGC 輸出GC日誌
  • -XX:+PrintGCDetails 輸出GC的詳細日誌
  • -XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
  • -XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC 在進行GC的先後打印出堆的信息
  • -Xloggc:../logs/gc.log 日誌文件的輸出路徑

引用計數算法

public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /**
     * 這個成員屬性的惟一意義就是佔點內存,以便能在GC日誌中看清楚是否被回收過
     */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        //假設在這行發生了GC,objA和ojbB是否被回收
        System.gc();
    }

}

設置JVM運行參數:

-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-verbose:gc
-Xloggc:gc.log

獲得GC日誌,咱們分析當調用System.gc()的時候,objA和ojbB是否被回收。

2017-07-01T12:23:12.844-0800: 0.268: [Full GC (System.gc()) [PSYoungGen: 624K->0K(38400K)] [ParOldGen: 8K->530K(87552K)] 632K->530K(125952K), [Metaspace: 3043K->3043K(1056768K)], 0.0059705 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap after GC invocations=2 (full 1):
 PSYoungGen      total 38400K, used 0K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
  eden space 33280K, 0% used [0x0000000795580000,0x0000000795580000,0x0000000797600000)
  from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
  to   space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
 ParOldGen       total 87552K, used 530K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
  object space 87552K, 0% used [0x0000000740000000,0x0000000740084858,0x0000000745580000)
 Metaspace       used 3043K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 336K, capacity 386K, committed 512K, reserved 1048576K
}

從日誌中[PSYoungGen: 624K->0K(38400K)]能夠看出虛擬機並無由於a和b互相引用就不回收它們,這也說明虛擬機並非經過引用計數算法來判斷對象是否存在引用的。

新生代MInor GC

經過此例能夠分析JVM的內存分配和回收策略:對象優先在Eden分配。

public class TestAllocation {

    private static final int _1MB = 1024 * 1024;

    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];  // 出現一次Minor GC
    }

    public static void main(String[] args) {
        testAllocation();
    }
}

設置JVM運行參數:

-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8

參數解釋:在運行時經過-Xms20M、-Xmx20M、-Xmn10M這三個參數限制了Java堆的大小爲20M,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。XX:SurvivorRatio=8決定了新生代的中Eden區和一個Survivor區的空間比例爲8:1。

運行結果:

Heap
 PSYoungGen      total 9216K, used 7799K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 95% used [0x00000007bf600000,0x00000007bfd9dda0,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
 ParOldGen       total 10240K, used 4096K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 40% used [0x00000007bec00000,0x00000007bf000010,0x00000007bf600000)
 Metaspace       used 3114K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 342K, capacity 386K, committed 512K, reserved 1048576K

執行testAllocation()中分配allocation4對象的語句時會發生一次MinorGC,此次GC的結果是新生代6651KB變爲148KB,而總內存佔用量則幾乎沒有減小(由於allocation一、allocation二、allocation3三個對象都是存活的,虛擬機幾乎沒有佔到可回收的對象)。此次GC發生的緣由是給allocation4分配內存的時候,發現Eden已經被佔用了6MB,剩餘空間已不足以分配allocation4所需的4MB內存,一次發生了MInorGC。GC期間虛擬機又發現已有的3個2MB所需的對象所有沒法放入Survivor空間(Survivor空間只有1MB大小),因此只能經過分配擔保機制提早轉移到老年代中去。

因此此次GC的結果就是4MB的allocation4對象順利分配在Eden中,Survivor空閒,老年代被佔用6MB(被allocation一、allocation二、allocation3佔用)。

調優

GC優化是無可奈何才採用的手段,多數致使GC問題的Java應用,都不是由於參數設置不當,而是代碼問題。因此在實際使用中,分析GC狀況優化代碼比優化GC參數要多得多。

GC優化的目的有兩個

  1. 將轉移到老年代的對象數量下降到最小;
  2. 減小full GC的執行時間;

爲了達到上面的目的,通常地,你須要作的事情有:

  1. 減小使用全局變量和大對象;
  2. 調整新生代的大小到最合適;
  3. 設置老年代的大小爲最合適;
  4. 選擇合適的GC收集器

參考資料:

《深刻理解Java虛擬機》

相關文章
相關標籤/搜索