JVM优化之垃圾收集器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
没有最好的垃圾收集器,只有根据具体应用场景选择适合自己的垃圾收集器。
Serial收集器
#使用方法
-XX:+UseSerialGC -XX:+UseSerialOldGC
Serial(串行)是最基本、历史最悠久的单线程收集器。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
Serial收集器优于其他垃圾收集器的地方在于它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:
- 在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用
- 作为CMS收集器的后备方案
新生代采用复制算法,老年代采用标记-整理算法。
Parallel Scavenge收集器
JDK8默认的新生代和老年代收集器.
#使用方法
-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。
默认的收集线程数跟cpu核数相同,可以用参数指定收集线程数,但是不推荐修改。
-XX:ParallelGCThreads
Parallel Scavenge收集器关注吞吐量(高效率的利用CPU)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
新生代采用复制算法,老年代采用标记-整理算法。
ParNew收集器
#使用方法
-XX:+UseParNewGC
ParNew收集器和Parallel收集器类似,区别主要在于它可以和CMS收集器配合使用。新生代采用复制算法。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器配合工作。
CMS收集器
#使用方法
-XX:+UseConcMarkSweepGC
CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器。是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。它非常适合在注重用户体验的应用上面使用。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为五个步骤:
- 初始标记
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
- 并发标记
该阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程。过程耗时,用户线程可以和垃圾收集线程一起并发运行,不会造成用户线程停顿,故而,可能导致已经标记过的对象状态发生改变。
- 重新标记
修正并发标记期间,用户线程不停顿导致的标记发生变动那一部分的标记记录(主要是处理漏标记问题),这阶段停顿时间一般比初始标记阶段要长,但是比并发标记时间短。主要用到三色标记里面的增量更新算法做重新标记。
- 并发清理
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色(见三色标记算法详解)不做任何处理。
- 并发重置
重置本次GC过程中的标记数据。
优缺点
优点:
- 并发收集
- 低停顿
缺点:
- 对CPU资源敏感
会和服务抢资源
- 无法处理浮动垃圾
在并发标记和并发清理阶段又产生垃圾,这种垃圾只能等下一次GC被清理
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生
通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
- 会存在多次触发垃圾回收的情况,即上次垃圾回收还没有执行完,然后又被触发垃圾回收。
在并发标记和并发清理阶段,一边回收,一边系统运行,此时回收还没有结束,就再次触发FULL GC,也就是"concurrent mode failure",此时会进入stop theworld,用serial old垃圾收集器来回收。
CMS的相关核心参数
- 启用cms
-XX:+UseConcMarkSweepGC
- 并发的GC线程数
-XX:ConcGCThreads
- FullGC之后做压缩整理(减少碎片)
-XX:+UseCMSCompactAtFullCollection
- 多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-XX:CMSFullGCsBeforeCompaction
- 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-XX:CMSInitiatingOccupancyFraction
- 指定用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction参数的值),如果不指定,JVM仅在第一次使用设定值,后续则会根据运行时采集的数据做自动调整,如果指定了该参数,那么每次JVM都会在到达规定设定值时才进行GC。不过大多数情况下,JVM都能够作出更好的垃圾收集决策,所以如果不是很有信心的话,不建议使用该参数,放心的把决定权交给JVM
-XX:+UseCMSInitiatingOccupancyOnly
- 在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时80%都在标记阶段
-XX:+CMSScavengeBeforeRemark
- 表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallellnitialMarkEnabled
- 在重新标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled
G1收集器
#使用方法
-XX:+UseG1GC
G1(Garbage-First)是面向配置多处理器以及大内存的服务器的垃圾收集器。满足GC低停顿时间要求的同时,还具备高吞吐量性能特征。
G1将堆内存划分为多个大小相同的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048(例如堆大小为2048M,则一个Region的大小为1M;可以通过参数"-XX:G1HeapRegionSize"手动指定Region大小,推荐默认的计算方式)。
G1保留的年轻代和老年代的概念,但不在进行物理隔离,它们都是(可以不连续)Region的集合。Humongous区专门存放短期巨型对象。
年轻代默认占用堆内存的5%(例如,堆内存为4G,则年轻代大小为200M左右,对应大约100个Region区域),可以通过参数“-XX:G1NewSizePercent”设置新生代初始占比。在系统运行时,JVM会动态调整年轻代的内存占比(最多60%,可以通过参数“-XX:G1MaxNewSizePercent”调整)。年轻代内,Eden和Survivor的默认占比为8:1:1(假设年轻代有100个Region区域,则Eden区有80个,s0区有10个,s1区有10个)。
Region区域的功能是动态变化的,经过垃圾回收后,年轻代可能变成老年代。
G1垃圾收集器除了对大对象的处理不同外,对于其他对象跨代转移和别的垃圾收集器一样。G1内专门分配大对象的Region区域叫Humongous区,不会让大对象直接进去老年代的Region内,以节约老年代空间,避免因老年代空间不够的GC开销。对大对象的判定是这个对象超过了一个Region大小的50%(例如一个Region大小为2M,若一个对象大小超过1M,则被之间放入Humongous区域),若一个对象大小超过了Region区域大小,则会横跨多个Region区域进行存放。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
G1收集器一次GC的运作过程大致分为以下几个步骤:
-
初始标记(initial mark,STW)
暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快。
-
并发标记(Concurrent Marking)
同CMS的并发标记
-
最终标记(Remark,STW)
同CMS的并发标记
-
筛选回收(Cleanup,STW)
对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间(通过参数“-XX:MaxGCPauseMillis”指定)来指定回收计划。例如:此时老年代,有100Region区域满了,根据预期停顿时间配置,本次只允许停顿200毫秒,结合之前回收成本的计算得知,回收其中的80个Region区域刚好需要200毫秒,那么此次GC就只会回收80个Region,尽可能的把停顿时间控制在200毫秒。
新生代和老年代都采用复制的垃圾回收算法,将一个Region中的存活对象复制到另外一个Region区域内,这样在回收完毕后不会有太多的内存碎片,也不用像CMS那样回收完毕后因内存碎片过多还需要整理一次。
CMS在回收阶段是跟用户线程一起并发执行的,而G1在回收阶段因为内部实现太复杂暂时并没有实现并发回收。其实可以做到与用户线程并发运行的,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。到了Shenandoah就实现了并发收集,Shenandoah可以看做是G1的升级版本。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也是名字的由来)。例如:一个Region占用10M的空间,回收需要20毫秒,另外一个Region占用20M的空间,回收需要10毫秒,G1会优先选择后面这个Region进行回收。这种回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
G1垃圾被视为JDK1.7以上版本Java虚拟机的一个重要进化特征。它具备以下特点:
- 并行与并发
G1能充分利用CPU多核环境下的硬件优势,使用多个CPU来缩短Stop The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
- 空间整合
与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。
毫无疑问,可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。不过,这里设置的“期望值”必须是符合实际的,不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。它默认的停顿目标为两百毫秒,一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
G1垃圾收集分类
YoungGC
G1并不会在Eden区满了就去触发YoungGC,而是先计算当前Eden区回收时间。如果该回收时间远小于参数“-XX:MaxGCPauseMillis”,那么增加年轻代的Region区域,进去给新对象存放,不会马上做YoungGC,知道下一次Eden区被放满。如果回收时间接近参数“-XX:MaxGCPauseMillis”设定的值,那么就会触发YoungGC。
MixedGC
老年代堆占有率达到参数“-XX:InitiatingHeapOccupancyPercent”设定的值触发,回收所有的新生代和部分老年代(根据期望的GC停顿时间,确定老年代垃圾收集的优先顺序)以及大对象区。
正常G1垃圾收集器先做MixedGC,采用复制算法,把各个Region区内存活的对象拷贝到别的空闲Region区内,拷贝的过程中如果发现没有足够的Region区能够承载拷贝的对象,就会触发一次FullGC。
Full GC
停止系统程序,采用单线程进行标记、清理以及压缩整理,以便空闲出一批Region供下一次MixedGC使用。这个过程非常耗时。(Shenandoah优化成多线程收集)
G1收集器参数设置
- 使用G1收集器
-XX:+UseG1GC
- 指定GC工作的线程数量
-XX:ParallelGCThreads
- 指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:G1HeapRegionSize
- 用于设定垃圾收集过程中最大暂停时间的目标(默认200ms)。在使用 G1 垃圾收集器时尤为重要。
这个参数告诉垃圾收集器在每次垃圾回收暂停期间应尽量不超过设定的时间。垃圾收集器会尝试调整其行为和策略,以满足这个暂停时间目标。
- 该值降低时:
- 垃圾收集器会尝试缩短每次垃圾回收的暂停时间。
- 可能导致更频繁的垃圾收集操作,以满足更严格的暂停时间目标。
- 可能增加总体的 CPU 开销,因为垃圾收集需要更频繁地运行。
- 该值增加时:
- 垃圾收集器可以允许更长的暂停时间,减少垃圾收集的频率。
- 可能会导致较长的垃圾收集暂停时间,但总体上减少垃圾收集的次数。
- 适用于对暂停时间不敏感,但需要减少垃圾收集频率的应用场景。
-XX:MaxGCPauseMillis
- 新生代内存初始空间(默认整堆5%)
-XX:G1NewSizePercent
- 新生代内存最大空间
-XX:G1MaxNewSizePercent
- Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:TargetSurvivorRatio
- 最大年龄阈值(默认15)
-XX:MaxTenuringThreshold
- 老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC)。
比如堆默认有2048个region,如果有接近1000个的Region都是老年代,则可能就要触发MixedGC了
-XX:InitiatingHeapOccupancyPercent
- 指定Region内存活对象小于指定阈值(默认85%)时,才会回收此Region。如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCLiveThresholdPercent
- 指定一次回收过程中做几次筛选回收(默认8次)。
在筛选回收阶段,可以多次回收,以降低单次停顿时间。可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收。
-XX:G1MixedGCCountTarget
- 控制G1垃圾收集器在决定何时启动混合垃圾回收(MixedGC)时允许堆中浪费空间的百分比(默认5%)。
当堆中的垃圾(即无法再被程序使用的内存)达到设置的阈值时,G1垃圾收集器将尝试进行混合垃圾回收。这种回收不仅会收集年轻代(Young Generation)的垃圾,还会收集一些老年代(Old Generation)的垃圾,以减少整体堆的浪费。
- 该值降低时:
- 垃圾收集会更频繁地启动,从而减少堆中浪费的内存。
- 可能导致更频繁的垃圾收集暂停,影响应用程序的性能。
- 该值增加时:
- 减少垃圾收集的频率,从而减少垃圾收集暂停的次数。
- 可能导致更多的内存浪费,因为回收启动不够频繁。
-XX:G1HeapWastePercent