1、對象是否存活
1.1 可達性分析算法
(1) java是通過可達性分析來判定對象是否存活:通過GC Roots的對象作為起始點向下搜索,搜索走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,證明該對象不可用。如上圖所示,Object5和Object6到GC Roots沒有引用鏈,它們將被判定為可回收對象
(2) 可作為GC Roots的對象包括:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區中靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI(即一般說的Native方法)引用的對象
1.2 引用概念的擴充
(1) 強引用:類似Object obj = new Object()的引用,只要強引用還在,永遠不會被GC回收
(2) 軟引用:通過SoftReference實現軟引用,虛擬機將要拋出OutOfMemoryError異常之前,將會發起一次GC動作,回收掉所有非強引用的對象
(3) 弱引用:通過WeakReference實現弱引用,比軟引用更弱,無論當前內存是否足夠,都會回收掉被弱引用關聯的對象
(4) 虛引用:通過PhantomReference實現虛引用,最弱的一種引用,唯一作用是該對象回收時收到一個通知
1.3 對象的自我拯救 - finalize方法
被虛擬機判定為不可達的對象,還有一次拯救自己的機會。如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,將會被第一次標記,并進行一次篩選,如果該對象沒有重寫finalize()方法,或者finalize()方法被虛擬機調用過,將視為不必執行。
如果被判定為有必要執行finalize()方法,該對象將會放入F-Queue隊列,并在稍后由虛擬機自動創建的、低優先級的Finalizer線程去執行,finalize()方法是對象逃脫死亡的最后一次機會,如果對象在finalize()方法中重新和引用鏈建立連接,就可避免被回收。finalize()方法只會被系統自動調用一次
1.4 方法區(永久代)的回收
(1) gc主要回收兩部分內容:廢棄常量和無用的類?;厥諒U棄常量和回收堆中的對象類似。判定一個類是否無用,需同時滿足3個條件:一是該類的所有實例都被回收,二是加載該類的ClassLoader已被回收,三是該類對應的java.lang.Class對象沒有在任何地方被引用
(2) HotSpot虛擬機通過 -Xnoclassgc 參數進行控制是否啟用類卸載功能。在大量使用反射、動態代理、CGLib等框架,需要虛擬機具備類卸載功能,避免方法區發生內存溢出
2、垃圾回收算法
2.1 標記-清除(Mark-Sweep)算法
(1) 算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,然后統一回收有標記的對象
(2) 兩個不足:一是效率不高,二是會產生大量不連續的內存碎片
2.2 復制(Copying)算法
(1) 新生代分為一個Eden,兩個Survival空間,默認比例是8:1?;厥諘r,將Eden和一個Survival的存活對象全部放入到另一個Survival空間中,最后清理掉剛剛的Eden和Survival空間
(2) 當Survival空間不夠時,由老年代進行內存分配擔保
2.3 標記-整理(Mark-Compact)算法
標記過程和“標記-清除”算法類似,不同之處是讓所有存活的對象向一端移動,然后直接清理掉邊界以外的對象
2.4 分代收集(Generational Collection)算法
將堆分為新生代和老年代,新生代對象存活率低,選用復制算法,老年代存活率高,選用標記-清除或標記-整理算法
3、hotspot的算法實現
3.1 枚舉根節點
(1) 可達性分析時,必須停頓所有java執行線程(Stop The World)
(2) HotSpot通過OopMap記錄對象引用存放的地址,類加載完后,HotSpot把偏移量對應的類型數據計算出來,JIT編譯過程中,會在特定位置記錄下棧和寄存器中哪些位置是引用
3.2 安全點
(1) 程序執行時,只有到達安全點才能停頓下來開始GC
(2) 安全點的選定以“是否讓程序長時間執行”為標準,“長時間執行”最明顯的特征是指令序列復用,如方法調用、循環跳轉、異常跳轉等
(3) 采用主動中斷的方式讓所有線程都跑到最近的安全點上停頓下來。設置一個標志,各個程序執行的時候輪詢這個標志,發現中斷標志為真時自己就中斷掛起
3.3 安全區域
安全區域是指在代碼片段中,引用關系不會發生變化。在這塊區域任何地方GC都是安全的,可以把Safe Region看做是Safe Point的擴展
4、垃圾回收器
上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,說明它們可以搭配使用,所處區域表示收集器屬于新生代還是老年代。
4.1 Serial收集器
? ? ? ?Serial收集器是一款年輕代的垃圾收集器,使用標記-復制算法。它是一款歷史最悠久的垃圾收集器。Serial收集器只能使用一條線程進行垃圾收集工作,并且在進行垃圾收集的時候,所有的工作線程都需要停止工作,等待垃圾收集線程完成以后,其他線程才可以繼續工作。工作過程可以簡單的用下圖來表示:
4.2 ParNew收集器
(1) ParNew收集器是Serial收集器的多線程版本,除了多線程外,與Serial相比沒有太多創新之處
(2) 多線程版本的年輕代收集器中,只有它可以和CMS一起搭配搭配使用
(3) ParNew收集器在單核環境性能不如Serial收集器(線程交互的開銷),默認開啟的收集線程數和CPU數量相同,可以使用 -XX:ParallelGCThreads 限定垃圾回收的線程數
4.3 Parallel Scavenge收集器
(1) 目標是達到一個可控的吞吐量(Throughput),吞吐量=運行用戶代碼時間/(運行用戶代碼時間+GC時間)
(2) 控制吞吐量的兩個參數:-XX:MaxGCPauseMillis(最大GC停頓時間)和-XX:GCTimeRatio(設置吞吐量大小)
(3) -XX:MaxGCPauseMillis允許設置一個大于0的毫秒數,GC停頓時間短是以犧牲吞吐量和新生代空間換取的
(4) -XX:GCTimeRatio是一個大于0且小于100的整數,默認值是99,即允許的最大GC時間1%(即1/(1+99)
(5) -XX:UseAdaptiveSizePolicy,打開后虛擬機會根據當前系統的允許情況收集監控信息,動態調整以提供最適合的停頓時間或最大的吞吐量,該方式稱為GC自適應的調節
4.4 Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,同樣是一個單線程收集器,使用“標記-整理”算法,主要用于Client模式下老年代的垃圾收集器。在Server模式下,主要是在JDK1.5版本之前和Parallel Scavenge年輕代收集器配合使用,或者作為CMS收集器的后備收集器
4.5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和"標記-整理"算法,主要是和Parallel Scavenge收集器一起配合(注重吞吐量及CPU資源敏感的場景),可以實現對Java堆內存的吞吐量優先的垃圾收集策略
4.6 CMS收集器
(1) CMS(Concurrent Mark Sweep)收集器是以最短GC停頓時間為目標的收集器,符合重視服務響應時間的應用
(2) CMS收集器的工作過程可以分為4個階段:
初始標記(CMS initial mark)階段、并發標記(CMS concurrent mark)階段、重新標記(CMS remark)階段、并發清除(CMS concurrent sweep)階段
從圖中可以看出,在這4個階段中,初始標記和重新標記這兩個階段都只有GC線程在運行,用戶線程會被停止,所以這兩個階段會發送STW(Stop The World)。初始標記階段的工作是標記GC Roots可以直接關聯到的對象,速度很快。并發標記階段,會從GC Roots 出發,標記處所有可達的對象,這個過程可能會花費相對比較長的時間,但是由于在這個階段,GC線程和用戶線程是可以一起運行的,所以即使標記過程比較耗時,也不會影響到系統的運行。重新標記階段,是對并發標記期間因用戶程序運行而導致標記變動的那部分記錄進行修正,重新標記階段耗時一般比初始標記稍長,但是遠小于并發標記階段。最終,會進行并發清理階段,和并發標記階段類似,并發清理階段不會停止系統的運行,所以即使相對耗時,也不會對系統運行產生大的影響。
由于并發標記和并發清理階段是和應用系統一起執行的,而初始標記和重新標記相對來說耗時很短,所以可以認為CMS收集器在運行過程中,是和應用程序是并發執行的。由于CMS收集器是一款并發收集和低停頓的垃圾收集器,所以CMS收集器也被稱為并發低停頓收集器。
(3) 不足:一是CMS收集器對CPU資源非常敏感;二是CMS收集器在處理垃圾收集的過程中,可能會產生浮動垃圾,由于它無法處理浮動垃圾,所以可能會出現Concurrent Mode Failure問題而導致觸發一次Full GC。所謂的浮動垃圾,是由于CMS收集器的并發清理階段,清理線程是和用戶線程一起運行,如果在清理過程中,用戶線程產生了垃圾對象,由于過了標記階段,所以這些垃圾對象就成為了浮動垃圾,CMS無法在當前垃圾收集過程中集中處理這些垃圾對象;三是它在進行垃圾收集時使用的"標記-清除"算法,在進行垃圾清理以后,會出現很多內存碎片,過多的內存碎片會影響大對象的分配,會導致即使老年代內存還有很多空閑
4.7 G1收集器
為解決CMS算法產生空間碎片和其它一系列的問題缺陷,HotSpot提供了另外一種垃圾回收策略,G1(Garbage First)算法,通過參數-XX:+UseG1GC來啟用,該算法在JDK 7u4版本被正式推出,官網對此描述如下:
The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:
1.Can operate concurrently with applications threads like the CMS collector.
2.Compact free space without lengthy GC induced pause times.
3.Need more predictable GC pause durations.
4.Do not want to sacrifice a lot of throughput performance.
5.Do not require a much larger Java heap.
在G1算法中,堆內存被劃分為多個大小相等的內存塊(Region),每個Region是邏輯連續的一段內存,結構如下:
每個Region被標記了E、S、O和H,說明每個Region在運行時都充當了一種角色,其中H是以往算法中沒有的,它代表Humongous,表示這些Region存儲的是巨型對象(humongous object,H-obj),當新建對象大小超過Region大小一半時,直接在新的一個或多個連續Region中分配,并標記為H。
堆內存中一個Region的大小可以通過-XX:G1HeapRegionSize參數指定,大小區間只能是1M、2M、4M、8M、16M和32M,總之是2的冪次方。
GC模式:G1中提供了三種模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的條件下被觸發。
young gc:發生在年輕代的GC算法,一般對象(除了巨型對象)都是在eden region中分配內存,當所有eden region被耗盡無法申請內存時,就會觸發一次young gc,這種觸發機制和之前的young gc差不多,執行完一次young gc,活躍對象會被拷貝到survivor region或者晉升到old region中,空閑的region會被放入空閑列表中,等待下次被使用。
參數含義:-XX:MaxGCPauseMillis設置G1收集過程目標時間,默認值200ms;-XX:G1NewSizePercent新生代最小值,默認值5%;-XX:G1MaxNewSizePercent新生代最大值,默認值60%
mixed gc:當越來越多的對象晉升到老年代old region時,為了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即mixed gc,該算法并不是一個old gc,除了回收整個young region,還會回收一部分的old region,這里需要注意:是一部分老年代,而不是全部老年代。
mixed gc中有一個閾值參數-XX:InitiatingHeapOccupancyPercent,當老年代大小占整個堆大小百分比達到該閾值時,會觸發一次mixed gc。mixed gc的執行過程有點類似cms,主要分為以下幾步:
(1) initial mark: 初始標記過程,整個過程STW,標記了從GC Root可達的對象
(2) concurrent marking: 并發標記過程,整個過程gc collector線程與應用線程可以并行執行,標記出GC Root可達對象衍生出去的存活對象,并收集各個Region的存活對象信息
(3) remark: 最終標記過程,整個過程STW,標記出那些在并發標記過程中遺漏的,或者內部引用發生變化的對象
(4) clean up: 垃圾清除過程,如果發現一個Region中沒有存活對象,則把該Region加入到空閑列表中
full gc:如果對象內存分配速度過快,mixed gc來不及回收,導致老年代被填滿,就會觸發一次full gc,G1的full gc算法就是單線程執行的serial old gc,會導致異常長時間的暫停時間,需要進行不斷的調優,盡可能的避免full gc
4.8 理解GC日志
33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs]
100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
最前面的數字“33.125:”和“100.667:”代表了GC發生的時間,這個數字的含義是從Java虛擬機啟動以來經過的秒數。
GC日志開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發生了Stop-The-World的,例如下面這段新生代收集器ParNew的日志也會出現“[Full GC”(這一般是因為出現了分配擔保失敗之類的問題,所以才導致STW)。如果是調用System.gc()方法所觸發的收集,那么在這里將顯示“[Full GC(System)”。
[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]
接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發生的區域,這里顯示的區域名稱與使用的GC收集是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。
如果是ParNew收集器,新生代名稱就會變為“[ParNew”,意為“Parallel New Generation”。
如果采用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。
后面方括號內部的“3324K->152K(3712K)”含義是“GC前該內存區域已使用容量->GC后該內存區域已使用容量(該內存區域總容量)”。?
而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆總容量)”。
再往后,“0.0025925 secs”表示該內存區域GC所占用的時間,單位是秒。
有的收集器會給出更具體的時間數據,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,這里面的user、sys和real與Linux的time命令所輸出的時間含義一致,分別代表用戶態消耗的CPU時間、內核態消耗的CPU事件和操作從開始到結束所經過的墻鐘時間(Wall Clock Time)。
CPU時間與墻鐘時間的區別是,墻鐘時間包括各種非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時。當系統有多CPU或者多核的話,多線程操作會疊加這些CPU時間,所以讀者看到user或sys時間超過real時間是完全正常的。
5、內存分配和回收策略
5.1 對象優先在Eden區分配
一般情況下,對象在新生代Eden區分配,當Eden區沒有足夠空間時,將發起一次Minor GC。-XX:PrintGCDetails可以設置虛擬機在發生GC時打印內存回收日志,并在進程退出時輸出當前內存各區域分配情況。
5.2 大對象直接進入老年代
大對象是指需要大量連續內存空間的對象,如很長的字符串或數組。通過設置-XX:PretenureSizeThreshold參數的值,大于該值的對象直接在老年代分配
5.3 長期存活的對象將進入老年代
虛擬機給每個對象定義了年齡,每經過一次Minor GC且在新生代存活的對象,年齡加1,當達到年齡閥值(默認15,可以通過-XX:MaxTenuringThreshold設置),將會被晉升到老年代中
5.4 對象年齡動態判斷
為了更好的適應不同程序的內存情況,并不是完全根據對象的年齡來晉升到老年代,如果Survivor空間中相同年齡所有對象大小總和大于Survivor空間一半,年齡大于或等于該年齡的對象將直接進入老年代
5.5 空間分配擔保
在發生Minor GC之前,虛擬機會檢查老年代最大可用連續空間是否大于新生代所有對象總空間,如果成立可以確保Minor GC成功,否則虛擬機會查看HandlePromotionFailure是否設置失敗擔保,如果允許,會檢查老年代最大可用連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試進行一次Minor GC,盡管存在風險,如果小于,則進行一次Full GC。