垃圾收集算法
分代回收理论
目前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块,一般java堆分为新生代、老年代,这样我们可以根据不同年代的特点,选中合适的垃圾收集算法。
比如新生代,在垃圾收集中,大部分对象(99%)都将死亡,我们可以选择复制算法
老年代,在垃圾收集中,大部分对象还在存活,我们可以选择标记整理、标记清除算法
注意:标记复制/标记清理算法,会比复制算法慢10倍以上
复制算法
为了解决效率问题复制算法就出现了,它将内存分为大小相同的两份。每次只使用其中的一份,垃圾收集时,将存活的对象复制到另一块儿内存,然后吧使用的空间一次性清掉。
清理前:
清理后:
标记清理算法
分为:标记、清理2个阶段。标记存活的对象,统一清理未被标记的对象(一般选中这种),也可以反过来,标记死亡的对象,统一清理标记的对象。
缺点:
- 效率问题(如果标记的对象很多,效率不高)
- 空间问题(会产生大量不连续的内存碎片)
清理前:
清理后:
标记整理算法
根据老年代的特点,特出的一种算法,分为:标记、整理2个阶段。标记过程中和标记清理算法一致,整理时,将存活对象向一端移动,然后清理掉边界外的内存。
清理前:
清理后:
垃圾收集器
垃圾收集器分为:Serial、ParNew、Parallel、CMS、G1、ZGC
Serial收集器
串行收集器
新生代采用复制算法,老年代采用标记整理算法。
常用参数:
参数 | 说明 |
---|---|
-XX:+UseSerialGC | 新生代使用Serial收集器 |
-XX:+UseSerialOldGC | 老年代使用Serial收集器 |
Parallel Scavenge收集器
Parallel是Serial收集器的多线程版本。默认收集线程受和CPU合数一致。
新生代采用复制算法,老年代采用标记-整理算法。
常用参数:
参数 | 说明 |
---|---|
-XX:+UseParallelGC | 新生代使用Parallel收集器 |
-XX:+UseParallelOldGC | 老年代使用Parallel收集器 |
-XX:ParallelGCThreads | 指定收集线程数,一般不建议修改 |
ParNew收集器
ParNew收集器和Parallel收集器类似,唯一的区别是,ParNew收集器可以配合CMS收集器使用。
新生代采用复制算法,老年代采用标记-整理算法。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短停顿时间为目标的垃圾收集器。它是HotSpot虚拟机第一款真正意义上的并发收集器,首次实现了让垃圾收集器与用户线程(几乎)同时工作。
老年代采用:标记 - 清除算法。
运作过程:
- 初始标记:暂停所用的其他线程(STW),并记录下 gc roots直接能引用的对象,速度很快
- 并发标记:从gc roots直接关联的对象开始遍历整个对象图的过程,这个过程比较耗时,但不需要停顿用户线程。可能会导致已经标记过的对象状态发生变化。
- 重新标记:是为了修正并发标记过程中,用户线程运行导致的对象标记状态发生变化的对象。这个阶段停顿时间比初始标记暂停的时间稍长,但远比并发标记阶段时间短,主要用到三色标记里的增量更新算法做重新标记。
- 并发清理:开启用户线下,同时GC线程对未标记的区域清理。这个阶段如果有新对象产生,将会标记会黑色,不做任何处理。
- 并发重置:重置本次GC过程中的标记数据。
优点:并发、低停顿
缺点:
- CPU敏感(会和业务线程抢资源)
- 无法处理浮动垃圾(GC过程中可能会场所新的垃圾,只能等待下一次GC清理)
- 会导致大量的内存碎片(算法使用的是:标记 - 清除算法),可以通过设置:-XX:+UseCMSCompactAtFullCollection参数,标记清除后整理内存
- 执行过程中的不确定性。会存在上一次垃圾回收还没执行完,然后又触发垃圾回收的情况。特别是在并发标记和并发清理阶段出现,一边回收,系统一边运行,可能没回收完,再次触发Full GC,此时会进入Stop The World,用serial old垃圾收集器来回收。
常用参数:
参数 | 说明 |
---|---|
-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%都在标记阶段 |
-XX:+CMSParallellnitialMarkEnabled | 表示在初始标记的时候多线程执行,缩短STW |
-XX:+CMSParallelRemarkEnabled | 在重新标记的时候多线程执行,缩短STW |
G1收集器(-XX:+UseG1GC)
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,那么Region大小为2M,当然也可以用参数-XX-G1HeapRegionSize
手动设置Region大小。
G1保留了年轻代和老年代的概念,但不再物理分割,它们都是可以不连续的Region集合。
默认年轻代对堆内存的占比是5%,例:堆大小为4096M,那么年轻代占据大概200M左右内存。可以通过-XX:G1NewSizePercent
设置新生代初始占比。在运行过程中,JVM会不停的给年轻代增加内存占比,但最多不超过堆内存的60%,可以通过-XXG1MaxNewSizePercent
调整。年轻代中Eden和Survivor对应的Region也和之前一样,默认比例:8:1:1。
一个Region可能之前是年轻代,回收之后变成老年代,也就是说Region区域的功能可能会发生变化。
G1垃圾收集器的对象转移原则和之前一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,大对象的判定规则是:对象大小是否超过了Region大小的50%,如果一个对象太大,可能会横跨多个Region区域来存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代空间,避免老年代空间不够而引起的GC开销。
Full GC除了收集年轻代和老年代之外,Humongous区也会一并回收。
G1收集器一次GC的运作过程大致分为以下几个步骤:
- 初始标记:暂停所用的其他线程,并记录下Gc Roots直接能引用的对象,速度很快
- 并发标记:和CMS收集器的并发标记一样
- 最终标记:和CMS收集器的重新标记一样
- 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(-XX:MaxGCPauseMillis)来制定回收计划(用最少的时间收集最多的垃圾),
回收算法使用的是复制算法,将一个region中存活的对象复制到另一个region中,几乎不会有太多的内存碎片
。
G1垃圾收集分类
YoungGC
YoungGC并不是说现有的Eden区放满了就好马上触发,G1会计算现在Eden区回收大概需要的时间,如果回收时间远小于参数(-XX:MaxGCPauseMills)设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,知道下一次Eden区放满,G1计算回收时间接近参数(-XX:MaxGCPauseMills)设定的值,那么就好触发Young GC。
MixedGC
老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望GC停顿时间确定Old区垃圾收集的优先顺序)以及大对象区,正常情况G1垃圾收集器是先做MixedGC,使用复制算法,将region中存活的对象复制到别的region中去,复制过程中发现没有足够的空region,就会触发Full GC
Full GC
停止用户线程,采用单线程进行标记、清理、压缩整理,空闲出来的region供下一次MixedGC使用,Full GC非常耗时(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%,此时就会立即停止混合回收,意味着本次混合回收就结束了。 |
ZGC收集器(-XX:+UseZGC)
ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC可以说源自Azul System公司开发的C4(Concurrent Continuously Compacting Collector)收集器。ZGC的目标主要有4个:
- 支持TB量级的堆
- 最大GC停顿时间不超10ms
- 奠定未来GC特性的基础
- 最糟糕的情况吞吐量会降低15%
内存布局
ZGC收集器基于Region内存布局,暂时不分代,使用了读屏障、颜色指针等技术来实现可并发的标记-整理算法,以低延迟为首要目标。
ZGC的Region可以具有大、中、小三类容量:
- 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region):容量固定为32MB,用于放置 >= 256KB && < 4MB的对象。
- 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置 >= 4BM的大对象。每个大型Region中只会存放一个大对象。大型Region在ZGC的实现中是不会被重新分配,因为复制一个大对象的代价非常高昂。
颜色指针
以前垃圾收集器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中。
每个对象有一个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周期开始,会交换使用标记位(第一次使用Marked0,第二次使用Marked1,依次循环)。
颜色指针优势
- 某个Region的存活对象被移走后,这个Region立即就能够释放和重用,而不必等待整个对重所有指向该Region的引用都被修正后才能清理,理论上只要有一个空闲的Region,ZGC就能完成收集。
- 颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用,ZGC只使用了读屏障。
- 颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提升性能。
读屏障
之前的GC都是采用Write Barrier,ZGC采用了完全不同的方案:读屏障。假如我们尝试读取堆中的一个对象引用并赋值给另一个对象,如果这时候对象在GC时被移动了,接下来JVM就会加上一个读屏障,这个读屏障会把读出的指针更新到对象的新地址上,并把堆里的之歌指针“修正”到元吧的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用程序持有的就是更新后的有效指针,而且不需要STW。
运作过程
并发标记(Concurrent Mark)
:与G1一样,并发标记是遍历对象图做可达性分析的阶段,他的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新颜色指针中的Marked0、Marked1标志位。并发预备重分配(Concurrent Prepare for Relocate)
:这个阶段需要根据特定的查询条件统计得出本次收集过程需要清理哪些Region,将那些Region祖册重分配集(Relocation Set)。ZGC每次回收都会扫码所有的Region。用范围更大的扫码成本换取省去G1中记忆集的维护成本。并发重分配(Concurrent Relocate)
:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table)
,记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集中,如果用户线程次数并发记录了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,时期直接指向新对象,ZGC将这种行为称为指针的“自愈(Self-Healing)”能力。并发重映射(Concurrent Remap)
:重映射所做的就是修正整个对重指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所有这个重映射操作并不是很迫切。ZGC巧妙的将并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段中去完成,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正后,原来记录新旧对象关系的转发表就可以释放掉了。
ZGC存在的问题
最大的问题是浮动垃圾,ZGC的听到时间是在10ms以下,但ZGC的执行时间还是员大于这个时间的。假如ZGC全过程执行需要10分钟,在这期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC时进行回收,这些等待下次回收的对象就是浮动垃圾。
解决方案
目前唯一的办法时增大对容量,使程序有更多的喘息时间。
ZGC参数设置
参数 | 示例 | 说明 |
---|---|---|
-XX:+UseZGC | 启用ZGC | |
-XX:ZAllocationSpikeTolerance | -XX:ZAllocationSpikeTolerance=5 | 不设置定时gc,zgc根据自己算法进行gc 系数为5 |
-XX:ZCollectionInterval | ZGC 发生的最小时间间隔,单位秒 | |
-XXX:ZFragmentationLimit | 25 | 控制 ZGC 的对象的碎片化程度,ZFragmentationLimit 越低,ZGC 回收越彻底 |
-XX:ZMarkStackSpacelimit | 调节 ZGC 标记栈空间大小 | |
-XX:ZProactive | true | 是否启用主动回收策略 |
-XX:-ZUncommit | 归还未使用内存空间 | |
-XX:ZUncommitDelay | 不再使用的内存最多延迟多长时间才会被归还给操作系统 |
ZGC诊断参数
参数 | 说明 |
---|---|
-XX:+UnlockDiagnosticVMOptions | 解锁诊断参数 |
-XX:ZStatisticsInterval | 打印ZStat统计数据(cpu、内存等log)的间隔 |
-XX:ZVerifyForwarding | 检验转发表 |
-XX:ZVerifyMarking | 检验标记集 |
-XX:ZVerifyObjects | 检验对象 |
-XX:ZVerifyRoots | 检验根节点 |
-XX:ZVerifyViews | 检验堆视图访问 |
ZGC触发时机
定时触发
:默认为不使用,可通过ZCollectionInterval参数配置。预热触发
:最多三次,在堆内存达到10%、20%、30%时触发,主要统计GC时间,为其他GC机制使用。分配速率
:基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 一次GC最大持续时间 一次GC检测周期时间)。主动触发
:(默认开启,可通过ZProactive参数配置)
1)距上次GC完成堆内存增长10%
2) 距上次GC完成超过5分钟,标记为:TIMEelapsed
如果这两个条件同时满足,预测垃圾回收时间为TIMEgc,定义规则:如果NUMgc * TIMEgc < TIMEelapsed,则触发垃圾回收。其中NUMgc是ZGC设计的常量,假设应用程序的吞吐率从50%下降到1%,需要触发一次垃圾回收。
GC 常用配置
参数 | 含义 | 默认值 | 说明 |
---|---|---|---|
-Xms | 初始堆大小 | 物理内存的1/64(<1GB) | 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. |
-Xmx | 最大堆大小 | 物理内存的1/4(<1GB) | 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 |
-Xmn | 年轻代大小 | 注意:此处的大小是(eden+ 2 survivor space)与jmap -heap中显示的New gen是不同的。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 | |
‐Xss | 每个线程的堆栈大小 | linux 64位服务器默认为1M | 一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。 |
‐XX:MetaspaceSize | 元空间大小 | 20M左右 | 元空间并不在虚拟机中,而是使用本机内存。因此,元空间大小仅受本地内存限制 |
‐XX:MaxMetaspaceSize | 最大元空间大小 | 默认不受限制 | |
‐XX:SurvivorRatio | Eden区与Survivor区的大小比值 | 8 | 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 |
‐XX:MaxTenuringThreshold | 提升年老代的最大临界值 | 15 | |
‐XX:PretenureSizeThreshold | 对象超过多大是直接在老年代分配 | ||
‐XX:+UseParNewGC | 使用ParNewGC | ||
‐XX:+UseConcMarkSweepGC | 使用CMS GC | ||
‐XX:CMSInitiatingOccupancyFraction | CMS垃圾收集器,老年代内存使用比例达到多少百分比时开始垃圾回收 | ||
‐XX:+UseCMSCompactAtFullCollection | 在FULL GC的时候,对年老代的压缩 | CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片 | |
‐XX:CMSFullGCsBeforeCompaction | CMS Full GC多少次后进行内存压缩 | 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生”碎片”,使得运行效率降低,此值设置运行多少次GC以后对内存空间进行压缩、整理 |
辅助信息
参数 | 含义 | 默认值 | 说明 |
---|---|---|---|
-XX:+PrintGC | 打印GC | 输出形式:[Full GC (Metadata GC Threshold) 8776K->5951K(62464K), 0.0630269 secs] | |
-XX:+PrintGCDetails | 打印GC详情 | ||
-XX:+PrintGCTimeStamps | 打印GC时间 | ||
-XX:+PrintGC:PrintGCTimeStamps | 等同于:-XX:+PrintGC -XX:+PrintGCTimeStamps | ||
-XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期间程序暂停的时间.可与上面混合使用 | ||
-XX:+PrintGCApplicationConcurrentTime | 打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 | ||
-XX:+PrintHeapAtGC | 打印GC前后的详细堆栈信息 | ||
-Xloggc:filename | 把相关日志信息记录到文件以便分析,与上面几个配合使用 | ||
-XX:+PrintTLAB | 查看TLAB空间的使用情况 | ||
-XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的阈值 | Desired survivor size 6291456 bytes, new threshold 5 (max 15),new threshold 5即标识新的存活周期的阈值为5 | |
-XX:+HeapDumpOnOutOfMemoryError | 当JVM发生OOM时,自动生成DUMP文件 | ||
XX:HeapDumpPath | DUMP文件路径,注意:路径需要存在 | ||
-XX:+HeapDumpAfterFullGC | 运行Full GC后生成DUMP文件 | ||
-XX:+HeapDumpBeforeFullGC | 运行Full GC前生成DUMP文件 |