到底誰是辣雞?(對象是否存活和GC日誌分析)

垃圾收集要搞清楚的三件事(除了方法區回收其他都針對對象也就是堆區的回收)

垃圾收集(Carbage Collection,GC),垃圾收集需要考慮三件事:
1.哪些內存需要回收

方法計數器、虛擬機棧、本地方法棧三個區域隨着線程生和滅,每一個棧幀所分配的內存在編譯器大體上都是可知的,內存的回收和分配都具備確定性,所以不需要過多的考慮回收問題,因爲方法結束或者是線程結束內存自動就會回收了。

方法區和堆區(垃圾收集區域需要回收)的內存分配則不一樣,接口的多個實現、方法的多個分支佔用的內存都可能不一樣,只有在運行期才知道哪些對象會創建,這部分的內存回收和分配都是動態的,所以垃圾收集器關注的是這部分內存。

2.什麼時候回收

對象死亡時回收(不被任何途徑使用)。

3.如何回收

垃圾收集算法。

判斷對象是否存活(什麼時候回收和GC日誌開啓方式以及日誌分析)

1.引用計數法

原理:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能被再使用的。

優點:實現簡單、判定效率高。

缺點:對象間相互循環引用問題無法解決,
在下邊的栗子裏objA和objB的instance字段相互持有對方的引用,除此之外沒有別的任何引用,而且兩個對象已經不可能被訪問,但是相互引用着對方,計數器都不是0,於是引用計數法無法告知GC去回收內存。

舉個官方栗子:

public class GCObj{
    public Object instance = null;
    private static final int _1MB = 1024*1024;
    //判斷對象的這部分內存是否會被回收,判斷是不是使用的引用計數法
    private byte[] isGCByte = new byte[2*_1MB];

    public static void main(String[] args) {
       GCObj objA = new GCObj();
       GCObj objB = new GCObj();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        //假設在此GC,objA和objB是否會被回收
        System.gc();
    }
}

GC日誌

[GC (System.gc()) [PSYoungGen: 7270K->664K(36864K)] 7270K->664K(121856K), 0.0181725 secs] [Times: user=0.02 sys=0.00, real=0.03 secs] 

[Full GC (System.gc()) [PSYoungGen: 664K->0K(36864K)] [ParOldGen: 0K->616K(84992K)] 664K->616K(121856K), [Metaspace: 3155K->3155K(1056768K)], 0.0915562 secs] [Times: user=0.11 sys=0.00, real=0.09 secs] 

觀察日誌可分析出(此處涉及到GC日誌的分析,以後單獨介紹),虛擬機並沒有因爲兩個對象相互引用就不回收他們,這也說明虛擬機沒有使用引用計數法判斷對象存活。

GC日誌分析:
首先如何讓代碼運行時打印GC日誌:
日誌分析

點擊Edit Configurations…
設置VM optional參數

然後設置VM optinals參數:-XX:+PrintGCDetails
新生代老年代,GC,對內存分配

堆設置
-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數。並行收集線程數。

更多設置參考:IDEA和Eclipse GC日誌分析打印開啓方法和參數設置

書上的GC日誌
針對以上書中的例子來具體分析:
33.125 和 100.667 代表了GC發生的時間,這個數字代表從Java虛擬機啓動以來經過的秒數。
[GC 和[Full GC 代表了GC的停頓類型並不是來區分GC發生在新生代還是老年代的,如果有Full說明本次GC發生了Stop The World (簡稱STW)的停頓。

ParNew收集器中的GC
ParNew收集器中的GC也出現Full,這一般是出現了擔保失敗之類的問題出現了STW
如果是調用System.gc()方法觸發的GC,則如我們上邊運行的程序中的那樣,[GC (System.gc()) ,[Full GC (System.gc())

[DefNew(新生代)、[Tenured(老年代)、[Perm(永久代)表示GC發生的區域
在Serial收集器中顯示[DefNew(Default New Generation)
ParNew收集器中顯示[ParNew(Parallel New Generation)
Parallel Scavenge收集器中顯示[PSYoungGen
老年代和永久帶一致都是跟收集器決定的

後邊方括號中的3324k->152k(3712k)含義是:“GC前該內存區域使用量->GC後該內存區域使用量(該內存區域總容量)”

方括號之外的3324k->152k(11904k)的含義是:“GC前Java堆的已使用量”->GC後Java堆的已使用量(Java堆的總容量)

再往後,0.0025925secs表示此次GC所佔時間單位是秒,有的收集器(需要在VM optional中設置)可以顯示更具體的時間數據,如:[Time:
user=0.01 sys=0.01,real=0.02 secs]這裏邊的 user、sys、real和Linux的time命令輸出的時間含義一致分別代表:用戶態消耗的CPU時間(user)、內核態消耗的CPU時間(sys)、操作從開始到結束所經過的牆鍾時間(Wall Clock Time)(real)
牆鍾時和CPU時間的區別:
牆鍾時間包括各種非運算的等待耗時,例如磁盤IO、等待線程阻塞
而CPU時間則不包括這些耗時
但是當有多CPU或者多核時多線程操作會疊加這些CPU時間
所以有時user或者sys的事件會超過real的事件。

2.可達性分析算法

通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時(就是從GC Roots 到這個對象是不可達),則證明此對象是不可用的。所以它們會被判定爲可回收對象(例如圖中的Obj5、Obj6、Obj7對象既是不可達的)。

可達性分析圖

在Java語言中,可作爲GC Roots對象的包括下面一下幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類能靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(Native方法)引用的對象

在可達性分析算法中,要真正宣告一個對象死亡,至少要經歷兩次標記過程:

1.(緩刑)如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有 覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。

2.(對象自我拯救)如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫做F-Queue隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。finalize()方法是對象逃脫死亡命運的最後一次機會,稍候GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將會被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。

對象可以在被GC時自我拯救,這種自救機會只有一次,因爲一個對象的finalize()方法最多隻會被系統調用一次。

判斷對象是否存活與“引用”有關(補充)

在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。

強引用(Strong Reference):就是指在程序代碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用(Soft Reference):用來描述一些還有用但並非必須的對象。在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。

弱引用(Weak Reference):用戶描述非必須對象的。被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

虛引用(Phantom Reference):一個對象是否有虛引用存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用的唯一目的就是能在這個對象被收集器回收時刻得到一個系統通知。

方法區回收(擴展)

方法區(在Hotpot虛擬機中的永久代),垃圾收集效率要低於堆中對象的收集效率(70%-90%),永久代中回收的主要內容爲兩塊:廢棄常量和無用類

  • 廢棄常量回收:以常量池中的字面量爲例,如字符串常量“abc”,沒有任何String對象引用常量池中的“abc”通俗點說就是沒有任何String字符串叫“abc”,此時abc就會被清理出常量池,其他類(接口)、方法、字段的符號引用也是如此。
  • 無用類的回收:滿足三個條件①該類的所有實例都已經被回收,也就是Java堆中不存在此類的任何實例②加載該類的ClassLoarder已經被回收③該對象的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。此三項只代表該類可以回收,要想真正回收類,還要在虛擬機參數中設置:-Xnoclassgc

參考:《深入理解Java虛擬機第二版:JVM高級特性與最佳實踐》
http://blog.csdn.net/u014381710/article/details/48554465

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