# 垃圾回收算法
# 发展
整个发展过程可分为这几个阶段Serial -> Parallel -> ParNew -> CMS -> G1 -> ZGC
# Serial
历史最悠久的垃圾回收器,最大的特点是使用单线程进行垃圾回收,回收过程中会STW(stop the world),新生代使用复制算法,老年代使用标记-整理算法。
# Parallel
Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和Serial收集器类似。默认的收集线程数跟cpu核数相同,也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。 新生代采用复制算法,老年代采用标记-整理算法。
吞吐量=CPU中用于运行用户代码的时间/CPU总消耗时间
参数 | 说明 |
---|---|
-XX:ParallelGCThreads | 设置并行收集器收集时使用的CPU数,默认与cpu核数相同 |
-XX:MaxGCPauseMillis | 设置并行收集最大暂停时间,单位是毫秒 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整 Java堆大小或者其他一些参数。该参数需谨慎使用 |
-XX:GCTimeRatio | 垃圾收集时间占总时间的比例(=1 / (N+1))。用于衡量吞吐量的大小。取值范围(0,100)。 默认值99,也就是垃圾回收时间不超过1%。 |
-XX:YoungGenerationSizeIncrement | 年轻代gc后扩容比例,默认是20(%) |
-XX:+UseAdaptiveSizePolicy | 设置Parallel Scavenge收集器具有自适应调节策略 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿之间的平衡点 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。 |
# ParNew
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。 新生代采用复制算法
# CMS
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 the world,用serial old垃圾收集器来回收
# CMS的相关核心参数
-XX:+UseConcMarkSweepGC:启用cms
-XX:ConcGCThreads:并发的GC线程数
-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,如果开启FullGC之后做压缩整理,代表每次FullGC后都会压缩一次
-XX:CMSInitiatingOcczupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW
X越多代表参数越不稳定,在当前版本可用,在将来可能被jvm废弃掉。简单举个栗子,比如java -version,没有带X,说明这个参数很稳定,在未来也一直支持这个参数
+代表开启参数,适用于值只有开启,关闭的参数
# G1垃圾回收器
- G1将Java堆划分为多个大小相等的独立区域(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有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。
- Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
- Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
# 垃圾回收过程
G1收集器一次GC(主要值Mixed GC)的运作过程大致分为以下几个步骤:
- 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
- 并发标记(Concurrent Marking):同CMS的并发标记
- 最终标记(Remark,STW):同CMS的重新标记
- 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了ZGC,Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
# G1垃圾收集分类
# Young GC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
# MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
# FullGC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
# G1收集器参数设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
# G1垃圾收集器优化建议
假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。
那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。
或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。
所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
# 什么场景适合使用G1
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间特别长,超过1秒
- 8GB以上的堆内存(建议值)
- 停顿时间是500ms以内
# ZGC
# ZGC目标
- 支持TB量级的堆。
- 最大GC停顿时间不超10ms。
- 奠定未来GC特性的基础。
- 最糟糕的情况下吞吐量会降低15%。
- 它最大的优点是:它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是10ms以下,几百G甚至上T堆的停顿时间也是10ms以下。
# ZGC内存布局
ZGC收集器是一款基于Region内存布局的, 暂时不设分代的, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整理算法的, 以低延迟为首要目标的一款垃圾收集器。
ZGC的Region分为大、 中、 小三类容量:
- 小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
- 中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
- 大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作, 用于复制对象的收集器阶段)的, 因为复制一个大对象的代价非常高昂。
# ZGC运作过程
ZGC的运作过程大致可划分为以下四个大的阶段:
- 并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新颜色指针(见下面详解)中的Marked 0、 Marked 1标志位。
- 并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
- 并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障(见下面详解))所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
ZGC的颜色指针因为“自愈”(Self-Healing)能力,所以只有第一次访问旧对象会变慢, 一旦重分配集中某个Region的存活对象都复制完毕后, 这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉, 因为可能还有访问在使用这个转发表。
- 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
# 不分代(暂时)
# NUMA-aware
每个CPU对应有一块内存,且这块内存在主板上离这个CPU是最近的,每个CPU优先访问这块内存
# 颜色指针
Colored Pointers,即颜色指针,ZGC的核心设计之一。以前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中。 每个对象有一个64位指针,这64位被分为:
- 18位:预留给以后使用;
- 1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;
- 1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合);
- 1位:Marked1标识;
- 1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;
- 42位:对象的地址(所以它可以支持2^42=4T内存);
为什么有2个mark标记?
- 每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
- GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
- GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。
- 通过对配置ZGC后对象指针分析我们可知,对象指针必须是64位,那么ZGC就无法支持32位操作系统,同样的也就无法支持压缩指针了(CompressedOops,压缩指针也是32位)。
颜色指针的三大优势:
- 一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。
- 颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。
- 颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
# 读屏障
之前的GC都是采用Write Barrier,这次ZGC采用了完全不同的方案读屏障,这个是ZGC一个非常重要的特性。 在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个Load Barriers。
# ZGC存在的问题
ZGC最大的问题是浮动垃圾。ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。
# 解决方案
目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。
# ZGC参数设置
启用ZGC比较简单,设置JVM参数即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」。调优也并不难,因为ZGC调优参数并不多,远不像CMS那么复杂。它和G1一样,可以调优的参数都比较少,大部分工作JVM能很好的自动完成。下图所示是ZGC可以调优的参数:
# ZGC触发时机
ZGC目前有4种机制触发GC:
- 定时触发,默认为不使用,可通过ZCollectionInterval参数配置。
- 预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用。
- 分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。
- 主动触发,(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。
# 垃圾回收算法比较
算法 | 原理 | 优点 | 缺点 |
---|---|---|---|
标记复制 | 将一块内存分为两份,一份为存活对象区,一份为空闲区。 每次垃圾回收,将存活对象复制到空闲区,然后空闲区变为存活区,空闲区的对象全部清除 | 简单 | 1. 有一半的空间被浪费 |
标记清除 | 每次垃圾回收,标记存活对象,将可回收的对象直接清除 | 简单 | 1. 效率问题,如果标记的对象太多,效率不高 2. 空间浪费,会有内存碎片 |
标记整理 | 与标记清除类似,但不同的是,会把存活的对象向一端移动,然后直接清理掉边界以外的内存 | 清理完成之后,空间规整,避免了内存碎片 | 1. 由于需要挪动对象,存在效率问题 2. 由于需要将对象往一端移动,存在线程问题 |
# 垃圾收集器比较
前言:没有一个绝对好的垃圾收集器,需要根据实际的应用场景选择最适合自己的垃圾收集器
垃圾回收器 | 特点 使用方式 | 流程 | 年轻代 | 老年代 |
---|---|---|---|---|
Serial | 单线程 -XX:+UseSerialGC | GC(STW),应用线程暂停 | 复制 | - |
Serial Old | 单线程 XX:+UseSerialOldGC | GC(STW),应用线程暂停 | - | 标记整理 |
Parallel Scavenge | 并行 -XX:+UseParallelGC | GC(STW),多线程并发,应用线程暂停 | 复制 | - |
Parallel Old | 并行 -XX:+UseParallelOldGC | GC(STW),多线程并发,应用线程暂停 | - | 标记整理 |
ParNew | 1. 与Parallel类似 2. 区别主要在于它可以和CMS收集器配合使用 3. 吞吐量高,所有线程集中处理垃圾回收 4. 只能用在年轻代 -XX:+UseParNewGC | GC(STW),多线程并发,应用线程暂停 | 复制 | 老年代使用CMS |
CMS | 1. 比较主流的垃圾回收器,实现了GC线程和用户线程同时运行,感知上STW时间短,用户体验好 2. 缺点是有可能产生浮动垃圾,因为在并发清理阶段产生的垃圾,只有在下一次垃圾回收的时候才能会回收掉 3.使用标记清除算法, 会导致在收集结束时产生大量的空间碎片,可以通过参数-XX:+UseCMSCompactAtFullCollection让jvm在执行完标记清除后再做整理,内存整理阶段会STW -XX:+UseConcMarkSweepGC | 初始标记(STW) ->并发标记(占据80%的时间) -> 重新标记(STW) -> 并发清理 -> 并发重置 如果并发失败(上一次垃圾回收没有执行完成,下一次垃圾回收又被触发,会导致并发失败),整个GC过程STW,用serial old垃圾收集器来回收,等垃圾回收结束之后再重新运行用户线程 | 无年轻代,年轻代使用ParNew | 标记清除 |
G1 | 1. 也分老年代,年轻代(8(E):1(S0):1(S1)), 新增了Humongous区域,用于存放大对象。 但只是逻辑划分,在运行时期,每个格子都可能是年轻代,老年代或者Humongous区域。 2. 默认情况下,会把堆内存分为2048个region,如果是4GB的内存,那么每个region的大小就是2M。 3.可用通过参数-XX:MaxGCPauseMillis(默认200ms)来控制每次GC的最大停顿时间(一般不建议修改),参数的实现是在筛选回收阶段,通过计算最大停顿时间和预估的垃圾回收时间占比,来计算回收多少百分比的垃圾(需要被回收的垃圾称为collection set),剩余垃圾等到下一次垃圾垃圾回收再做处理。 4. 使用复制算法,不会产生太多内存碎片,内存空间比较规整。会优先回收“回收比”高的区块,既存活对象较少的区块,复制的效率更高。 -XX:+UseG1GC | 初始标记(STW) ->并发标记 -> 最终标记(STW) -> 筛选回收(STW) | 复制 | 复制 |
ZGC | 1. 不分代(暂时) |
# 默认垃圾回收器
jdk1.7:Parallel Scavenge(新生代)+ Parallel Old(老年代)
jdk1.8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
jdk1.9:G1
# 主流垃圾回收器搭配
1.8(一般需要机器内存大于4GB,经验值): ParNew + CMS
# 每秒几十万并发的系统如何优化JVM
Kafka类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。
G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。
# 三色标记
三色指的是扫描过程,扫描结束后,只存在黑色和白色。
- 黑色:所有的引用都被扫描过,非垃圾对象
- 灰色:至少存在一个引用没有被扫描到,非垃圾对象
- 白色:不存在任何引用的对象,垃圾对象
# 多标
多标指的是,垃圾对象被标记为非垃圾对象。这对实际的代码运行不会造成太大的影响,可作为浮动垃圾,在下一次垃圾回收进行清理。
# 漏标
漏标指的是,非垃圾对象没有被正常标记为黑色,如果存在这个问题,那么就是严重的bug,在垃圾回收阶段将正常的对象清除导致应用程序崩溃。
下面是关于漏标问题的两种处理方式
方法 | 说明 | 优缺点 | 使用该方法的垃圾回收算法 |
---|---|---|---|
原始快照(SATB) | 若引用被移除,将被移除对象标记为黑色对象,当做浮动垃圾在本次垃圾回收不做处理,等到下一次垃圾回收再做清理。 | 效率高,但会存在较多垃圾对象,占用内存空间。 | G1 |
增量更新 | 若引用被移除,将被移除对象添加到列表中。若存在新增引用,将新增引用的变量标记灰色对象,等待重新标记阶段,在维护的列表中重新扫描引用,标记垃圾对象。 | CMS |
# 回答问题
漏标错标问题,为什么G1使用SATB(原始快照)而CMS使用增量更新?
- SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。
- 定位问题,G1在大内存的时候去使用,用SATB的话,可以理解为空间换时间,效率更高。而G1的最大停顿时间,在算法设计之初,本身就会有浮动垃圾,所以多一点,其实没有关系。
# 垃圾回收优化策略
- 如果大对象比较多,可以将对象进入老年代的年龄(默认的15岁)改小一点,比如改为5
- 一般需要配置开启压缩整理(+UseCMSCompactAtFullCollection)参数,然后需要再配置一个压缩周期(CMSInitiatingOccupancyFraction),如果FullGC频繁发生,一般是3~5,三到五次FullGC之后进行整理。如果是抢购应用场景,偶尔出现系统压力很大的情况,也可以将压缩周期配置为0/1,每次FullGC后进行内存整理
# 安全点与安全区域
# 安全点
安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。 这些特定的安全点位置主要有以下几种:
- 方法返回之前
- 调用某个方法之后
- 抛出异常的位置
- 循环的末尾 大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和安全点是重合的。
# 安全区域
Safe Point 是对正在执行的线程设定的。 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。 因此 JVM 引入了 Safe Region。 Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。