jvm垃圾收集器介绍
新生代收集器(young generation)
1,serial收集器
是最基本的,发展最悠久的。是一个单线程收集器,只会使用一个cpu或者一个线程完成垃圾收集。它进行垃圾收集时,必须暂停其他所有的线程直到收集结束。serial收集器对于运行在client模式下的虚拟机来说是一个很好地选择。使用标记复制方法来进行新生代的清理。使用serial收集器:-XX:+UseSerialGC(serial + serial old)
2,ParNew收集器
是serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、stop the world、对象分配规则、回收策略等都与serial收集器完全一样,在实现上,两者公用了相当多的代码。使用ParNew收集器: -XX+UseParNewGC(ParNew + Serial Old)
3,Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器.。parallel Scavenge收集器的目标是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
使用parallel Scavenge收集器: -XX:+UseParallelGC(Parallel Scavenge + Serial Old)-XX:+UseParallelOldGC(Parallel Scavenge + Parallel Old)
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMilli参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数:
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能保证内存回收花费的时间不超过设定值。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,-XX:GCTimeRatio=nnn:means not more than 1 / (1 + nnn) of the application execution time be spent in the collector。默认为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为"吞吐量优先"收集器。除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值的关注。这是一个开关参数,当这个参数打开之后,就不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
使用Parallel Scavenge自适应调节策略,把内存管理的调优交给虚拟机去完成是一个不错的选择。只需要把基本的内存数据设置好(如 -Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
Serial,ParNew,Parallel scavenge这几个新生代收集器都是使用标记复制方法进行垃圾收集
老年代收集器(old generation)
4,Serial Old收集器
这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。使用标记清理整理方法来进行垃圾清理。
5,Parallel old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-清理-整理"算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作过程如图3-9所示。
6,cms收集器 Concurrent Mark Sweep
是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字("Mark Sweep")上看,CMS收集器是基于"标记-清除"算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
(1)初始标记(CMS initial mark),记录gc roots直接能引用到的对象,速度很快。
(2)并发标记(CMS concurrent mark),并发标记阶段就是进行GC Roots Tracing的过程;
(3)重新标记(CMS remark),重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
(4)并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要"Stop The World"。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;耗时最大的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
主要优点:
- 并发收集、低停顿。
几个明显的缺点:
- 对CPU资源敏感(会和服务抢资源);
- 无法处理浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection 可以让jvm在执行完标记清除后再做整理
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
parallel Scavenge与cms不能配合工作,老年代使用cms垃圾收集器,那么新生代只能选择ParNew或者Serial收集器中的一个。
ParNew是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
CMS的相关参数
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次
FullGC后都会压缩一次 - -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认
是92,这是百分比) - -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-
XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定
值,后续则会自动调整 - -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少
老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在
remark阶段
7,G1收集器
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
G1收集器一次GC的运作过程大致分为以下几个步骤:
- 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
- 并发标记(Concurrent Marking):同CMS的并发标记
- 最终标记(Remark,STW):同CMS的重新标记
-
筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
具备以下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的"标记-清理"算法不同,G1从整体上来看是基于"标记-整理"算法实现的收集器,从局部上看是基于"复制"算法实现的,但无论如何,这几个算法都意味着G1运行期间不会产生内存碎片,收集后能提供规整的可用内存。
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点。G1除了追求低停顿外,还能建立可预测的停顿啥时间模型,可以明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1垃圾收集分类
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,而且G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。
G1垃圾收集器优化建议
假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。
或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。
所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
2 理解GC日志
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 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"和"[Full gc"说明了这次垃圾收集的停顿类型,书中说:而不是用来区分新生代GC还会老年代GC的。如果有"full",说明这次GC是发生了Stop-The-World的。我理解新生代和老年代GC都会发生Stop-The-World,只是老年代的STW的时间相对更长 GC说明垃圾回收发生在新生代,Full GC说明垃圾回收发生在新生代和老年代
[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前整个堆的占用量-> GC后整个堆的占用量(堆的总大小)"
再往后,"0.0025925secs"表示该内存区域GC所占用的时间,单位是秒。
参考
gc日志解析:https://plumbr.io/handbook/garbage-collection-algorithms-implementations
内存分配和回收策略
对象优先在eden分配
public class TestAllocation {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
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发生的原因是给allocation4分配内存时,发现Eden已经被占用了6MB,剩余空间已不足以分配案例location4所需的4MB的内存,因此发生minor FC。GC期间虚拟机又发现已有3个2MB大小的对象全部无法放入到Survivor空间(Survivor只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。
新生代GC(Minor GC): 指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC /Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC,Major GC的速度一般比Minor GC慢10倍以上。
大对象直接进入老年代
-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制(新生代使用复制算法收集内存)
public class TestAllocation1 {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation;
allocation = new byte[4* _1MB];
}
}
运行结果:
老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB,因此超过3MB的对象都会直接在老年代进行分配。(PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数)
长期存活对象进入老年代
public class TestTenuringThreshold {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[ _1MB/16];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB]; // 第一次Minor GC
allocation3 = null;
allocation3 = new byte[4 * _1MB]; // 第二次Minor GC
}
}
~
当MaxTenuringThreshold=1时,allocation3=new byte[4*_1MB] 第一次赋值时,因为eden区总空间为8192K,剩余空间不足4096K,所以会触发一次minor GC。触发GC后allocation1进入from区域,年龄为1,allocation2空间的大于suvivor的空间1MB,所以直接分配到老年代,allocation3分配到eden区占用51%的eden区空间。
allocation3=new byte[4*_1MB]第二次赋值时,eden区总空间为8192K,空间被占用51% eden区空间不足,所以触发第二次Minor GC。第二次的Minor GC触发后,由于MaxTenuringThreshold=1所以age=1的allocation1被提升到老年代,surivior的from区空间被清空,allocation3对象由于没有被GC root引用,所有allocation3原来的空间被回收,再次被分配给新的申明的allocation3对象。
MaxTenuringThreshold=15时,流程大部分与上面相同,不同的地方在于age=1的allocation1经过第二次GC后还是停留在surivior的from区,不进入tenured generation老年代。
动态对象年龄的判定
public class TestTenuringThreshold2 {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3,allocation4;
allocation1 = new byte[ _1MB/4];
allocation4 = new byte[ _1MB/4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];// 出现一次Minor GC
allocation3 = null;
allocation3 = new byte[4 * _1MB]; // 出现一次Minor GC
}
}
如果在Surivor空间的相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
上面allocation1与allocation4的对象加起来超过512KB并且是同年的,满足同年对象达到Survivor空间的一半规则。
空间担保
JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full GC。如果HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。
参考:
https://blogs.oracle.com/jonthecollector/our-collectors