本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容:
快速解读GC日志
作者:铁锚
出处:CSDN
什么时候触发MinorGC?什么时候触发FullGC?
作者:summerZBH123
出处:CSDN
Minor GC、Major GC和Full GC之间的区别
作者:javacodegeeks
出处:程序猿DD
Linux下Tomcat开启查看GC信息
作者:阿龙
出处:cnblogs
从实际案例聊聊Java应用的GC优化
作者:录录
出处:美团技术团队
JVM原理讲解和调优
作者:小小水滴
HotSpot VM GC收集器的易混淆的名称问题
作者:anranran
出处:51CTO
Java对象内存分配策略
作者:用户5673393671
JVM 垃圾回收GC Roots Tracing
作者:mine_song
[资料] 名词链接帖 [占位ing]
作者:RednaxelaFX
可能是最全面的G1学习笔记
作者:javaadu
java垃圾回收以及jvm参数调优概述
作者: 大鹏展翅
本文是 Plumbr 发行的 Java垃圾收集指南 的部分内容。文中将介绍GC日志的输出格式, 以及如何解读GC日志, 从中提取有用的信息。
堆内存划分为 Eden、Survivor 和 Tenured/Old 空间
MaxTenuringThreshold
设定,默认值为15。java.lang.OutOfMemoryError:PermGenspace
这个异常。这里的 PermGen space
其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有“PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。由于永久代实现方法区有不少问题,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
GC Roots搜索(即Gc Roots Tracing)算法的基本思路就是通过一系列名为GC Roots
的引用作为起始点,从这些节点开始向下搜索,通过引用关系遍历对象图。搜索所走过的路径称为引用链(Reference Chain),可达的对象就判定为活着的,不可被回收。而其余对象就是GC Roots不可达对象(即一个对象没有任何引用链相连),则证明此对象是不可用的,可以被回收。
比如上图中左侧的ABCD
皆从GC Roots可达,不能回收;右侧的Object IFGH
皆从GC Roots不可达,所以可以回收。
目前HotSpot主要使用两类作为GC roots:
更多可查看Garbage Collection Roots
因为可作为GC Roots的节点主要是在全局性引用(如常量或静态属性)与执行上下文(如栈帧中的本地变量表)中,范围大数据量大,不可能挨个检查引用。又由于HotSpot采用准确式GC(即知道某数据是什么类型,比如内存中有一个32位整数123456,JVM可以分辨出到底是数值还是reference类型指向123456的内存地址),所以当系统STW时,不需要全局查找,而只需要使用OopMap来查找。
具体来说,类加载完成时,HotSpot VM就会将对象内什么偏移量是什么类型的数据计算完毕,在JIT编译过程中也会在特定位置记录栈和寄存器中哪些位置是引用。这样,HotSpot就能知道哪些位置数据是引用了。在GC时,可以直接使用。
有了OopMap,HotSpot就能快速准确完成GC Roots枚举了。
可参考Java-JVM-安全点SafePoint
为每条指令生成OopMap开销太大,所以只是在特定位置记录信息,这些位置被称为安全点(SafePoint),具体是程序执行时并非在所有地方都能停止并开始GC,而是程序在达到sSafePoint的时候才能暂停并开始GC。
SafePoint的选定不能太少而让GC等待太久,也不能过密以使得运行时过多GC。一句话,安全点选定标准是以是否具有让程序长时间执行的特征
。具体来说,普通指令流执行速度很快,不能长时间运行,所以一般在方法调用、循环跳转、异常跳转等指令选择产生SafePoint。
GC时让所有线程(不包括JNI调用线程)都到最近的SafePoint并停止的主流方法是主动式中断:
当GC需要中断线程时,不直接对线程操作,而是设一个标志位,每个线程都主动去轮询该标志,发现中断标志就本线程中断并挂起。该轮询标志的地方和安全点就是重合的。
当线程没有分配CPU时间(Sleep或Blocking状态时),无法响应JVM中断请求从而到无法到安全点挂起。此时不可能一直等待线程重新分配CPU时间继续执行。此时就轮到SafeRegion发威了。
SafeRegion指在一段代码片段中引用关系不会改变,那么在该区域中任何地方开始GC都是安全的。
线程在执行到SafeRegion代码时,会标识自己进入SafeRegion,GC时就认为该线程可以开始GC。
线程在离开SafeRegion的时候需要判断GC Roots枚举或整个GC过程是否完成,如果已经完成就继续执行程序, 否则就必须等待直到收到可以安全离开SafeRegion的信号为止。
即使在可达性分析算法中的不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“死刑缓刑”阶段。而要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(任何对象的finalize()方法都只会被调用一次!),虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收该对象)
F-Queue
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue
的队列之中,并在稍后由一个由JVM自动建立的、低优先级的Finalizer线程去执行它。**这里所谓的“执行”是指JVM会触发F-Queue队列中的对象调用finalize方法,但并不承诺会等待该方法运行结束。**原因是如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个GC系统崩溃。
第二次标记
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可。譬如把自己(this关键字)赋值给某个类变量(方法区的类变量属于GC Roots)或者对象的成员变量(栈区的局部变量表中引用对象属于GC Roots),那在第二次标记时它将被移除出“即将回收”的集合;
如果对象这时候还没有逃脱,那基本上它就真的被回收了。
一般不要尝试用finalize做重要的工作
如前所述,finalize方法只能保证触发不能保证被执行完,所以不要尝试用finalize做重要的工作。
之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候Java允许在类中定义一个由finalize()方法。
特殊的区域例如:
强引用
存在则不会被GC
软引用
内存溢出前会再次对软引用对象进行GC,如果还是内存不够则会抛出OOM。
该引用过多会导致老年代中存活对象过多,FullGC时间长
弱引用
每次GC都会干掉弱引用对象。一般可用来做缓存,如WeakHashMap,
虚引用
随时可能被干掉,能通过引用队列获取到通知
概述
多数情况下,对象优先在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
代码示例:
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
from space 1024K, 14% used [0x032d0000, 0x032f5370, 0x033d0000)
to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
tenured generation total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)
the space 10240K, 60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)
compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.
配置说明
代码的testAllocation()
方法中,尝试分配3个2MB大小和1个4MB大小的byte数组对象。在运行时通过-Xms20M
、 -Xmx20M
、 -Xmn10M
这3个参数限制了Java堆大小为20MB,年轻代10MB,老年代10MB。-XX:SurvivorRatio=8
决定了新生代中Eden区与两个Survivor区的空间比例是8:1:1,从输出的结果也可以清晰地看到eden space 8192K
、from space 1024K
、to space 1024K
的信息,新生代总可用空间为def new generation total 9216K
(即Eden区+1个Survivor区的总容量)。
执行结果说明
执行testAllocation()
中分配allocation4
对象的语句时会因为给allocation4分配内存时Eden区剩余内存不足而发生一次Minor GC,这次GC的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少(因为allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。
GC发现已有的3个2MB对象全部无法放入1MB大小的Survivor,所以只好通过分配担保机制提前晋升到老年代。
本次GC结束后,4MB的allocation4对象可顺利分配在GC后的Eden中,因此程序执行完的结果是Eden占用4MB,Survivor基本空闲,老年代被分配担保机制晋升的3个2MB对象占用6MB。
概述
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
大对象对虚拟机的内存分配来说就是一个坏消息(比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来安置大对象。
虚拟机提供了一个-XX:PretenureSizeThreshold
参数,大于该阈值的对象直接在老年代分配,做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
代码示例:
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[4 * _1MB]; //直接分配在老年代中
}
1.Heap
2.def new generation total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)
3.eden space 8192K, 8% used [0x029d0000, 0x02a77e98, 0x031d0000)
4.from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
5.to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
6.tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
7.the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
8.compacting perm gen total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)
9.the space 12288K, 17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)
10.No shared spaces configured.
执行结果说明
执行代码中的testPretenureSizeThreshold()
方法后,Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold=3MB
,超过的对象都会直接在老年代进行分配。
注意:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。但并不是大对象直接进入老年代分配策略对其就是无效的,在Parallel Scavenge中自有它的实现,大约等于Eden区域一半的对象会被认成大对象。感兴趣的可以点击这里
如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。
概述
为了在内存回收时能识别哪些对象应放在新生代,哪些对象应放在老年代中。虚拟机给每个对象定义了一个对象年龄(Age)计数器,对象年龄的判定规则如下:
-XX:MaxTenuringThreshold
设置。代码示例:
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4];
// 什么时候进入老年代取决于XX:MaxTenuringThreshold设置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
以MaxTenuringThreshold=1
参数来运行的结果:
1.[GC [DefNew
2.Desired Survivor size 524288 bytes, new threshold 1 (max 1)
3.- age 1: 414664 bytes, 414664 total
4.: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
5.[GC [DefNew
6.Desired Survivor size 524288 bytes, new threshold 1 (max 1)
7.: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
8.Heap
9.def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
10.eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
11.from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
12.to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
13.to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
14.to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
15.to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
16.tenured generation total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)
17.the space 10240K, 43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)
18.compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
19.the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
20.No shared spaces configured.
以MaxTenuringThreshold=15参数来运行的结果:
1.[GC [DefNew
2.Desired Survivor size 524288 bytes, new threshold 15 (max 15)
3.- age 1: 414664 bytes, 414664 total
4.: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
5.[GC [DefNew
6.Desired Survivor size 524288 bytes, new threshold 15 (max 15)
7.- age 2: 414520 bytes, 414520 total
8.: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
9.Heap
10.def new generation total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)
11.eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
12.from space 1024K, 39% used [0x031d0000, 0x03235338, 0x032d0000)
13.to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
14.tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
15.the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
16.compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
17.the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
18.No shared spaces configured.
运行结果分析:
分别以两种设置来执行代码中的testTenuringThreshold()
方法,allocation1对象需要256KB内存,Survivor空间可以容纳。
当MaxTenuringThreshold=1
时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后非常干净地变成0KB。
而MaxTenuringThreshold=15
时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时新生代仍然有404KB被占用。
概述
为了能更好地适应不同程序的内存状况,JVM并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold
才能晋升老年代,所以引入了动态对象年龄判定
的概念:
当在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半时,年龄大于或等于该年龄的对象就可以直接进入老年代,无须再等到MaxTenuringThreshold
中要求的年龄。
代码示例
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4];
// allocation1+allocation2大于survivo空间一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
运行结果:
1.[GC [DefNew
2.Desired Survivor size 524288 bytes, new threshold 1 (max 15)
3.- age 1: 676824 bytes, 676824 total
4.: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
5.[GC [DefNew
6.Desired Survivor size 524288 bytes, new threshold 15 (max 15)
7.: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
8.Heap
9.def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
10.eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
11.from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
12.to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
13.tenured generation total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)
14.the space 10240K, 46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)
15.compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
16.the space 12288K, 17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)
17.No shared spaces configured.
运行结果说明
执行代码中的testTenuringThreshold2()
方法,并设置-XX:MaxTenuringThreshold=15
,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,也就是说,allocation1、allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个同Age的对象加起来已经到达了512KB达到了其中一个Survivor区大小一半,触发了动态对象年龄判定。
我们只要注释掉其中一个对象new操作,就会发现另外一个就不会晋升到老年代中去了。
新生代Survivor不够时需要依赖老年代HandlePromotion,如果MinorGC时,目的地Survivor无足够空间存放GC后还存活的对象,则需要通过HandlePromotion机制直接晋升到老年代。
具体来说,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于或HandlePromotionFailure设置不允许,那这时也要改为进行一次Full GC。
下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure),那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
当负责GC的线程进行tracing 标记(tracks object references
),或者移动标记对象等操作的时候,GC线程必须确保应用程序线程(Application threads)没有正在使用这些对象。当GC线程移动一个对象的时候,往往会导致对象在内存(堆)中的地址变化,这就会导致应用程序线程再也没有办法访问到这些对象;为了防止这种现象的发生,在GC 过程中, 应用程序必须暂停执行,称为STW(The pauses when all application threads are stopped are called stop-the-world pauses.
)。
Remembered Set
是一种抽象概念,而card table
可以是remembered set的一种实现方式。
Remembered Set是在实现部分垃圾收集(partial GC)时用于记录从非收集部分指向收集部分的指针的集合的抽象数据结构(比如老年代指向)。
partial GC的两种情况:
分代式GC-卡表
当分两代时,通常把这两代叫做young gen和old gen;通常能单独收集的只是young gen。此时remembered set记录的就是从old gen指向young gen的跨代指针,即卡表(card table)。
具体来说YoungGC时只会对年轻代对象做存活标记,只收集年轻代对象,不会管老年代对象。只不过会将老年代引用了年轻代的引用作为YoungGC的GC Roots的一部分。同时,经过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表来实现这一目的,即YoungGC用卡表记录了老年代对新生代引用,避免了去扫描老年代。
卡表使用字节数组来实现card的记录,每个card对应该数组里的一个bit或一个byte,例如:
struct CardTable {
byte table[MAX_CARDTABLE_SIZE];
};
Region式GC
Regional collector也是一种部分垃圾收集的实现方式,此时remembered set就要记录跨region的指针。
概念
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。
采用复制算法进行垃圾回收时:
forwarding pointer
指向新地址,forwarding pointer
来更新引用对象的地址即可复制算法没有mark和copy阶段,而就是一个合并起来的阶段称为scavenge(或copy)。
copy算法会在遇到一个未处理的引用的时候看这个引用指向的对象是否已经被copy了,如果没有则copy之并且修正引用,如果已经copy了则只修正引用。注意无论如何都是要修正引用的,所以复制后对象依然可以找到引用的对象(已经是复制后的那一份)。
优点
此算法每次只处理正在使用中的对象(即只关心活对象——每碰到一个未处理的活对象就把它copy走,没有标记阶段),因此复制成本比较小。具体来说,只需要遍历一趟存活对象而非整个堆,就完成了对象拷贝和指针修正,数据局部性非常好。
直接可覆盖使用旧有内存进行分配
在整个copy流程完成后,所有活对象都不在原来的空间了,原来的空间就可以直接被用来再次分配新对象——死对象自然就被覆盖了。
同时复制过去以后还能对原空间进行相应的内存整理,不过出现"碎片"问题。
复制算法的开销只跟活对象的多少(live data set)有关系,而跟它所管理的堆空间的大小没关系。
例如20GB的年轻代,应用程序在非常快速的向里面分配新对象,但是这些新对象的绝大部分(例如> 99%)都在下一次YoungGC时已死的话,那其实活对象也没多少(< 204MB),并行处理它们的话或许100-200ms这种范围的时间之内就够了。
而如果场景是一个没多大的young gen(例如2GB),但新生对象几乎全都能活过下一次young gen GC的话,活对象占堆空间的比例很高时copying GC的效率就会很差…此时young gen GC要消耗的时间会远大于前一个场景的。
缺点
需要两倍内存空间,不适用如老年代存在大量长期存活对象的情况。
应用
新生代采用了此算法,因为新生代大多对象的特点是朝生夕灭,一般MinorGC就要回收大量新生代对象,存活对象少,剩余对象直接从from survivor
和eden
区copy到to survivor
就好了,适用于快速的复制算法,相当于copy算法以较小的空间换较快的时间。
具体来说,DefNew ParNew PSScavenge都用了此算法。
关于为什么复制算法快于标记-整理算法的讨论
关于JVM垃圾搜集算法(标记-整理算法与复制算法)的效率
GC复制算法和标记-压缩算法的疑问
算法实现
此算法结合了"标记-清除"和"复制"两个算法的优点,经典的实现算法是LISP2,分为四个阶段:
注意mark阶段只遍历活对象,后面三步都要遍历全堆(可以通过bitmap marking来降低遍历全堆的开销。后来有一些更复杂的算法可以把后三步合成到一步来实现,但这样也至少有分离的mark和compact两步,总体效率仍然低于copying GC。
关于Compact
在GC器确定内存中的哪些对象是活的以及哪些是垃圾后,就可以压缩内存,即将所有活动对象移动到一起,最后完全回收剩余的内存。
压缩完成后,很方便地就能快速分配内存给新对象了,因为可以利用简单的指针就能跟踪可用于对象分配的下一个空闲位置。
优点
此算法避免了"标记-清除"的碎片问题可创造空闲的连续大空间,同时也避免了"复制"算法的两份空间导致空间利用率低的问题。
缺点
但因需要多次遍历堆中的对象关系图、标记、压缩等操作所以速度肯定没有复制算法块,故而新生代还是继续采用复制算法。
Compact和Coping区别:
实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。
增量收集适用于:当需要由并发收集器提供的低暂停时间的应用程序,在具有少量CPU(例如1或2核)的机器上运行时,此功能非常有用。
CMS能采用增量收集。该模式可通过周期性地停止并发收集阶段来处理应用程序,以此来减少很长并发阶段带来的的停止时间的影响。具体来说,CMS的工作被分为若干小ChunkTime,这些时间被分配在每次YoungGC之间调度。该特性适用于应用程序需要低暂停时间的CMS,且运行在少量CPU的机器上(比如1或2核)时。
分代GC是一种部分收集(partial collection
)的做法。在执行部分收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC Roots的一部分。
具体到分两代的分代式GC来说,如果第0代叫做年轻代,第1代叫做老年代。那么如果有minor GC只收集年轻代里的垃圾,则年轻代属于“收集部分”,而老年代属于“非收集部分”。此时,从老年代指向年轻代的引用就必须作为minor GC的GC Roots的一部分。
具体到HotSpot VM里的分两代式GC来说,除了老年代到年轻代的引用之外,有些带有弱引用语义的结构,例如记录所有当前被加载的类的sun.jvm.hotspot.memory.SystemDictionary
、记录字符串常量引用的sun.jvm.hotspot.memory.StringTable
等,在minor GC时必须要作为Strong GC Roots
。而他们在收集整堆的Full GC时则不会被看作Strong GC Roots
。换句话说,young GC比full GC的GC roots还要更大一些。
分代收集是基于对对象生命周期分析后得出的垃圾回收算法:
把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法进行回收
。
新生代因为对象特点是朝生夕灭,所以在minorGC中选择了只复制少量对象的复制算法(时间和存活对象数成正比,在活对象少时很快),且使用老年代作为HandlePromotion(分配担保)。
注意,YoungGC时只会对年轻代对象做存活标记,只收集年轻代对象,不会管老年代对象。只不过会将老年代引用了年轻代的引用作为YoungGC的GC Roots的一部分。同时,经过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。YoungGC会用到卡表,记录了老年代对新生代引用,避免了去扫描老年代。
具体来说,卡表的具体策略是将老年代的空间分成大小为512B的若干张卡片(card),JVM采用卡表维护了每一个块的状态。卡表本身是单字节数组,数组中的每个元素对应着一张卡。当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如下图所示,卡表3因为3号卡内的对象引用了新生代对象,所以该卡被标记为脏状态(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描:
老年代对象特点是存活时间长,而且没有其他空间来HandlePromotion,所以使用标记->整理/清理(特别是标记清理,因为该算法不用移动对象)。
注意,HotSpot VM里只有CMS有老年代GC即Major GC。
通常年轻代和年老代都是有地址范围的,当YoungGC从GC Roots开始搜索的时候,通过地址指针来匹配地址范围便可得知处于哪个分代中。
具体来说:
post write barrier
里分代检查是在异步处理的部分做的,而不是同步做的。缩短STW时间
对传统的、基本的GC实现来说,由于它们在GC的整个工作过程中都要STW,如果能想办法缩短GC一次工作的时间长度就是件重要的事情。如果说收集整个GC堆耗时太长,那不如只收集其中的一部分?于是就有好几种不同的划分(partition)GC堆的方式来实现部分收集,而分代式GC就是这其中的一个思路。
这个思路所基于的基本假设大家都很熟悉了:weak generational hypothesis
——大部分对象的生命期很短(die young
),而没有die young的对象则很可能会存活很长时间(live long
)。这是对过往的很多应用行为分析之后得出的一个假设。
基于这个假设,如果让新创建的对象都在年轻代里创建,然后频繁收集年轻代,则大部分垃圾都能在minor GC中被收集掉。由于年轻代的大小配置通常只占整个GC堆的较小部分(年轻代和老年代的比例默认为1:3),而且较高的对象死亡率(或者说较低的对象存活率)让它非常适合使用复制算法来收集,这样就不但能降低单次GC的时间长度,还可以提高GC的工作效率。
提高并发式GC所能应付的内存分配速率
对于一个并发GC来说,能够尽快回收出越多空间,就能够应付越高的应用内存分配速率,从而更好地保持GC以完美的并发模式工作。
因为并发GC根本上要跟应用玩追赶游戏:应用一边在分配,GC一边在收集,如果GC收集的速度能跟得上应用分配的速度,那就一切都很完美;一旦GC开始跟不上了,垃圾就会渐渐堆积起来,最终到可用空间彻底耗尽的时候,应用的分配请求就只能暂停来等GC追赶上来。
标记清除 | 标记压缩/整理 | 复制 | |
---|---|---|---|
速度 | 中 | 慢 | 快 |
时间开销 | mark存活对象阶段与存活对象数成正比,遍历清除不存活对象阶段与整堆大小成正比 | mark存活对象阶段与存活对象数成正比,遍历存活对象进行整理阶段与存活对象数成正比 | 遍历复制存活对象,与存活对象数成正比 |
空间开销 | 少,但有内存碎片 | 少,无内存碎片 | 两倍空间,无内存碎片 |
移动对象 | 否 | 是 | 是 |
原因如下:
Serial
(就是DefNew), ParNew
,CMS
,SerialOld
(MSC
)可以任意搭配。ParallelScavenge
体系里的PSScavenge
(即上图中的Parallel Scavenge
),只能和其同一体系的Parallel Old
或分代体系的MSC搭配(ParallelScavenge和Serial Old的连线,被称为PSMarkSweep
,是从分代式GC框架里抽出来的Serial Old收集器,加了一层包装而已)。原本HotSpotVM里并没有并行GC,当时只有NewGeneration
,且新生代,老年代都使用串行回收收集。
后来准备加入新生代并行GC,就把NewGeneration改名为DefNewGeneration
,然后把新加的并行版叫做ParNewGeneration
,他俩都在Hotspot VM”分代式GC框架“内:
最开始,新生代GC只有属于分代GC框架体系的DefNewGeneration和ParNewGeneration,但有个人自已硬写了个新的并行GC,测试后效果还不错。于是也被放入VM的GC中,即ParallelScavenge
-一个注重吞吐量优先的并行GC。
这个时候就出现了两个新生代的并行GC收集器:ParNewGeneration和ParallelScavenge。
Scavenge
或者叫scavenging GC
,其实就是copying GC(即复制算法GC)的另一种叫法而已。
HotSpot VM里的GC都是在minor GC收集器里用scavenging的,DefNew、ParNew和ParallelScavenge都是,只不过DefNew是串行的copying GC,而后两者是并行的copying GC。 由此名字就可以知道,ParallelScavenge的初衷就是把scavenge
给并行化。换句话说就是把minor GC并行化。
至于full GC则不是当初关注的重点。
PSMarkSweep
(“ParallelScavenge的MarkSweep”),仍然是老年代串行收集。这里的ParallelScavenge已经不是Parallel Scavenge(并行新生代收集器),而是一套GC框架体系。PSScavenge
,老年代收集器叫:PSMarkSweep
。(PS看成是ParallelScavenge缩写,作为前缀)。后来,因为未知的原因,老年代GC的并行化没有在VM分代式GC框架中完成,而选择了在ParallelScavenge框架中完成。其成果就是使用了LISP2算法的并行版的full GC收集器,名为PSCompact(“ParallelScavenge-MarkCompact”),收集整个GC堆,就是上图中右下方的Parallel Old
。
PSCompact基于标记-整理算法。
在JConsole查看时:
GC有很多分类,如:
-XX:+UseSerialGC
-XX:+UseParNewGC
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
-XX:+UseG1GC
(G1收集器)。Young GC + mixed GC(新生代,再加上部分老年代),以及在G1 GC算法赶不上新对象分配时使用 Full GC for G1 GC算法,此时开销很大。简介
最早的GC器,串行方式进行GC,其他所有线程暂停等待。
打开方式
使用-XX:+UseSerialGC
打开。
GC范围
分为年轻代和老年代。
特点
但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,SerialGC也可以用在小数据量(100M左右)情况下的多处理器机器上。
SeriaNew(DefNew)
Client模式下默认新生代收集器(内存100MB左右新生代时,STW时间可控制在100毫秒以内),且对STW没有极短的时长要求的,可在调试时使用,或是单CPU场景。
在当代硬件条件下,串行收集器可以有效地管理许多非常重要的应用程序,其中64MB堆和相对较短的最坏情况暂停时间还不到半秒。
SerialOld
Concurrent Mode Failure
时使用SerialOld进行Full GC。-XX:+UseParallelGC
打开-XX:ParallelGCThreads
参数来限制ParNew GC时的线程数。年轻代ParNew基于复制算法,需要STW,可以和基于标记-整理的SerioOld或基于标记-清除的CMS结合使用。
此收集器可以进行如下配置:
-XX:ParallelGCThreads=n
限制并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。简介
Parallel Scavenge收集器和ParNew类似,是一个新生代并行多线程收集器,使用复制算法。
不过和ParNew不同的是,Parallel Scavenge收集器的关注点在于达到一个可控制的吞吐量。
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+运行GC时间)。如果虚拟机一共运行100分钟,垃圾收集运行了1分钟,那么吞吐量就是99%。
停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多用户交互的任务。
开启方法
分为年轻代(-XX:+UseParallelGC
)和老年代PS Compact(-XX:+UseParallelOldGC
)。
特点
侧重于吞吐量的控制,提升用户线程的CPU使用率。
下图为PSScavenge+ParallelOld组合的GC过程:
PSScavenge
,老年代收集器叫:PSMarkSweep
。(PS看成是ParallelScavenge缩写,作为前缀)。PS Compact
基于标记整理算法,可以和年轻代Parallel Scavenge配合使用,是个并行的全堆收集器-XX:+ScavengeBeforeFullGC
。PSScavenge和ParNew都是并行收集年轻代,目的和性能其实都差不多。最明显的区别有下面几点:
UseDepthFirstScavengeOrder
参数来选择是用深度还是广度优先。但在JDK6u18之后这个参数被去掉,PSScavenge变为只用深度优先遍历。ParNew则是一直都只用广度优先顺序来遍历。adaptive size policy
,而ParNew及分代式GC框架内的其它GC都没有实现完(倒不是不能实现,就是麻烦+没人力资源去做)。所以千万别在用ParNew+CMS的组合下用UseAdaptiveSizePolicy,请只在使用UseParallelGC或UseParallelOldGC的时候用它。Non Uniform Memory Access Architecture
)技术,可以使众多服务器像单一系统那样运转,同时保留小系统便于编程和管理的优点)的优化;而ParNew一直没有得到NUMA优化的实现。最大垃圾回收暂停时间
指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=
指定,单位为毫秒。如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以尽量达到指定值。
注意,设定此值可能会减少应用的吞吐量,因为GC时间的缩短是以牺牲吞吐量和新生代空间为代价的,短的GC时间会导致更加频繁的垃圾收集行为,从而导致整体吞吐量的降低。比如系统会自动把新生代调小,GC更小的新生代肯定快于更大的,所以GC会更频繁,比如原来每10秒一次GC+每次STW100毫秒,现在变为5秒一次GC+每次STW70毫秒,注定GC会更加频繁,整体吞吐量下降。
吞吐量
吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=N
来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19
时,表示5%的时间用于GC。默认情况为99,即1%的时间用于GC。
GC自适应策略
参数-XX:UseAdaptiveSizePolicy
,这是一个开关参数,当打开后,不需手工指定新生代的大小(-Xmn
)、Eden和Survivor的比例(-XX:SurvivorRatio
)、晋升老年代对象年龄(-XX:PretenureSizeThreshold
)等细节了。JVM会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量,这叫GC自适应的调节策略。这也是Parallel Scavenge和ParNew的一个重要区别。
简介
CMS(Concurrent Mark Sweep)收集器是一种以获取最短GC停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多。
打开方式
使用-XX:+UseConcMarkSweepGC
打开
并发收集思想
并发GC根本上要跟应用玩追赶游戏:应用一边在分配,GC一边在收集。如果GC收集的速度能跟得上应用执行分配新对象的速度,那就一切都很完美;一旦GC开始跟不上了,垃圾就会渐渐堆积起来,最终到可用空间彻底耗尽的时候,应用的分配请求就只能暂停等GC追赶上来。所以,对于一个并发GC来说,能够尽快回收出越多空间,就能够应付越高的应用内存分配速率,从而更好地保持GC以完美的并发模式工作。
总结来说,CMS的设计聚焦在获取最短的时延,为此它“不遗余力”地做了很多工作,包括尽量让应用程序和GC线程并发、增加可中断的并发预清理阶段、引入卡表等,虽然这些操作牺牲了一定吞吐量但获得了更短的回收停顿时间。
目标和特点
可保证大部分用户代码和GC并发进行,GC只STW很少的时间,以获取最短回收停顿时间为目标。CMS适合对响应时间要求比较高的中、大规模应用。CMS缺点如下:
初始标记(CMS initial mark)- 需要STW
初始标记只标记GC Roots
能直接关联到的对象,速度很快。
并发标记(CMS concurrent mark)- 时间较长
并发标记阶段就是进行从GC Roots开始沿着引用链搜索存活对象的过程。此时用户线程和GC线程并发地继续运行,又可能产生垃圾。此过程中如果遇到old->young
会立刻停止这一路径的tracing动作。
重新标记(CMS remark)- 需要STW
重新标记阶段是为了修正并发标记期间因用户程序继续运行而导致的GC标记产生变动(变动是指修改了对象与引用的关系)的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
CMS的incremental update
设计使得它在remark阶段必须重新扫描所有线程栈和整个年轻代作为GC Roots进行扫描。
并发清除(CMS concurrent sweep)- 时间较长
并发清除阶段会清除没有被标记存活的对象区域(无论没有对象还是有死亡对象都进行回收)。此时用户线程和GC线程并发地继续运行,又可能产生垃圾,这些垃圾需要等到下一次GC处理。
实现算法
CMS基于标记-清除算法实现。可参考并发垃圾收集器(CMS)为什么没有采用标记-整理算法来实现?
而使用标记清除算法不用移动大量存活的老年代对象,也不用修正指针,而且标记和清除两个阶段都可以和用户代码逻辑并行执行,效率高,所以CMS老年代GC采用了标记清除算法。
CMS基于并行的标记-清除算法,主要减少年老代的STW时间,可在应用不停止的情况下使用独立的GC线程跟踪可达对象。CMS会定时去检查老年代的使用量,当超过触发比例就会启动一次CMS Major GC,对老年代做并发收集.
在每个年老代垃圾回收周期中,在收集初期CMS会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
CMS使用多处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
CMS原则
宁愿放过已死亡的对象不回收(下次GC再回首回收),也不能错误回收活着的对象。
假设有对象b如下:
class B {
A a;
..
}
其中Field a
在并发标记时是指向对象a1,但在并发标记完成前Field a
转而指向a2。
我们原则是必须要保证a2不会被回收(而a1可以被错误保留,等待下次gc再回收)。这就需要重标记阶段来标记a2存活。简单地说,就是在并发标记阶段,记下所有改动的引用到Remember Set log(RSLog。最后在重标记阶段再trace一遍从变动过的引用开始的局部object graph。
并发标记阶段变动对象处理
关于空闲区域指定-Free List
连续空闲内存可用指针来指向,但因CMS采用标记-清除算法会产生不连续的区域,所以使用了若干称之为Free List
的结构来进行标注,每个free list都指向一个内存Region。分配对象时,必须根据所需的内存大小来搜索适合的list,使用其对应的Region来存放新对象。这种分配代价较昂贵。
关于浮动垃圾
由于在应用运行的同时进行GC,虽然在重标记阶段后可保证所有存活对象都被正确标记出来,但有些对象可能在标注阶段已经死掉应该被回收,对于CMS来说本次GC不会处理而是留到下一次GC处理,这样就被称为浮动垃圾(Floating Garbage
)。
所以,CMS一般需要20%的预留空间用于这些浮动垃圾。
Concurrent Mode Failure
并发收集器在应用运行时进行并发收集,所以需要保证堆在GC的这段时间有足够的空间供程序使用。否则,GC还未完成,堆空间先满了。这种情况下将会发生Concurrent Mode Failure
(并发模式失败),此时会发生Stop the world,回退到使用serialOldGC的mark-compact算法做full GC来进行GC。
通过设置-XX:CMSInitiatingOccupancyFraction=
指定还有多少剩余堆百分比时开始执行并发收集,使用CMS时必须非常小心的调优,尽量推迟由碎片化引致的full GC的发生。一旦发生full GC,暂停时间可能又会很长,这样原本为低延迟而选择CMS的优势就没了。
内存碎片
CMS Full GC除了上述Concurrent Mode Failure,还可能由于老年代搜集后的内存碎片导致对象无法分配,也会触发Full GC来整理内存。这种情况的表现是堆内存还有较多剩余空间,但却发生了Full GC!
为什么初始标记不能也做成并发的?
可以做成并发的,就是实现起来麻烦一些而已。Android Runtime里的CMS实现在初始标记的时候采用了checkpointing做法,就不是完全stop-the-world而是一个个线程分别错开一点时间来暂停。这样系统在扫描一个线程的栈的时候其它线程还可以跑,比stop-the-world的影响小。
如果在并发清除阶段产生了新的对象,这个对象没有被标记为可达,并且此时已经错过了重新标记阶段,如何保证这个对象在并发清除阶段不被回收?(GC是标记活的,收回未标记的内存区域)
这是并发GC的一个重要的设计点。CMS的做法是在并发标记过程中创建出来的新对象都是在young gen里的,并发标记并不管它们的生死。而且过程中还可以有若干次ParNew GC(young GC)收集young gen;
等到concurrent marking结束,会做一个STW的重新标记,此时会完整扫描一次young gen,把里面的对象当作root set的一部分,这样新创建出来的对象就会被扫描上。中间还有concurrent precleaning
/ abortable concurrent precleaning
。
-XX:CMSInitiatingOccupancyFraction=
指定还有多少剩余堆百分比时开始执行并发收集总的来说,如果您的应用程序需要更短的GC暂停,并且在应用程序运行时能够与GC共享处理器资源(由于它
并发性,CMS收集器在收集周期中占用CPU周期),请使用CMS收集器。
通常,拥有大量长寿对象的应用,或者最低拥有1-2核的机器可开始尝试CMS。
重视响应速度和用户体验的应用,如网站
B/S系统
可参考Garbage First介绍
从设计目标看G1完全是为了大型服务端应用而准备的,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量:
当然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。适合追求停顿时间的应用。
Region
G1采取了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控制等问题——G1将整个Java堆分成相同大小的分区(Region),如下图所示:
分代
每个Region都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。
年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。而他们在物理上不需要连续,则带来了额外的好处——为了达到对回收时间的可预计性,G1在扫描了Region以后,对其中的活跃对象的大小进行排序,每次根据允许的收集时间来优先回收价值最大的Region,以便快速回收空间(要复制的活跃对象少了,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。
而新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。
在G1中,还有一种特殊的区域,叫Humongous
区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
内存整理
G1还是一种带压缩(整理)的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。
一组可被回收的Region的集合,称为Collection Set
,简称CSet。在CSet中存活的数据会在GC过程中被移动到另一个可用Region,CSet中的Region可以来自Eden空间、survivor空间、或者老年代。
CSet会占用不到整个堆空间的1%大小。
已记忆集合即Remembered Set
,简称为RSet
,G1引入了RSet来记录其他Old Region引用Eden区Region对象的情况,属于points-into
结构(谁引用了我的对象)。作为对比,在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out
,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
而G1中因为分区数量众多,有时候不需要扫描全Region,仅关心部分Region及该Region对象被其他Region的引用情况,所以G1 RSet是point-in
思想。具体来说,由于每次GC时,所有年轻代Region都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。
如下图所示,Region1和Region3中的对象都引用了Region2中的对象,因此在Region2的RSet中记录了这两个引用。
摘一段R大的解释
G1 GC则是在points-out的card table之上再加了一层结构来构成points-into RSet:每个region会记录下到底哪些别的Region有指向自己的指针,而这些指针分别在哪些card的范围内。
这个RSet其实是一个HashTable,key是别的Region的起始地址,value是一个集合,里面的元素是card table的index。举例来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,它的意思就是region B的一个1234 card里有引用指向region A。
所以对region A来说:
points-into
的关系;card table
仍然记录了points-out的关系。考虑如下情况,其中黑色代表根对象或已扫描该对象及子对象,灰色代表对象本身被扫描但还没扫描完该对象中的子对象,白色代表未被扫描对象及扫描完成后依然不可达对象:
这个时候执行A.c=C;B.c=null
,则对象图变为:
这时候垃圾收集器再标记扫描的时候就会下图成这样:
我们会发现随着B被扫描完后本来或者的C却被认为是垃圾需要被回收了,显然错误。解决方式有两种:
在插入的时候记录对象
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象(未被扫描对象)的引用被赋值到一个黑对象(已扫描本身及子对象的对象)的字段里,那就把这个白对象变成灰色的,表明需要扫描其子对象。即CMS是在插入的时候记录下来。
而CMS会忽略那些本来或者但在并发标记阶段删除(死亡)的对象,留到下一次GC处理。
在删除的时候记录对象
在G1中,使用的是STAB
(snapshot-at-the-beginning
)的方式,删除的时候记录所有的对象,它有3个步骤:
SATB是维持并发GC的正确性的一个手段,G1 GC的并发理论基础就是SATB,SATB是由Taiichi Yuasa
为增量式标记清除垃圾收集器设计的一个标记算法。Yuasa的SATAB的标记优化主要针对标记-清除垃圾收集器的并发标记阶段。按照R大的说法:CMS的incremental update
设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。
SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous marking bitmap
和next marking bitmap
。previous marking bitmap保存了最近一次完成的标记信息,而并发标记周期会创建并更新next marking bitmap。随着时间的推移,previous marking bitmap会越来越过时,最终在并发标记周期结束的时候,next marking bitmap会将previous marking bitmap覆盖掉。
G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。分代式G1模式下有两种选定CollectionSet的子模式,分别对应young GC与mixed GC:
Young GC
选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。
Mixed GC
选定所有young gen里的region,外加根据global concurrent marking
统计得出收集收益高的若干old gen region。在用户指定的开销目标范围内尽可能选择收益高的old gen region。
可以看到young gen region总是在CSet内。因此分代式G1不维护从young gen region出发的引用涉及的RSet更新。
还需要注意的是,Mixed GC的initial marking阶段一般都会在young GC里面就顺带做了,因为工作有重叠。(除了System.gc()+ -XX:+ExplicitGCInvokesConcurrent的情况)
Full GC
如果mixed GC无法跟上mutator(指会修改引用的应用程序)分配的速度(针对老年代分区的回收速度比较慢、对象过快得从新生代晋升到老年代、有很多大对象直接在老年代分配),导致没有足够的空region来完成mixed GC,那么就会使用serial old GC( mark-compact)来对整堆收集一次(Full GC)。
dirty card
队列更新RSet
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分可以相对独立的执行的两步:
具体步骤如下:
初始标记(Initial Marking)- 需要STW
初始标记阶段仅仅只是标记一下GC Roots
能直接关联到的对象,并将它们的字段压入扫描栈(marking stack
)中等到后续扫描,且修改TAMS
(Next Top at Mark Start
)的值,让下一并发标记阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要STW,但耗时很短。具体如下:
previous marking bitmap
,一个为next marking bitmap
,bitmap中包含了一个bit的地址信息来指向对象的起始点。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。next marking bitmap
,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入TAMS
(Next Top at Mark Start
)中,之后恢复所有应用线程。并发标记(Concurrent Marking)- 时间较长
按照之前Initial Marking
扫描到的对象图(在marking stack中)进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的依赖关系则记录到remembered set logs
(RSLog)中,而新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。
并发标记阶段耗时较长,可与用户程序并发执行,也可以被 STW的年轻代GC中断
最终标记暂停(Final Marking Pause)- 需要STW
最终标记阶段是为了修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记(变化记录在线程RSLog里),G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。当应用线程的RSLog未满时,是不会放入filled RS buffers
中的,此时,这些RSLog中记录的card的修改就会被更新了。具体来说,最终标记阶段需要把RSLog的数据合并到Remembered Set
中。
同时这个阶段也进行弱引用处理(reference processing)。
注意最终标记阶段暂停与CMS的remark阶段有一个本质上的区别,那就是G1最终标记暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。
最终标记阶段需要停顿线程,多GC线程并行执行。
存活对象计算及清除-筛选回收(Live Data Counting and Cleanup)- 需要STW
该阶段首先对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间来制定回收计划并。但需要注意的是,该阶段不会在堆上清理实际对象,而是在marking bitmap
里统计每个region被标记为活的对象有多少。
这个阶段如果发现完全没有活对象的region,就会将其整体回收到可分配region列表中。
筛选阶段其实也可以做到与应用程序并发执行,但未这么做的原因是:
在全局并发标记完成后,G1现在可以知道哪些老的分区可回收垃圾最多。在某个时刻开始Mixed GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。混合式垃圾收集如下图:
在完成evacuation后,Eden和Survivor区部分移动到新的Survivor区,部分晋升到Old区。
注意:Eden和混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
evacuation后如下图:
值得注意的是,在G1中,并不是说Final Marking Pause
执行完了,就肯定执行Cleanup
这步的,由于这步需要STW,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:
to space
的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H
时,就执行Cleanup这个步骤;full-young
和partially-young
的分代模式的G1而言,则还有情况会触发Cleanup的执行:
具体来说,Evacuation阶段依赖每个Region的Remember Set来选定若干Region构成CollectionSet,采用并行复制算法把一部分region里的活对象拷贝到空region里去,然后回收原本的region的空间。整个过程完全暂停。
这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
类似其他GC器老年代和年轻代之间的引用,G1中每个Region也有对应的Remmbered Set用来记录跨Region引用。具体来说,JVM发现程序对Reference
类型的数据进行写操作时,会产生一个Write Barrier来暂时中断写操作,然后检查该引用是否处于不同的Region之间。如果跨Region就通过Card Table将引用信息记录到被引用对象所属Region的Remembered Set中。
随后,在GC发生时将Remembered Set加入到GC Roots,可避免全堆扫描,而只扫描GC Roots和相关的Region即可。
巨型对象概念
在G1中,如果一个对象的大小超过Region大小的一半,该对象就被定义为巨型对象(Humongous Object)。
巨型对象直接分配到老年代Region(注:有的文章说是有专门的巨型对象Region),如果一个对象的大小超过一个Region的大小,那么会直接在老年代分配两个连续的Region来存放该巨型对象。
巨型对象Region一定是连续的,分配之后也不会被移动——没啥益处。
由于巨型对象的存在,G1的堆中的分区就分成了三种类型:新生代分区、老年代分区和巨型分区,如下图所示:
-XX:G1HeapRegionSize
参数,减少或消除巨型对象的分配。在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?
GC日志
G1启动了标记周期,但是在并发标记完成之前,就发生了Full GC,日志通常如下所示:
51.408: [GC concurrent-mark-start]
65.473: [Full GC 4095M->1395M(4096M), 6.1963770 secs]
[Times: user=7.87 sys=0.00, real=6.20 secs]
71.669: [GC concurrent-mark-abort]
GC分析
GC concurrent-mark-start
开始之后就发生了FULL GC,这说明在G1启动标记周期之后、Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。
原因可能是针对老年代分区的回收速度比较慢 / 对象过快得从新生代晋升到老年代 / 有很多大对象直接在老年代分配。
解决方案
60.238: [GC pause (young) (to-space overflow), 0.41546900 secs]
-XX:G1ReservePercent
的值以及增加总的堆大小,为“目标空间”增加预留内存量-XX:InitiatingHeapOccupancyPercent
提前启动标记周期-XX:ConcGCThreads
选项的值来增加并发标记线程的数目GC分析
如果在GC日志中看到莫名其妙的FULL GC日志,又对应不到上述讲过的几种情况,那么就可以怀疑是巨型对象分配导致的。当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。
这里我们可以考虑使用jmap命令进行堆dump,然后通过MAT对堆转储文件进行分析。
解决方案
这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
G1(Garbage first)收集器是最先进的收集器之一,在JDK9中提升了性能,自动决定若干重要优化参数,被当做了默认GC器。G1是面向服务端的垃圾收集器。与其他收集器相比,G1收集器有如下优点:
并行与并发
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW停顿的时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集
可以不使用其他收集器配合就可管理整个Java堆。使用G1收集器时,Java堆的内存布局与其他收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
内存整理-无碎片
与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
可预测的停顿
G1除了降低停顿外,还能建立可预测的停顿时间模型。降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。这几乎是实时JavaGC器特征。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行所有Region的垃圾收集。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表。每次优先收集价值最大的那个Region。这样就保证了在有限的时间内尽可能提高效率。
一文读懂Java 11的ZGC为何如此高效
串行处理器:
并行处理器:
并发处理器:
在JDK7中ParNew/CMS对比G1
以下对比中,G1的参数为-XX:G1RSetUpdatingPauseTimePercent=20
,红色为ParNew/CMS,蓝色为G1。
可参考JVM服务端常用启动配置(JDK8)
-Xms:设置堆的最小空间大小。
-Xmx:设置堆的最大空间大小。
-Xmn:设置年轻代大小
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
-XX:MaxTenuringThreshold=0:设置新生代对象最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
注意:没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制:
老年代空间大小=堆空间大小-年轻代大空间大小
典型JVM参数配置参考:
-Xmx3550m
-Xms3550m
-Xmn2g
-Xss128k
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
其中:
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大 小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000 左右。
-XX:+UseParallelOldGC
新生代,老年代都使用并行回收收集器。其中PS Scavenge 收集新生代,而Parallel Old 收集整堆。
-XX:+UseParallelGC -XX:ParallelGCThreads=20
UseParallelGC:新生代使用并行回收收集器,老年代使用串行收集器;
ParallelGCThreads:配置新生代并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
-XX:MaxGCPauseMillis=100
设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
-XX:+UseAdaptiveSizePolicy
设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4
的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
-XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
UseCMSCompactAtFullCollection:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生"碎片",使得运行效率降低。此配置可打开CMS对年老代的压缩。可能会影响性能,但是可以消除碎片.
CMSFullGCsBeforeCompaction:此值设置运行多少次CMS GC以后对老年代内存空间进行压缩、整理。
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
-XX:+CMSClassUnloadingEnabled:可使permgen加入cms collection cycle中来
-XX:+UseCMSInitiatingOccupancyOnly:控制CMS cycle是不是纯粹由oldgen(或permgen if CMSClassUnloadingEnabled=TRUE)的使用比例来触发;
-XX:CMSInitiatingOccupancyFraction=70和-XX:CMSInitiatingPermOccupancyFraction
可以精细在oldgen和permgen分别控制触发CMS cycle的比例。
注意,在永久代移除后,CMS相关参数中,CMSInitiatingPermOccupancyFraction
被去掉了,其他三个参数继续有用:UseCMSInitiatingOccupancyOnly、CMSClassUnloadingEnabled和CMSInitiatingOccupancyFraction。
参数名 | 含义 | 默认值 |
---|---|---|
-XX:+UseG1GC | 使用G1收集器 | JDK1.8中还需要显式指定 |
-XX:MaxGCPauseMillis=n | 设置一个期望的最大GC暂停时间,这是一个柔性的目标,JVM会尽力去达到这个目标 | 200 |
-XX:InitiatingHeapOccupancyPercent=n | 当整个堆的空间使用百分比超过这个值时,就会触发一次并发收集周期,记住是整个堆 | 45 |
-XX:NewRatio=n | 新生代和老年代的比例 | 2 |
-XX:SurvivorRatio=n | Eden空间和Survivor空间的比例 | 8 |
-XX:MaxTenuringThreshold=n | 对象在新生代中经历的最多的新生代收集,或者说最大的岁数 | G1中是15 |
-XX:ParallelGCThreads=n | 设置垃圾收集器的并行阶段的垃圾收集线程数 | 不同的平台有不同的值 |
-XX:ConcGCThreads=n | 设置垃圾收集器并发执行GC的线程数 | n一般是ParallelGCThreads的四分之一 |
-XX:G1ReservePercent=n | 设置作为空闲空间的预留内存百分比,以降低目标空间溢出(疏散失败)的风险。默认值是 10%。增加或减少这个值,请确保对总的 Java 堆调整相同的量 | 10 |
-XX:G1HeapRegionSize=n | 分区的大小 | 堆内存大小的1/2000,单位是MB,值是2的幂,范围是1MB到32MB之间 |
-XX:G1HeapWastePercent=n | 设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,JavaHotSpotVM不会启动混合垃圾回收周期(注意,这个参数可以用于调整混合收集的频率)。 | JDK1.8是5 |
-XX:G1MixedGCCountTarget=8 | 设置并发周期后需要执行多少次混合收集,如果混合收集中STW的时间过长,可以考虑增大这个参数。(注意:这个可以用来调整每次混合收集中回收掉老年代分区的多少,即调节混合收集的停顿时间) | 8 |
-XX:G1MixedGCLiveThresholdPercent=n | 一个分区是否会被放入mix GC的CSet的阈值。对于一个分区来说,它的存活对象率如果超过这个比例,则改分区不会被列入mixed gc的CSet中 | JDK1.6和1.7是65,JDK1.8是85 |
输出格式:
[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]
打印GC中的详细信息,包括使用什么搜集器
输出格式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
PrintGCTimeStamps可与上面两个混合使用,打印GC发生的时间,自JVM启动以后开始统计,单位为秒。
还有个-XX:+PrintGCDateStamps
,可记录GC发生的时间信息。
输出格式:
11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
打印每次GC期间,用户程序继续并发执行时间。可与前面提到的参数混合使用
输出格式:
Application time: 0.5291524 seconds
打印GC期间用户程序暂停的时间。可与上面混合使用
输出格式:
Total time for which application threads were stopped: 0.0468229 seconds
打印GC前后的详细堆栈信息
输出格式:
34.702: [GC {Heap before gc invocations=7:
def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)
to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)
tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
}
, 0.0757599 secs]
与上面几个配合使用,把相关日志信息记录到文件以便分析。
我们通过 -XX:+UseSerialGC
选项指定使用串行垃圾收集器, 并使用下面的启动参数让 JVM 打印出详细的GC日志:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
这样配置以后,发生GC时输出的日志就类似于下面这种格式(为了显示方便,已手工折行):
2015-05-26T14:45:37.987-0200: 151.126:
[GC (Allocation Failure) 151.126:
[DefNew: 629119K->69888K(629120K), 0.0584157 secs]
1619346K->1273247K(2027264K), 0.0585007 secs]
[Times: user=0.06 sys=0.00, real=0.06 secs]
2015-05-26T14:45:59.690-0200: 172.829:
[GC (Allocation Failure) 172.829:
[DefNew: 629120K->629120K(629120K), 0.0000372 secs]
172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]
1832479K->755802K(2027264K),
[Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs]
[Times: user=0.18 sys=0.00, real=0.18 secs]
6.2中的GC日志暴露了JVM中的一些信息。事实上,这个日志片段中发生了 2 次垃圾回收事件(Garbage Collection events)。其中一次清理的是年轻代(Young generation), 而第二次处理的是整个堆内存。下面我们来看,如何解读第一次GC事件,发生在年轻代中的小型GC(Minor GC):
通过上面的分析, 我们可以计算出在垃圾收集期间, JVM 中的内存使用情况。在垃圾收集之前, 堆内存总的使用了 1.54G (1,619,346K)。其中, 年轻代使用了 614M(629,119k)。可以算出老年代使用的内存为: 967M(990,227K)。
下一组数据( -> 右边)中蕴含了更重要的结论, 年轻代的内存使用在垃圾回收后下降了 546M(559,231k), 但总的堆内存使用(total heap usage)只减少了 337M(346,099k). 通过这一点,我们可以计算出, 有 208M(213,132K) 的年轻代对象被提升到老年代(Old)中。
这个GC事件可以用下面的示意图来表示, 上方表示GC之前的内存使用情况, 下方表示结束后的内存使用情况:
user
用户空间(Linux系统内核以外)在GC期间消耗的CPU时间
sys
Linux系统内核在GC期间消耗的CPU时间。
real
GC期间消耗的总CPU时间。
需要注意的是,几乎所有GC事件中real时间< user时间 + sys时间。原因是GC线程并发执行,比如user+sys=2000ms,有5个GC线程并行,则real时间略高于2000ms/5=400ms,原因是CPU竞争或是IO负载较高(可参考brilliant article from LinkedIn engineers)。
首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理
响应时间优先的应用???
尽可能将年轻代设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用
尽可能将年轻代设大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
新生代设置过小
新生代设置过大
Survivor设置过小
导致对象从eden直接到达老年代,降低了在新生代的存活时间,导致更多的FullGC
Survivor设置过大
导致eden过小,增加了MinorGC频率
MaxTenuringThreshold调大晋升年龄
通过将-XX:MaxTenuringThreshold=n
设置为一个较大值,尽量让对象在新生代Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,最终增加对象在年轻代即被回收的概率,从而避免升到老年代造成FullGC。
减少年轻代和年老代花费的时间,一般会提高应用的效率
参见这里
可参考关于full gc频繁的分析及解决
一般FullGC有如下原因和解决方案:
老年代空间不足
调优时尽量让对象在新生代GC时被回收,让对象在新生代多存活一段时间和不要创建过大的对象及数组,从而避免直接在旧生代创建对象 。
Concurrent Mode Failure
参见这里。需要设置-XX:CMSInitiatingOccupancyFraction=
指定还有多少剩余堆百分比时开始执行并发收集,避免CMS 老年代GC时因为又有对象进入老年代,而空间不够导致的FullGC。
还可参考这里开启CMS GC对老年代压缩。
老年代内存碎片
由于采用CMS造成老年代大量内存碎片,无法提供连续空间存入大对象导致触发Full GC。关于内存碎片可参考这里
Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象 。移除永久代换为元空间后没有这个。
统计得到的GC后晋升到老年代的平均大小大于老年代剩余空间
控制好新生代和老年代大小的比例
System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
不要自己显式设置新生代的大小(避免使用-Xmn或-XX:NewRatio参数),如果显式设置新生代的大小,会导致目标时间这个参数失效。
关于设置最大暂停时间
由于G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单
首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡:
-XX:G1HeapRegionSize=n
设置的G1区域的大小。值是2的幂,范围是1MB到32MB之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。
-XX:ParallelGCThreads=n
设置垃圾收集器的STW并行阶段的垃圾收集线程数。将n的值设置为逻辑处理器的数量,最多为8。
如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中n的值可以是逻辑处理器数的 5/16 左右。
-XX:ConcGCThreads=n
设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads) 的1/4左右。
-XX:InitiatingHeapOccupancyPercent=45
设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。
见G1 Full GC
-XX:MaxGCPauseMillis=n
这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,只收集young gen。
当发生MinorGC事件的时候,有一些有趣的地方需要注意:
stop-the-world
,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,MinorGC 执行时暂停的时间将会长很多。所以 Minor GC 的情况就相当清楚了——每次 MinorGC 会清理年轻代的内存。
当young gen中的eden区分配满的时候触发MinorGC。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
至于为什么不是eden+from survivor满才触发minor gc,是因为HotSpot的GC设计是只在eden里直接分配新对象的,survivor space只是个young跟old之间的缓冲区,用来让中等存活期的对象不要过早晋升到old gen。
PSScavenge和SerialOld(PSMarkSweep)搭配使用时,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为参数是-XX:+ScavengeBeforeFullGC
。
虚拟机在进行MinorGC之前,会先判断老年代最大的可用连续空间是否大于新生代的所有对象总空间
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。
如果是指代OldGC,则是清理老年代,只有CMS的concurrent collection
是这个模式,其他的MajorGC都会触发full gc。
CMS GC主要是定时去检查old gen的使用量,CMS GC的initial marking的触发条件是当使用量超过了触发比例就会启动一次CMS GC(因为是并发收集,会产生浮动垃圾,需要预留空间),对old gen做并发收集。
清理整个堆空间—包括年轻代和老年代。
**注意,HotSpot VM的GC里,除了CMS的concurrent collection
能只收集old gen以外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC。
FullGC的例外情况
但PSScavenge和SerialOld(PSMarkSweep)搭配使用时,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量),然后紧接着去用PS MarkSweep执行一次真正的full GC收集全堆。所以有可能观察到这个跟随的full GC时,Eden区没什么变化,Survivor区反而被清空,老年代上升的情况。
两种算法
老年代空间不足
如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发FullGC。为了避免这种情况,最好就是不要创建太大的对象。
持久代空间不足(如果还有的话)
如果有持久代空间的话,系统当中需要加载的类/调用的方法很多,同时持久代当中没有足够的空间,就触发一次FullGC
YoungGC出现promotion failure
promotion failure
发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC.
统计YoungGC发生时晋升到老年代的平均总大小大于老年代的空闲空间
在发生前YoungGC时会判断,是否安全,即当前老年代空间可以容纳YoungGC晋升的对象的平均大小。如果不安全,就不会执行YoungGC,转而执行Full GC。
CMS出现concurrent mode failure,会使用Full GC
显示调用System.gc
mark word
域这类算法有Serial Old GC、PS MarkSweep GC、Parallel Old GC、Full GC for CMS算法和Full GC for G1 GC算法。应该能想到步骤1 2 3 4 都可以并行处理。
需要注意的是,FullGC是整体收集的,无所谓先收集old还是young。marking阶段是整体一起做的,然后compaction是old gen先来再处理young gen。
也就是说,Full GC时,就不在分 “young gen使用young gen自己的收集器(一般是copy算法);old gen使用old gen的收集器(一般是mark-sweep-compact算法)”,而是,整个heap以及perm gen,所有内存,全部的统一使用 old gen的收集器(一般是mark-sweep-compact算法) 一站式搞定。
G1 GC的initial marking的触发条件是Heap使用比率超过某值,跟CMS类似;
打开$TOMCAT_HOME/bin/catalina.sh
export JAVA_OPTS="-Xms4G -Xmx6G -XX:PermSize=256m
-XX:MaxPermSize=512m -Xss1024k -XX:+DisableExplicitGC
-XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=80
-XX:+PrintGC
-XX:+PrintGCTimeStamps"
export CATALINA_OUT=/var/log/java/tomcat-xxx-8000/tomcat/catalina.out
export JAVA_OPTS="-Xms4G -Xmx6G -XX:PermSize=256m
-XX:MaxPermSize=512m -Xss1024k -XX:+DisableExplicitGC
-XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=80
-XX:+PrintGC -XX:+PrintGCTimeStamps
-Xloggc:var/log/java/tomcat-xxx-8000/tomcat/tomcat_gc.log"
可参考
write barrier在GC中的作用?如何理解G1 GC中write barrier的作用?
写屏障是一种内存管理机制,用在这样的场景:
当代码尝试修改一个对象的引用时,在前面放上写屏障就意味着将这个对象放在了写屏障后面。
write barrier在GC中的作用有点复杂,我们这里以trace GC算法为例讲下:
trace GC有些算法是并发的,例如CMS和G1,即用户线程和垃圾收集线程可以同时运行,即mutator(用户应用程序)一边跑,collector一边收集。这里有一个限制是:黑色的对象不应该指向任何白色的对象。如果mutator视图让一个黑色的对象指向一个白色的对象,这个限制就会被打破,然后GC就会失败。针对这个问题有两种解决思路:
通过添加read barriers阻止mutator看到白色的对象;
通过write barrier阻止mutator修改一个黑色的对象,让它指向一个白色的对象。
write barrier的解决方法就是将黑色的对象放到写write barrier后面。如果真得发生了white-on-black这种写需求,一般也有多种修正方法:增量得将白色的对象变灰,将黑色的对象重新置灰等等。我理解,增量的变灰就是CMS和G1里并发标记的过程,将黑色的对象重新变灰就是利用卡表或SATB的缓冲区将黑色的对象重新置灰的过程,当然会在重新标记中将所有灰色的对象处理掉。关于G1中write barrier的作用,可以参考R大的这个帖子里提到的:
然后G1在mutator一侧需要使用write barrier来实现:
这两个动作都使用了logging barrier,其处理有一部分由collector一侧并发执行。 `
G1里在并发标记的时候,如果有对象的引用修改,要将旧的值写到一个缓冲区中,这个动作前后会有一个write barrier,这段可否细说下?
这块涉及到SATB标记算法的原理,SATB是指start at the beginning,即在并发收集周期的第一个阶段(初始标记)是STW的,会给所有的分区做个快照,后面的扫描都是按照这个快照进行;在并发标记周期的第二个阶段,并发标记,这是收集线程和应用线程同时进行的,这时候应用线程就可能修改了某些引用的值,导致上面那个快照不是完整的,因此G1就想了个办法,我把在这个期间对对象引用的修改都记录动作都记录下来,有点像mysql的操作日志。
GC算法中的三色标记算法怎么理解?
trace GC将对象分为三类:白色(垃圾收集器未探测到的对象)、灰色(活着的对象,但是子对象依然没有被垃圾收集器扫描过)、黑色(活着的对象,并且已经被垃圾收集器扫描过)。
垃圾收集器的工作过程,就是通过灰色对象的指针扫描它指向的白色对象,如果找到一个白色对象,就将它设置为灰色,如果某个灰色对象的可达对象已经全部找完,就将它设置为黑色对象。当在当前集合中找不到灰色的对象时,就说明该集合的回收动作完成,然后所有白色的对象的都会被回收。