[TOC]
聲明
本篇文章是本人閱讀《深入理解JVM》和《java虛擬機規范》時的筆記。
記錄的都是一些概念性的東西。
JVM是HotSpot,jdk1.7。
大神繞路,不喜勿噴。
1 GC算法
先來走馬觀花般地瀏覽一些著名的GC算法。
這里也僅僅是說一下大致過程,具體細節的介紹對于我一個Java程序員來說表示無能為力,因為底層實現要牽扯到具體的實現語言了,而且不同的JVM實現商肯定有不同的實現細節。
1.1 標記/清除算法
這種算法的大概過程是:
- 標記出所有需要回收的對象
- 統一回收所有被標記的對象
這種算法很直觀,但他的缺點如下:
- 標記和清除的兩個階段,效率并不是很好,因為回收的粒度太細了
- 清除后的內存區域一般都是千瘡百孔,可用內存區域一般都不連續
1.2 復制算法
上面說的標記/清除算法不太好的主要原因就是其回收粒度
太過細微了。
簽于此,復制算法的主要做法是:
- 將內存分為大小相等的兩塊,暫且稱之為內存塊
- 每次當某一內存塊(A)占滿之后,將該內存塊(A)的有用數據復制到另外一塊內存塊(B)
- 將A內存塊的整個塊直接清除
這種算法相比于標記/清除算法的最大特點是:
- 每次回收都是以內存塊為單位(粒度較大)
- 只有兩個內存塊,收集完后內存不連續的情況也就不用考慮了
- 但是,將整個內存分為兩塊,實際的可用內存也就減半了(這就有點無法接受了)
- 存在大量的對象復制操作
1.3 標記/整理算法
上面說的復制算法的最大缺點就是對象的復制操作。尤其是在有效的對象很多的情況下。
這里的標記/整理算法的大致過程是:
- 標記應該回收的區域
- 將有用的對象/數據集中移動到一塊區域,暫且稱之為"有效區",有效區的位置往往是在兩端的某一端
- 將"有效區"之外的區域集體清理
1.4 分代收集算法(Generational Collection)
既然上面說集中算法都各有優劣,那么根據他們各自的優點,在不同的情況下使用最優的算法會不會更好呢?
分代收集的大致思路就是這樣的:
- 將JVM堆內存分為新生代和年老代
- 年老代中的對象存活率一般都很高,采用'標記/清理'或'標記/復制'算法
- 新生代中一般對象的'死亡率'都很高,采用復制算法
2 GC的代價——Stop The World
上面說了一大堆GC的理論。但是忽略了一點:
怎么確定哪些對象或內存區域是可以被回收的呢???
在java中對于對象是否還“活著”,采用的不是像Python或者其他語言中的"引用計數"的方法。
java中采用的是"可達性分析"。
至于可達性分析的細節沒必要去深究,但是由"判斷對象是否還存活?"引出的另一個問題卻不得不考慮,看下文。
無論采用什么方法去區分哪些對象還活著,不得不做的一個讓步就是:這個判定過程中必須暫時讓其他所有的線程都暫時停頓,這個現象對于JVM中的各個對象來說就相當于整個世界停止了。也就是所謂的Stop The World
。
這個停頓當然是有必要的,比如你開始分析對象的存活狀態時一個對象是無用的,當你分析完成后那個對象卻讓其他線程操作了變成有效對象了。
所以,在整個判斷過程中,要能夠確保一致性。也就免不了Stop The World
。
當然,應用的規模越大,Stop The World
帶來的影響越大。
所以,頻繁的GC也不見得是好事。
3 垃圾收集器
上面說的都是GC的大致理論知識,現在看看GC的實現:垃圾收集器。
3.1 Serial收集器
Serial收集器是眾多垃圾收集器中的元老。是一個單線程的收集器。在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束(Stop The World)。
雖然它的出現非常早,但是它依然是虛擬機運行在Client模式下的默認新生代收集器
,也有其獨特的優點:
- 簡單而高效(與其他收集器的單線程比)
- 對于限定單個CPU的環境來說,Serial收集器由于沒有線程交互的開銷
3.2 ParNew收集器
這個ParNew的介紹是來自《深入理解JVM》的作者說的,與本人沒任何關系 _ .. _
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、 -XX:HandlePromotionFailure等)、 收集算法、 Stop The World、 對象分配規則、 回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的代碼。
- 是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作
- ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果
- 甚至由于存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。
- 當然,隨著可以使用的CPU的數量的增加,它對于GC時系統資源的有效利用還是很有好處的。 它默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數
3.3 Parallel Scavenge收集器
3.3.1 簡介
他的特點如下:
- 是一個新生代收集器
- 使用復制算法的收集器
- 并行的多線程收集器
- Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量
- 吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
- 也經常稱為“吞吐量優先”收集器
3.3.2 參數
- -XX:MaxGCPauseMillis ==> 控制最大垃圾收集停頓時間,大于零的毫秒數
- -XX:GCTimeRatio ==> 直接設置吞吐量大小,吞吐量的倒數,既然是個比率,也就是個0到100的整數
- -XX:+UseAdaptiveSizePolicy
- 是一個開關參數,當這個參數打開之后,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、 晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了.
- 虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)
3.4 Serial Old收集器
- Serial收集器的老年代版本
- 一個單線程收集器
- 使用“標記-整理”算法
3.5 Parallel Old收集器
- 是Parallel Scavenge收集器的老年代版本
- 使用多線程和“標記-整理”算法
3.6 CMS收集器
- CMS:Concurrent Mark Sweep
- 是一種以獲取最短回收停頓時間為目標的收集器
- 此處的停頓指的就是上文提到的"Stop The World"
- 其名稱中的MS指的就是Mark Sweep,它采用的算法就是標記/清除
- 他有另外一個名字就是:Concurrent Low Pause Collector(并發、低停頓)
他的缺點如下:
- 對CPU資源非常敏感
- 它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程而導致應用程序變慢,總吞吐量會降低
- 無法處理浮動垃圾
- 標記/清除------->內存不連續
3.7 G1收集器
- 是一款面向服務端應用的垃圾收集器
- 并行與并發
- G1能充分利用多CPU、 多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓Java程序繼續執行
- 分代收集
- 空間整合
- 不會產生內存空間碎片,收集后能提供規整的可用內存
- 分配大對象時不會因為無法找到連續內存空間而提前觸發下次GC
- 可預測的停頓:低停頓
《深入理解JVM》一書是這么說的:
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。 使用G1收集器時,Java堆的內存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
4 GC日志
GC日志的格式乍看起來亂七八糟,烏漆嘛黑的。當然他肯定是有格式的。就拿《深入理解JVM》中的這段代碼來說吧:
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 這個成員屬性的唯一意義就是占點內存,以便能在GC日志中看清楚是否被回收過
*/
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和objB是否能被回收?
System.gc();
}
}
虛擬機參數:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
在我的機器(jdk1.7)上輸出如下:
2016-12-17T16:11:19.650+0800: 0.093: [GC [PSYoungGen: 5427K->568K(38400K)] 5427K->568K(124416K), 0.0016819 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-12-17T16:11:19.652+0800: 0.095: [Full GC [PSYoungGen: 568K->0K(38400K)] [ParOldGen: 0K->463K(86016K)] 568K->463K(124416K) [PSPermGen: 2514K->2513K(21504K)], 0.0109008 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 998K [0x00000007d5c80000, 0x00000007d8700000, 0x0000000800000000)
eden space 33280K, 3% used [0x00000007d5c80000,0x00000007d5d79a60,0x00000007d7d00000)
from space 5120K, 0% used [0x00000007d7d00000,0x00000007d7d00000,0x00000007d8200000)
to space 5120K, 0% used [0x00000007d8200000,0x00000007d8200000,0x00000007d8700000)
ParOldGen total 86016K, used 463K [0x0000000781600000, 0x0000000786a00000, 0x00000007d5c80000)
object space 86016K, 0% used [0x0000000781600000,0x0000000781673eb0,0x0000000786a00000)
PSPermGen total 21504K, used 2520K [0x000000077c400000, 0x000000077d900000, 0x0000000781600000)
object space 21504K, 11% used [0x000000077c400000,0x000000077c676178,0x000000077d900000)
解釋如下:
2016-12-17T16:11:19.650+0800
-XX:+PrintGCDateStamps的作用,就是GC的時間了
0.093:表示的從JVM啟動以來經過的秒數
GC [PSYoungGen:....
GC發生的區域
PSYoungGen表示采用的收集器為Parallel Scavenge
如果使用的是Serial收集器,新生代名為“Default New Generation”,顯示就是“[DefNew”
如果使用的是ParNew收集器,新生代名稱為“[ParNew”,意為“Parallel New Generation”
如果采用的是Parallel Scavenge收集器,新生代名稱就是“PSYoungGen”
“Full”,說明這次GC是發生了Stop-The-World
GC日志,暫時就先寫這么多吧,在后續的文章中再詳細介紹GC日志。
5 和GC相關的JVM參數
注:以下參數總結來自《深入理解JVM》一書
- UseSerialGC : 是否使用Serial收集器
- 啟用后將使用
Serial + Serial Old
的組合來進行垃圾回收 - 這也是Client模式下的默認值
- 啟用后將使用
- UseParNewGC : 是否使用ParNew收集器
- 將使用
ParNew + Serial Old
的組合來進行垃圾回收
- 將使用
- UseConcMarkSweepGC
- 啟用后將使用
ParNew + CMS + Serial Old
的組合來進行垃圾回收 - Serial Old 作為CMS的后備收集器(Concurrent Mode Failure)
- 啟用后將使用
- UseParallelGC
- 使用
Parallel Scavenge + Serial Old
的組合來進行垃圾回收 - 這也是Server模式下的默認值
- 使用
- UseParallelOldGC
- 使用
Parallel Scavenge + Parallel Old
的組合來進行垃圾回收
- 使用
- SurvivorRatio
- 新生代中Eden和Survivor的比值
- 默認為8,即:
Eden:Survivor=8:1
- PretenureSizeThreshold
- 這個大小值,表示對象大小大于多少之后直接分配到老年代而不進入新生代
- MaxTenuringThreshold
- 這個年齡值表示對象在經過多少次Minor GC之后就進入老年代
- 每次Minor GC之后,對象的該屬性值就加1
- UseAdaptiveSizePolicy
- 動態調節堆中各個區域的大小和進入老年代的年齡
- HandlePromotionFailure
- 是否允許分配擔保失敗
- 擔保失敗指的是: 老年代的剩余空間大小無法容納新生代中的Eden和Survivor的情況
- ParallelGCThreads
- 并行GC的線程數
- GCTimeRatio
- GC時間占總時間的比例
- 只有在使用Parallel Scavenge的情況下生效
- 默認值:99
- MaxGCPauseMillis
- GC的最大停頓時間
- 只有在使用Parallel Scavenge的情況下生效
- CMSInitiatingOccupancyFraction
- CMS收集器在老年代空間被占用多少后觸發GC
- 只對CMS收集器生效
- 默認值:68%
- UseCMSCompactAtFullCollection
- CMS收集器在完成垃圾回收后是否進行內存碎片整理
- 只對CMS收集器生效
- CMSFullGCsBeforeCompaction
- CMS經過多少次GC后再進行碎片整理
- 也就是設置CMS收集器在進行N次垃圾收集后再進行一次碎片整理
- 只對CMS收集器生效
6 Minor GC 和 Full GC/Major GC
- Minor GC指的是新生代的GC
- Minor GC比較頻繁
- 速度也比較快
- Full GC/Major GC指的是老年代的GC
- Full GC的速度一般比Minor GC慢10倍左右
- Full GC的出現往往會有Minor GC的伴隨
參考文章
- 《深入理解JVM》
- 《Java虛擬機規范》-JDK1.7