垃圾回收器
Serial
新生代垃圾回收器,单线程,会产生STW(Stop The World),采用拷贝算法。
它在停止线程时,并不是直接将线程强行停止,而是等线程运行到一个安全点(Safe Point)的时候才停止。
JDK刚诞生时的垃圾回收器,那个时候内存都比较小,所以停顿时间也不长。但如果现在的机器再用这个垃圾回收器,停顿时间就不可接受了,所以这个目前来说不怎么用了。
Serial Old
配合Serial回收器使用,用在老年代,特点同上。采用标记清除或者标记压缩算法。也基本不用了。
Parallel Scavenge
很多JDK版本的默认垃圾回收器,作用于新生代,采用拷贝算法。也会STW。只是STW之后,通过多线程来清理垃圾。
Parallel Old
一般和Parallel Scavenge配套使用,作用与老年代,采用标记压缩算法。
ParNew
在Parallel Scavenge基础上的新垃圾回收器,作用于新生代,大部分特点和Parallel Scavenge差不多。只是针对和CMS的配合他做了增强。所以他更适合配合CMS使用。在CMS运行到某个特定阶段时,它会同时运行。
PS 和 PN区别的延伸阅读
CMS
JDK1.4开始引入,作用于老年代,采用concurrent mark sweep算法,头一个支持垃圾回收线程和工作线程可以同时进行的垃圾回收器。
由于CMS先天存在很多问题,所以现在没有一个版本默认是CMS,只能手工指定。
阶段
- 初始标记。
- 单线程。STW。
- 主要是收集GC roots直接引用的对象并标记这些对象为存活对象,这些对象可能在新生代或者老年代,即需要对整个堆进行扫描,不过由于直接引用的对象较少,故不需要消耗很长的时间;
- 并发标记。
- 与应用程序并发执行,从初始标记阶段所收集到的GC roots直接引用的对象出发,继续扫描查找和标记可达的对象,这个过程涉及到整个堆,即包括新生代和老年代,故需要耗费较长时间,所以与应用程序并发执行,不会对应用程序造成干扰
- 并发预清理
- 有的文档中会有这个过程,有的没有。
- 并发预清理主要是将新生代中在并发标记阶段中晋升到老年代的对象进行清理,减少新生代中对象的数量,减少下一阶段重新标记需要扫描的对象数量。并发预清理阶段默认执行时间不能超过5s,否则直接进入重新标记阶段,默认值5s可以通过JVM参数:-XX:CMSMaxAbortablePrecleanTime 来调整。除此之外,还可以通过JVM参数:-XX:+CMSScavengeBeforeRemark 来开启在进行重新标记Remark之前,强制对新生代执行一次MinorGC,从而减少重新标记阶段需要扫描的对象数量,减少Remark的执行时间;
- 重新标记。
- 多线程。STW。由于第二步运行了以后会产生新的垃圾,同时之前被标记为垃圾的也可能不在是垃圾。
- 由于并发标记阶段,应用程序在并发执行,会产生新的垃圾,同时之前被标记为垃圾的也可能不在是垃圾。故在该阶段暂停应用,然后对在并发标记阶段中状态发生了变化的对象进行重新标记,此过程涉及到整个堆中,即从GC roots引用的对象出发,对新生代和老年代中发生了变化的对象进行重新标记。具体remark阶段的执行时间,可以通过GC日志的CMS-remark日志内存来查看
- 并发清除。
- 将之前标记为垃圾的对象清理掉。
- 并发重置
- 此步骤也在部分文档中出现。
- 对该次CMS垃圾回收中的数据结构进行重置,以便下次进行CMS
标记算法
CMS标记使用的算法是:三色标记算法+Incremental Update
优缺点
- CMS垃圾回收的特点是应用程序可以并发执行,所以应用停顿时间较短,实现了高响应时间的目的。
- 应用线程和垃圾回收线程同时执行,需要消耗较多的CPU资源外
- 由于是基于标记清除算法,故会造成内存碎片。
- 为了解决内存碎片问题,CMS提供了JVM参数:-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=1,来配置在进行了Full GC时,对老年代进行压缩整理,处理掉内存碎片,其中CMSFullGCsBeforeCompaction配置进行了多少次Full GC之后执行一次内存压缩。
- 与Parallel一样,也可以通过:-XX:ParallelGCThreads来配置并行垃圾回收线程数。
- Concurrent Mode Failure
- 由于CMS在执行过程中是与应用程序并发执行的,如果在此过程中,应用程序需要在老年代分配空间来存放对象,而老年代此时没有足够的空闲空间,此时会触发Concurrent Mode Failure,之后会进行一次Full GC,老年代降级为使用Serial Old垃圾回收器,此时会暂停所有的应用线程的执行。
G1
介绍
官方文档
G1在JDK1.7出现,1.8成熟,1.9成为默认垃圾回收器。英语:Garbage First Garbage Collector (G1 GC)
G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
G1还有一个及其重要的特性:软实时(soft real-time)。所谓的实时垃圾回收,是指在要求的时间内完成垃圾回收。“软实时”则是指,用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上的垃圾回收时间都在这个时限内。
特点
- 并发收集,标记算法:三色标记
- 压缩空闲空间不会延长GC的暂停时间
- 更易预测的GC暂停时间;
- 适用不需要实现很高的吞吐量的场景
内存模型
分区(Region)
上图摘自官网,可以看到G1和之前垃圾回收器的分代模型完全不一样!
G1将Java堆划分为多个大小相等的独立区域(Region)。JVM目标是不超过2048个Region,实际可以超过该值,但是不推荐。每个分区的大小从1M到32M不等,但是都是2的冥次方。一般Region大小约等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。
每个分区可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处。有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。
新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收。整个新生代中的对象要么被回收、要么晋升。至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。
默认年轻代对堆内存的占比是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个。
巨形对象(Humongous Object)
一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。
卡片Card
由于老年代对象也可能指向新生代,所以做YGC时,需要扫描整个OLD区,效率非常低。因此设计了CardTable
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。结构上,Card Table用BitMap来实现
如果一个OLD区的Card中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描Dirty Card
CSet
CSet = Collection Set,用于记录堆中可被回收的分区的集合。等到回阶段时,会从CSet中查找可回收的分区。
在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代CSet会占用不到整个堆空间的1%大小。
RSet
RememberedSet,记录了其他Region中的对象到本Region的引用。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。
缺点:每个分区存储这种指针,大概要占10%的空间,所以到了ZGC中取消了这个RSet。
垃圾回收流程
- 年轻代垃圾收集. 当年轻一代的占用率达到某个阈值时,就开始年轻代的垃圾回收,G1开始并发收集年轻代。
- 并发收集阶段. 并发标记确定了在下一个空间回收阶段中要保留的内存中所有当前可达的(活动)对象。标记尚未完全完成时,年轻代的垃圾收集同时开始时进行.
- 初始标记(Initial Mark)这个过程共用了Minor GC的暂停,这是因为它们可以复用root scan操作。虽然是STW的,但是过程很短
- Root区扫描 (Root Region Scan)即suvivor区域扫描.可能会有老年代的引用.
- 并发标记(Concurrent Mark)这个阶段从GC Roots 开始对heap中的对象标记,标记线程与应用线程并行执行,并且收集各个Region的存活对象信息
- 重新标记 (Remarking)和CMS类似,也是STW的。标记那些在并发标记发生变化的对象,使用了snapshot-at-the-beginning (SATB) 算法,比cms收集器remark阶段更高效.
- 清理阶段 (Cleanup) 不需要STW。如果发现Region里全是垃圾,这个阶段立马被清理掉。不全是垃圾的Region,并不会被立马处理,它会在Mixed GC阶段,进行收集。
MixedGC
当新对象产生的特别多时,YGC之后,空间依然达到了指定值(-XX:InitiatingHeapOccupancyPercent,默认:45%)启动MixedGC。
过程,类似CMS
- 初始标记STW,标记根对象
- 并发标记,从根对象往下找
- 最终标记STW(重新标记)。找的过程中如果有新的变化,标记出来。
- 筛选回收STW(并行)。对最需要回收,垃圾最多的区域进行回收。
FullGC
G1也会FullGC的,而且Java10以前是串行的,之后是并行。如何避免FGC?
- 扩内存
- 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
- 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
GC何时触发
YGC
-
Eden空间不足
-
多线程并行执行
FGC
-
Old空间不足
-
System.gc()
三色标记算法
将对象标记成3种颜色,白色:未被标记对象,灰色:自身被标记,成员变量未被标记,黑色:自身和成员变量均已标记完成。
漏标
在remark过程中,黑色指向了白色如果不对黑色重新扫描,则会漏标会把白色D对象当做没有新引用指向从而回收掉
并发标记过程中,Mutator删除了所有从灰色到白色的引用,会产生漏标此时白色对象应该被回收
解决方案
打破上述两个条件之一即可
incrementalupdate – 增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性SATB snapshot at the beginning-关注引用的删除当B->D消失时,要把这个引用推到GC的堆栈,保证D还能被GC扫描到
为什么G1用SATB?
灰色>白色引用消失时,如果没有黑色指向白色引用会被push到堆栈下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高SATB配合RSet,浑然天成
日志分析
ZGC
ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求极致低延迟的实验性质的垃圾收集器,它曾经设计目标包括:
停顿时间不超过10ms;
停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
支持8MB~4TB级别的堆(未来支持16TB)。
当初,提出这个目标的时候,有很多人都觉得设计者在吹牛逼。
但今天看来,这些“吹下的牛逼”都在一个个被实现。
基于最新的JDK15来看,“停顿时间不超过10ms”和“支持16TB的堆”这两个目标已经实现,并且官方明确指出JDK15中的ZGC不再是实验性质的垃圾收集器,且建议投入生产了。
ZGC内存布局
为了细粒度的控制内存的分配,和G1垃圾收集器一样,ZGC讲内存划分为小的分区,在ZGC中称为页面(page),ZGC中没有新生代、老年代分代的概念。ZGC支持3种页面,分别为小页面、中页面和大页面。其中小页面指的是2MB的页面空间,中页面指32MB的页面空间,大页面指受操作系统控制的大页。
当对象大小小于等于256KB时,对象分配在小页面。
当对象大小在256KB和4MB之间,对象分配在中页面。
当对象大于4MB,对象分配在大页面。
颜色指针算法
在JVM对象头的64bit中拿出3位,当对象的指针发生变化时,就在这里做记录。
总结
所有的垃圾回收器都会产生STW,只是时间长短而已,ZGC号称可以控制STW在10ms内。实际可以达到2ms。
在G1之前的垃圾回收器都进行了物理分代(年轻代,老年代),G1是逻辑分代。
垃圾回收和内存大小关系
Serial,几十M
PS,几个G
CMS,20G
G1,上百G
ZGC,4TB
常见组合
Serial+SerialOld,单线程的垃圾回收器,
Parallel Scavenge + Parallel Old,后面为了提高效率,诞生了PS(ParallelScavenge),为并发垃圾回收是因为无法忍受STW
ParNew+CMS,了配合CMS,诞生了PN,CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前任何一个JDK版本默认是CMS
参考说明
本文章内容为个人笔记,部分内容来源于马士兵课程视频学习笔记。部分参考自其他相关博客。如有错误欢迎指正。