HotSpot JDK 7从update 4开始引入了G1垃圾收集器。
G1收集器是服务器风格的垃圾回收器,主要针对多处理器机器上占用大量内存的应用。G1能缩短暂停时间,也能提供高吞吐量,可预测的停顿时间模型。
和CMS(Concurrent Mark-Sweep)收集器相比,G1具备压缩功能,能避免碎片问题;G1的暂停时间更加可控,用户可以指定暂停时间指标。
以前的垃圾收集器(顺序、并行、CMS)都把堆分为三个部分:年轻代、老年代、永久代,三个部分的大小都是固定的。而在G1里,堆被分为若干区域(Region),每个区域里的内存是连续的。区域会有“角色”,但某个“角色”的大小并不固定,这就提供了更大的内存使用灵活度。
执行垃圾回收的时候,G1的操作和CMS有些类似,G1也会和应用并行地进行全局标记。标记阶段结束后,G1会优先收集存活对象少(占用空间小)的区域,也就是垃圾(可回收对象)多的区域。G1采用“pause prediction模型”,根据用户定义的暂停时间指标确定回收区域的个数。
G1要回收的区域会被清空,里面的存活对象会被拷贝到堆里的另一个单独区域,压缩、释放原区域里的内存。在多处理器机器上,这会并行地执行,减少暂停时间、增加吞吐量。
但G1不是实时收集,它会基于前面收集的相关数据进行评估,看在用户指定的暂停时间里能收集几个区域。
G1里有两个数据结构:
G1适合堆大小差不多是6GB或者更大,暂停时间要求在0.5秒以下的场景。如果应用具备如下一个或多个特征用G1会有比较好的效果:
但如果应用或系统的GC暂停时间本来就不长,建议还是保持原先的GC,不要换成G1
堆被划分为很多区域,区域大小由JVM确定(用户也可以设置),通常是1MB-32MB,大约2000个。
区域会具备“Eden”、“Survivor”、“老年代”的角色,但它们不是连续的。还有另外一种区域,叫Humongous区域,它用来存放大对象(大小超过区域大小的50%),这些区域是连续的。
存活对象会被拷贝/移动到一个或多个“Survivor”区域,存活时间够长的直接移到“老年代”区域。这里会stop the world,但young GC的过程是多线程执行的。
1、初始标记(stop the world):标记引用老年代里对象的Survivor区域,这些Survivor区域叫root区域 GC pause (young)(inital-mark)
2、扫描root区域:扫描survivor区域,找到引用老年代里内容的对象。young GC开始前要完成。
3、并发标记:找出整个堆里可达的对象,计算各个区域的对象存活率。这个阶段可能会被年轻代的GC中断。
4、Remark(stop the world):对并发标记阶段的结果查漏补缺,使用snapshot-at-the-beginning(SATB)算法,比CMS使用的算法快很多;而且会回收空区域(没有存活对象的区域)
5、清理:先stop the world,收集对象存活率最低的区域,然后清理Remembered Set。接着应用可以运行了,这时会重置空闲区域,并将这些区域返回给free list。年轻代和老年代同时回收
6、拷贝(stop the world):将存活对象拷贝到没有使用的区域里
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。
使用G1时的最佳实践
1、不要设置年轻代的大小(-Xmn),否则会扰乱G1的缺省行为,JVM也不会满足用户指定的暂停时间。而且设置了固定值的话,G1将无法随需扩展年轻代的大小
2、GC暂停时间不是100%能保证的
3、如果GC的晋升过程中遇到堆区域溢出(使用-XX:+PrintGCDetails看到to-space overflow),可以通过下面几种方式避免:
G1收集器采用一种不同的方式来分配堆. 下面通过图解的方式一步步地讲述G1系统.
堆内存被划分为固定大小的多个区域.
每个heap区(Region)的大小在JVM启动时就确定了. JVM 通常生成 2000 个左右的Region区, 根据堆内存的总大小,区的size范围允许为 1Mb 到 32Mb.
实际上,这些区域(regions)被映射为逻辑上的 Eden, Survivor, 和 old generation(老年代)空间.
图中的颜色标识了每一个区域属于哪个角色. 存活的对象从一块区域转移(复制或移动)到另一块区域。设计成 heap 区的目的是为了并行地进行垃圾回收(的同时停止/或不停止其他应用程序线程).
如图所示,heap区可以分配为 Eden, Survivor, 或 old generation(老年代)区. 此外,还有第四种类型的对象被称为巨无霸区域(Humongous regions),这种巨无霸区是设计了用来保存比标准heap区大50%及以上的对象, 它们存储在一组连续的区中. 最后一个类型是堆内存中的未使用区(unused areas).
备注: 截止英文原文发表时,巨无霸对象的回收还没有得到优化. 因此,您应该尽量避免创建太大(大于32MB?)的对象.
堆被分为大约2000个区. 最小size为1 Mb, 最大size为 32Mb. 蓝色的区保存老年代对象,绿色区域保存年轻代对象.
注意G1中各代的heap区不像老一代垃圾收集器一样要求各部分是连续的.
存活的对象被转移(copied or moved)到一个/或多个存活区(survivor regions). 如果存活时间达到阀值,这部分对象就会被提升到老年代(promoted to old generation regions).
此时会有一次 stop the world(STW)暂停. 会计算出 Eden大小和 survivor 大小,给下一次年轻代GC使用. 清单统计信息(Accounting)保存了用来辅助计算size. 诸如暂停时间目标之类的东西也会纳入考虑.
这种方法使得调整各代区域的尺寸很容易, 让其更大或更小一些以满足需要.
存活对象被转移到存活区(survivor regions) 或 老年代(old generation regions).
刚刚被提升上来的对象用深绿色显示. Survivor 区用绿色表示.
总结起来,G1的年轻代收集归纳如下:
堆一整块内存空间,被分为多个heap区(regions).
年轻代内存由一组不连续的heap区组成. 这使得在需要时很容易进行容量调整.
年轻代的垃圾收集,或者叫 young GCs, 会有 stop the world 事件. 在操作时所有的应用程序线程都会被暂停(stopped).
年轻代 GC 通过多线程并行进行.
存活的对象被拷贝到新的 survivor 区或者老年代.
和 CMS 收集器相似, G1 收集器也被设计为用来对老年代的对象进行低延迟(low pause)的垃圾收集. 下表描述了G1收集器在老年代进行垃圾回收的各个阶段.
G1 收集器在老年代堆内存中执行下面的这些阶段. 注意有些阶段也是年轻代垃圾收集的一部分.
(1) 初始标记(Initial Mark):(Stop the World Event,所有应用线程暂停) 此时会有一次 stop the world(STW)暂停事件. 在G1中, 这附加在(piggybacked on)一次正常的年轻代GC. 标记可能有引用指向老年代对象的survivor区(根regions).
(2) 扫描根区域(Root Region Scanning) 扫描 survivor 区中引用到老年代的引用. 这个阶段应用程序的线程会继续运行. 在年轻代GC可能发生之前此阶段必须完成.
(3) 并发标记(Concurrent Marking) 在整个堆中查找活着的对象. 此阶段应用程序的线程正在运行. 此阶段可以被年轻代GC打断(interrupted).
(4) 再次标记(Remark) (Stop the World Event,所有应用线程暂停) 完成堆内存中存活对象的标记. 使用一个叫做 snapshot-at-the-beginning(SATB, 起始快照)的算法, 该算法比CMS所使用的算法要快速的多.
(5) 清理(Cleanup) (Stop the World Event,所有应用线程暂停,并发执行)
在存活对象和完全空闲的区域上执行统计(accounting). (Stop the world)
擦写 Remembered Sets. (Stop the world)
重置空heap区并将他们返还给空闲列表(free list). (Concurrent, 并发)
(*) 拷贝(Copying) (Stop the World Event,所有应用线程暂停) 产生STW事件来转移或拷贝存活的对象到新的未使用的heap区(new unused regions). 只在年轻代发生时日志会记录为 `[GC pause (young)]`. 如果在年轻代和老年代一起执行则会被日志记录为 `[GC Pause (mixed)]`.
顺着定义的阶段,让我们看看G1收集器如何处理老年代(old generation).
存活对象的初始标记被固定在年轻代垃圾收集里面. 在日志中被记为 GC pause (young)(inital-mark)
。
如果找到空的区域(如用红叉“X”标示的区域), 则会在 Remark 阶段立即移除. 当然,"清单(accounting)"信息决定了活跃度(liveness)的计算.
空的区域被移除并回收。现在计算所有区域的活跃度(Region liveness).
G1选择“活跃度(liveness)”最低的区域, 这些区域可以最快的完成回收. 然后这些区域和年轻代GC在同时被垃圾收集 . 在日志被标识为 [GC pause (mixed)]
. 所以年轻代和老年代都在同一时间被垃圾收集.
所选择的区域被收集和压缩到下图所示的深蓝色区域和深绿色区域.
总结下来,G1对老年代的GC有如下几个关键点:
并发标记清理阶段(Concurrent Marking Phase)
活跃度信息在程序运行的时候被并行计算出来
活跃度(liveness)信息标识出哪些区域在转移暂停期间最适合回收.
不像CMS一样有清理阶段(sweeping phase).
再次标记阶段(Remark Phase)
使用的 Snapshot-at-the-Beginning (SATB, 开始快照) 算法比起 CMS所用的算法要快得多.
完全空的区域直接被回收.
拷贝/清理阶段(Copying/Cleanup Phase)
年轻代与老年代同时进行回收.
老年代的选择基于其活跃度(liveness).
要启用 G1 收集器请使用: -XX:+UseG1GC
下面是启动 Java2Demo示例程序的命令行示例. Java2Demo位于下载 JDK demos and samples 后解压的文件夹中:
java -Xmx50m -Xms50m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar
-XX:+UseG1GC - 让 JVM 使用 G1 垃圾收集器.
-XX:MaxGCPauseMillis=200 - 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽力去达成这个目标. 所以有时候这个目标并不能达成. 默认值为 200 毫秒.
-XX:InitiatingHeapOccupancyPercent=45 - 启动并发GC时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
在使用 G1 作为垃圾收集器时,你应该遵循下面这些最佳实践的指导.
假若通过 -Xmn 显式地指定了年轻代的大小, 则会干扰到 G1收集器的默认行为.
G1在垃圾收集时将不再关心暂停时间指标. 所以从本质上说,设置年轻代的大小将禁用暂停时间目标.
G1在必要时也不能够增加或者缩小年轻代的空间. 因为大小是固定的,所以对更改大小无能为力.
设置 XX:MaxGCPauseMillis=
时不应该使用平均响应时间(ART, average response time) 作为指标,而应该考虑使用目标时间的90%或者更大作为响应时间指标. 也就是说90%的用户(客户端/?)请求响应时间不会超过预设的目标值. 记住,暂停时间只是一个目标,并不能保证总是得到满足.
对 survivors 或 promoted objects 进行GC时如果JVM的heap区不足就会发生提升失败(promotion failure). 堆内存不能继续扩充,因为已经达到最大值了. 当使用 -XX:+PrintGCDetails
时将会在GC日志中显示 to-space overflow (to-空间溢出)。
这是很昂贵的操作!
GC仍继续所以空间必须被释放.
拷贝失败的对象必须被放到正确的位置(tenured in place).
CSet指向区域中的任何 RSets 更新都必须重新生成(regenerated).
所有这些步骤都是代价高昂的.
要避免避免转移失败, 考虑采纳下列选项.
增加堆内存大小
增加 -XX:G1ReservePercent=n, 其默认值是 10.
G1创建了一个假天花板(false ceiling),在需要更大 'to-space' 的情况下会尝试从保留内存获取(leave the reserve memory free).
更早启动标记周期(marking cycle)
通过采用 -XX:ConcGCThreads=n 选项增加标记线程(marking threads)的数量.
下面是完整的 G1 的 GC 开关参数列表. 在使用时请记住上面所述的最佳实践.
选项/默认值 说明
-XX:+UseG1GC
使用 G1 (Garbage First) 垃圾收集器
-XX:MaxGCPauseMillis=n
设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标.
-XX:InitiatingHeapOccupancyPercent=n
启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45.
-XX:NewRatio=n
新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=n eden/survivor
空间大小的比例(Ratio). 默认值为 8.
-XX:MaxTenuringThreshold=n
提升年老代的最大临界值(tenuring threshold). 默认值为 15.
-XX:ParallelGCThreads=n
设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
-XX:ConcGCThreads=n
并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.
-XX:G1ReservePercent=n
设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
-XX:G1HeapRegionSize=n
使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.
我们要介绍的最后一个主题是使用日志信息来分享G1收集器的性能. 本节简要介绍垃圾收集的相关参数,以及日志中打印的相关信息.
可以设置3种不同的日志级别.
(1) -verbosegc (等价于 -XX:+PrintGC) 设置日志级别为 好 fine.
日志输出示例
[GC pause (G1 Humongous Allocation) (young) (initial-mark) 24M- >21M(64M), 0.2349730 secs]
[GC pause (G1 Evacuation Pause) (mixed) 66M->21M(236M), 0.1625268 secs]
(2) -XX:+PrintGCDetails 设置日志级别为 更好 finer. 使用此选项会显示以下信息:
每个阶段的 Average, Min, 以及 Max 时间.
根扫描(Root Scan), RSet 更新(同时处理缓冲区信息), RSet扫描(Scan), 对象拷贝(Object Copy), 终止(Termination, 包括尝试次数).
还显示 “other” 执行时间, 比如选择 CSet, 引用处理(reference processing), 引用排队(reference enqueuing) 以及释放(freeing) CSet等.
显示 Eden, Survivors 以及总的 Heap 占用信息(occupancies).
日志输出示例
[Ext Root Scanning (ms): Avg: 1.7 Min: 0.0 Max: 3.7 Diff: 3.7]
[Eden: 818M(818M)->0B(714M) Survivors: 0B->104M Heap: 836M(4096M)->409M(4096M)]
(3) -XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest 设置日志级别为 最好 finest. 和 finer 级别类似, 包含每个 worker 线程信息.
[Ext Root Scanning (ms): 2.1 2.4 2.0 0.0
Avg: 1.6 Min: 0.0 Max: 2.4 Diff: 2.3]
[Update RS (ms): 0.4 0.2 0.4 0.0
Avg: 0.2 Min: 0.0 Max: 0.4 Diff: 0.4]
[Processed Buffers : 5 1 10 0
Sum: 16, Avg: 4, Min: 0, Max: 10, Diff: 10]
有两个参数决定了GC日志中打印的时间显示形式.
(1) -XX:+PrintGCTimeStamps - 显示从JVM启动时算起的运行时间.
日志输出示例
1.729: [GC pause (young) 46M->35M(1332M), 0.0310029 secs]
(2) -XX:+PrintGCDateStamps - 在每条记录前加上日期时间.
日志输出示例
2012-05-02T11:16:32.057+0200: [GC pause (young) 46M->35M(1332M), 0.0317225 secs]
为了使你更好地理解GC日志, 本节通过实际的日志输出,定义了许多专业术语. 下面的例子显示了GC日志的内容,并加上日志中出现的术语和值的解释说明.
Note: 更多信息请参考 Poonam Bajaj的博客: G1垃圾回收日志.
Clear CT
CSet
External Root Scanning
Free CSet
GC Worker End
GC Worker Other
Object Copy
Other
Parallel Time
Ref Eng
Ref Proc
Scanning Remembered Sets
Termination Time
Update Remembered Set
Worker Start
414.557: [GC pause (young), 0.03039600 secs] [Parallel Time: 22.9 ms]
[GC Worker Start (ms): 7096.0 7096.0 7096.1 7096.1 706.1 7096.1 7096.1 7096.1 7096.2 7096.2 7096.2 7096.2
Avg: 7096.1, Min: 7096.0, Max: 7096.2, Diff: 0.2]
Parallel Time
– 主要并行部分运行停顿的整体时间
Worker Start
– 各个工作线程(workers)启动时的时间戳(Timestamp)
Note: 日志是根据 thread id 排序,并且每条记录都是一致的.
[Ext Root Scanning (ms): 3.1 3.4 3.4 3.0 4.2 2.0 3.6 3.2 3.4 7.7 3.7 4.4
Avg: 3.8, Min: 2.0, Max: 7.7, Diff: 5.7]
External root scanning
- 扫描外部根花费的时间(如指向堆内存的系统词典(system dictionary)等部分)
[Update RS (ms): 0.1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Avg: 0.0, Min: 0.0, Max: 0.1, Diff: 0.1]
[Processed Buffers : 26 0 0 0 0 0 0 0 0 0 0 0
Sum: 26, Avg: 2, Min: 0, Max: 26, Diff: 26]
Update Remembered Set
- 必须更新在pause之前已经完成但尚未处理的缓冲. 花费的时间取决于cards的密度。cards越多,耗费的时间就越长。
[Scan RS (ms): 0.4 0.2 0.1 0.3 0.0 0.0 0.1 0.2 0.0 0.1 0.0 0.0 Avg: 0.1, Min: 0.0, Max: 0.4, Diff: 0.3]F
Scanning Remembered Sets
- 查找指向 Collection Set 的指针(pointers)
[Object Copy (ms): 16.7 16.7 16.7 16.9 16.0 18.1 16.5 16.8 16.7 12.3 16.4 15.7 Avg: 16.3, Min: 12.3, Max: 18.1, Diff: 5.8]
Object copy
– 每个独立的线程在拷贝和转移对象时所消耗的时间.
[Termination (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0] [Termination Attempts : 1 1 1 1 1 1 1 1 1 1 1 1 Sum: 12, Avg: 1, Min: 1, Max: 1, Diff: 0]
Termination time
- 当worker线程完成了自己那部分对象的复制和扫描,就进入终止协议(termination protocol)。它查找未完成的工作(looks for work to steal), 一旦它完成就会再进入终止协议。 终止尝试记录(Termination attempt counts)所有查找工作的尝试次数(attempts to steal work).
[GC Worker End (ms): 7116.4 7116.3 7116.4 7116.3 7116.4 7116.3 7116.4 7116.4 7116.4 7116.4 7116.3 7116.3
Avg: 7116.4, Min: 7116.3, Max: 7116.4, Diff: 0.1]
[GC Worker (ms): 20.4 20.3 20.3 20.2 20.3 20.2 20.2 20.2 20.3 20.2 20.1 20.1
Avg: 20.2, Min: 20.1, Max: 20.4, Diff: 0.3]
GC worker end time
– 独立的 GC worker 停止时的时间戳.
GC worker time
– 每个独立的 GC worker 线程消耗的时间.
[GC Worker Other (ms): 2.6 2.6 2.7 2.7 2.7 2.7 2.7 2.8 2.8 2.8 2.8 2.8
Avg: 2.7, Min: 2.6, Max: 2.8, Diff: 0.2]
GC worker other
– 每个GC线程中不能归属到之前列出的worker阶段的其他时间. 这个值应该很低. 过去我们见过很高的值,是由于JVM的其他部分的瓶颈引起的(例如在分层[Tiered]代码缓存[Code Cache]占有率的增加)。
[Clear CT: 0.6 ms]
清除 RSet 扫描元数据(scanning meta-data)的 card table 消耗的时间.
[Other: 6.8 ms]
其他各种GC暂停的连续阶段花费的时间.
[Choose CSet: 0.1 ms]
敲定要进行垃圾回收的region集合时消耗的时间. 通常很小,在必须选择 old 区时会稍微长一点点.
[Ref Proc: 4.4 ms]
处理 soft, weak, 等引用所花费的时间,不同于前面的GC阶段
[Ref Enq: 0.1 ms]
将 soft, weak, 等引用放置到待处理列表(pending list)花费的时间.
[Free CSet: 2.0 ms]
释放刚被垃圾收集的 heap区所消耗的时间,包括对应的remembered sets。
在此OBE中, 您对Java JVM 中的G1垃圾收集器有了个大致的了解。首先你学到了为何堆和垃圾收集器是所有Java JVM的关键部分。接下来讲述了使用CMS和G1收集器进行垃圾回收的工作方式. 接下来,您了解了G1的命令行参数/开关以及和使用它们的最佳实践。最后,您了解了日志对象以及GC日志中的数据。