聲明:此篇文章是讀《深入理解JAVA虛擬機》的筆記
1. 對象已死?
??堆中幾乎存放著Java中所有的對象實例,垃圾收集器在回收前,如何判斷哪些對象是活著,哪些對象已經死去?
-
引用計數算法
??給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的對象就是不可能再被使用的。
??但是,Java語言中沒有選用引用計數算法來管理內在,其中最主要的原因是它很難解決對象之間的相互循環引用的問題。
??模擬代碼如下:
/**
*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;
//占用空間
private byte[] bigSize = new byte[2*_1MB];
public static void main(String[] args) throws InterruptedException {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
GC日志顯示:
[GC (System.gc()) [PSYoungGen: 5336K->504K(6144K)] 5336K->608K(19968K), 0.0012319 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-
根搜索算法
??這個算法的基本思路就是通過一系列的名為GC Roots的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。如圖所示,object5、object6、object7雖然互相有關聯,但是它們到GC Roots是不可達的,所以將會被判定為是可回收的對象:
根搜索算法判定對象是否可回收
??在Java語言里,可作為GC Roots的對象包括下面幾種:
??1. 虛擬機棧(棧幀中的本地變量表)中的引用的對象。
??2. 方法區中的類靜態屬性引用的對象。
??3. 方法區中的常量引用的對象。
??4. 本地方法棧中JNI引用的對象。 再談引用
??在JDK1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。
??1. 強引用就是在指程序代碼之中普遍存在的,類似Object obj= new Object()這類的引用,只要強引用存在,垃圾收集器永遠不會回收掉被引用的對象。
??2. 軟引用用來區描述一些還有用,但并非必需的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中并進行第二次回收。在JDK1.2之后,提供了SoftReference類來實現軟引用。
??3. 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2之后,提供了WeakReference類來實現弱引用。
??4. 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一個引用關系。虛引用對一個對象的生存時間完全不會影響。為一個對象設置虛引用關聯的唯一目的就是希望能在這個對象被收集器回收時收到 一個系統通知。生存還是死亡?
??在根搜索算法中不可達對象,也并非是非死不可的,虛擬機在回收對象之前會調用對象的finalize()方法,來判斷此對象是否重新被引用,前提是這個方法沒有被執行過,因為這個方法只能被虛擬機調用一次。但是此方法的運行代價大,不確認性高,所以不推薦使用。回收方法區
??永久代(元空間)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。
??當在發生內存回收的時候,常量池中的某些常量沒有被任何地方引用,那這個常量就會被請出常量池。
??如果要判定類是否為無用的類,條件要苛刻的多:
??1. 該類所有的實例都已經被回收。
??2. 加載該類的ClassLoader已經被回收。
??3. 該類對應的java.lang.Class對象沒有在任何地主被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足以上3個條件的無用類進行回收。但是對于類的回收不是必然的,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類的加載和卸載信息。
注意:在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成jsp和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保永久代(元空間)不會溢出。
2. 垃圾收集算法
-
標記-清除算法
??最基礎的收集算法,包含標記和清除兩個階段。缺點有兩個:一個是標記和清除的效率都不高;另一個是標記清除之后會產生大量不連續的內存碎片(當程序以后在分配較大的對象時,若無法找到足夠的連續的內存,就不得不提前觸發另一次垃圾回收動作)。
標記-清除算法 -
復制算法
??將可用的內存容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這種算法的代價是將內存縮小為原來的一半。
復制算法
??Tips:HotSpot虛擬機默認分為一個Eden空間和兩個Survivor。Eden空間的大小與Survivor的大小比例為8:1; -
標記-整理算法
??根據老年代的特點,提出的一種算法。先進行標記,然后把所有存活的對象都向一端移動,最后直接清理掉邊界以外的內存。
標記-整理算法 -
分代收集算法
??大部分虛擬機采用的算法,這種算法沒有什么新的思想,只是根據對象的存活周期和不同將內存劃分為幾塊。一般是把Java堆分為新生代和老生代,這樣就可以根據各個年代的特點采用最適當的收集算法。
3. 垃圾收集器
??簡單來說垃圾收集器就是內存回收的具體實現。雖然有不同的收集器,但是目前為止還沒有最好的收集器出現,也沒有萬能收集器,我們選擇的只是對具體應用最合適的收集器。
-
Serial收集器
??最基本,歷史最悠久的收集器。單線程收集器,這里的單線程不是只會使用一個CPU或者一條收集線程去完成垃圾收集工作,而在它在進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
Serial/Serial Old收集器
-XX:+UseSerialGC開啟Serial收集器。 -
ParNew收集器
??ParNew收集器其實就是Serial收集器的多線程版本,默認開啟的收集線程數與CPU數量相同,使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
ParNew收集器
-XX:+UseParNewGC開啟ParNew收集器。 -
Parallel Scavenge收集器
??同樣使用復制算法的收集器,又是并行的多線程收集器。其目標是達到一個可控制的吞吐量。所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間 +垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
Parallel Scavenge/Parallel Old收集器
Tips:
-XX:+UseParallelGC開啟Parallel收集器。
-XX:MaxGCPauseMillis用來設置最大垃圾收集停頓時間。
-XX:GCTimeRatio用來設置吞吐量的大小。
-XX:+UseAdaptiveSizePolicy打開此參數時,虛擬機可以自己調節GC策略。自適應調節策略是Parallel Scavenge收集器與ParNew收集器的一個重要區別。 Serial Old收集器
??Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-清理算法。Parallel Old收集器
??Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。在注重吞吐量及CPU敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。
-XX:+UseParallelOldGC開啟Parallel Old收集器。-
CMS收集器
??CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器是基于標記-清除算法,不過它的運作過程要復雜一些,分為4個步驟:
??1. 初始標記(CMS initial mark)
??2. 并發標記(CMS concurrent mark)
??3. 重新標記(CMS remark)
??4. 并發清除(CMS concurrent sweep)
Concurrent Mark Sweep收集器
并發收集、低停頓是CMS收集器的優點,但是它也是三個顯著的缺點:
??a. CMS收集器對CPU資源非常敏感。CMS默認啟動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,并發回收垃圾時垃圾收集線程最多占用不超過25%的CPU資。但是當CPU不足4個時,那么CMS對用戶程序的影響就可能變得很大。
??b. CMS收集器無法處理浮動垃圾,可能出現Concurrent Mode Failure失敗而導致另一次Full GC的產生。所謂的浮動垃圾是在清理期間用戶線程產生的新垃圾。也正是用戶線程還需要運行,所以還需要預留足夠的空間給用戶線程使用。可以用-XX:CMSInitiatingOccupancyFraction設置。
注意:設置的太高將會容易導致大量Concurrent Mode Failure,導致性能降低。
??c. 最后一個缺點是基于標記-清除算法的收集器,會產生大量的空間碎片。
Tips:
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC開啟CMS收集器(搭配新生代ParNew收集器)。
-XX:UseCMSCompactAtFullCollection參數用于在享受完一個FullGC后提供一次碎片整理過程。
-XX:CMSFullGCCsBeforeCompaction參數用于設置在執行多少次不壓縮的Full GC后,來一次帶壓縮的。 G1收集器
??基于標記-整理算法實現的收集器。優點:
??1. 不會產生空間碎片
??2. 可以非常精確地控制停頓
??G1收集器可以實現在基本不犧牲吞吐量的前提下完成低停頓的內存回收,這是由于它能夠極力地避免全區域的垃圾收集,之前的收集器進行收集的范圍都是整個新生代或老年代,而G1將整個Java堆(包括新生代、老年代)劃分為多個大小固定的獨立區域(Region),并且跟蹤這些區域里面的垃圾堆積程度,在后臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域。區域劃分及有優先級的區域回收,保證了G1收集器在有限的時間內可以獲得最高的收集效率。
Tips:
G1可用的命令行選項有:
-XX:+UseG1GC 讓JVM使用G1垃圾回收器。
-XX:MaxGCPauseMillis=200 設置GC暫停時間目標值,缺省200毫秒。但這不是硬指標,JVM會盡力滿足。
-XX:InitiatingHeapOccupancyPercent=45 整個堆被占用多少之后開始進行GC,缺省為45,0表示持續不停進行GC。
-XX:NewRatio=n 年輕代和老年代的比例,缺省為2。
-XX:SurvivorRatio=n Eden和Survivro的比例,缺省為8。
-XX:G1ReservePercent=n 保留的堆大小,減少晉升過程中出錯的可能性,也就是增加可用的to-space內存,缺省是10。
-XX:G1HeapRegionSize=n G1中,堆分為大小相等的區域。這個參數設置區域的大小,缺省值取決于堆的總大小,有效取值是1M-32M。