笔记目录
1.分代收集理论
现代大部分的虚拟机的垃圾回收器都遵循“分代收集”的理论基础来进行设计实现:
- 绝大部分的对象的声明周期很短,都是朝生夕死。
- 历经多次垃圾回收后仍然存活的对象就可能更加越难被回收。
根据这个理论基础,JVM的大部分虚拟机将堆空间逻辑划分为新生代/年轻代、老年代,垃圾收集器可以根据不同的区域选择不同的回收算法进行GC。
2.垃圾回收算法
2.1 标记-复制算法
复制算法
将内存分隔为两块相同大小的区域,每次只使用其中的一块,每当GC完成后,将存活的对象复制到另一块区域,然后将前一块清空。
2.2 标记-清除算法
标记-清除法
分为标记和清除两个阶段,标记存活的对象,然后GC清除掉没有被标记的对象。这种算法实现简单,但是有2个缺点:
- 内存越大,要标记的对象越多,耗时越久,性能越低。
- 内存碎片化问题,导致大对象(数组)分配困难。
2.3 标记-整理算法
标记-整理法
大体流程和标记清除法一致,但是它具有整理内存空间的功能。这种算法避免了内存碎片化的问题,但是效率上相对于标记-清除法略低。
一般情况下,年轻代大多采用标记-复制算法,而老年代则标记-清除和标记-整理算法都有,不同垃圾收集器的实现方案不一样。
2.常见垃圾收集器
3.Serial、Serial Old收集器组合(最古老、现在几乎不主动用)
Serial
和Serial Old
在JVM早期就诞生了,他俩的特点如下:
- 单线程工作模式
- GC期间用户线程暂停,STW一般控制在百级毫秒左右
- 适合几十到几百兆的堆内存使用,内存越大,这两者的回收效果越差
- 现在基本上不会用到他俩,-XX:+UseSerialGC、-XX:+UseSerialOldGC可以开启
4.Parallel Scavenge、Parallel Old(JDK8默认的收集器)
Parallel Scavenge
和Parallel Old
收集器已经是JDK8默认的组合,也是第一款并发收集器,特点:
- GC过程采用多线程模式,线程数可配置,默认是CPU核心数`-XX:ParallelGCThreads`
- 可配置`-XX:MaxGCPauseMillis`,控制GC暂停的最大时间,但吞吐量会降低,因为GC更频繁。
- 他俩更关注JVM系统的吞吐量
5.ParNew收集器(通常配合CMS)
ParNew
和Parallel Scavenge很相似,都是并发执行GC工作,但是ParNew只能负责新生代的GC任务。
6.CMS收集器(划时代意义的一款收集器)
Concurrent Mark Sweep
简称CMS,这是一款追求低延时的收集器,也是第一款真正意义上的并发垃圾收集器,它实现了GC线程和用户线程同时工作的能力,极大提高了JVM垃圾收集的亲和力;但是CMS也是唯一一款采用标记-清除算法的老年代收集器。
CMS整个工作流程分为5个阶段:
6.1 阶段一:初始标记
初始标记
阶段将GC Roots作为根节点扫码与之直连的对象,速度较快耗时很短,但是会暂停用户线程STW。
6.2 阶段二:并发标记
并发标记
阶段从GC Roots直连的对象作为开始,通过可达性分析算法扫码整个堆,这个过程很长,但是可以和用户线程并发执行,所以没有STW。但是并发标记阶段会存在多标、漏标的问题。
6.3 阶段三:重新标记
重新标记
阶段就是为了修正并发标记阶段因为用户线程在执行可能导致对象的引用链发生变化的记录,该过程会STW,暂停时间比初始标记略长。
6.4 阶段四:并发清理
并发清理
阶段将未被标记的对象进行垃圾清理,如果这个阶段还有新对象产生,则会标记为黑色,不会处理。
6.5 阶段五:并发重置
并发重置
阶段重置本次GC过程中标记的数据。
6.6 CMS收集器的特点:
- 和用户线程共同执行,真正的并发、低延迟
- 对CPU资源敏感
- `浮动垃圾`问题无法处理,只有等下一次GC回收,所以CMS的老年代只有92%的可用率,一小部分空间留给了浮动垃圾,这个阈值可以调。
- 内存碎片化问题,这是标记-清除算法的问题,CMS可以通过`-XX:+UseCMSCompactAtFullCollection`命令来执行空间整理。
- CMS适合几个G~一二十G的堆回收场景。
6.7 CMS收集器的一些问题:
- GC未执行完,又触发GC,容易造成`concurrent mode failure`并发清理失败,此时会进入STW,转而用单线程的Serial Old来回收。
7.垃圾收集器的实现原理
垃圾收集器的一些原理支撑了从标记到清理整个过程,有一些原理还是需要了解,例如根节点枚举、安全点、安全区域、三色标记、记忆集、卡表、写屏障、FQueue等。
7.1 根节点枚举
根节点枚举
是指在可达性分析算法中,GC Roots的遍历就是寻找全局性的引用如常量、静态变量、栈帧中的引用等,但是这个过程如果是暴力查找的话会很费时。为了解决这个遍历问题,HotSpot采用了一个叫OopMap的结构来存储这些GC Roots,一旦类加载完成后JVM就会将对象内什么偏移量上是什么类型的数据算出来,在即时编译过程中,会将这些位置的记录引用保存下来,这样提高了GC Roots遍历的速度,但是这个过程会短暂的STW。
7.2 安全点和安全区域
safepoint 安全点
:JVM要保证暂停用户线程的时候,用户线程所执行的指令不会让引用发生改变,所以JVM会在字节码指令中执行一些指令作为安全点
,比如方法调用、循环跳转、异常跳转等指令。因为JVM暂停用户线程不是抢占式中断(立即停下)
,而是用的主动式中断
,就是给线程设置一个中断标志位,用户线程不断轮询这个标志(轮询操作为了高效使用一条汇编指令),一旦发现中断标志,就会在临近自己的安全点主动挂起。
safe region 安全区域
:因为用户线程不一定都是在执行的,有可能处于Sleep,sleep或者blocked线程无法响应中断请求,那么程序就没法进入安全点,安全区域就是说能够保证在接下来某一段代码中,引用关系不会发生改变,这个区域中任意一个位置停下来都是安全的。
7.3 三色标记法
三色标记法
是可达性分析算法中,垃圾收集器在并发标记阶段为了提高效率和解决一些漏标问题提出的一种扫描标记的实现手段,三色标记法将对象分为3类:黑色、灰色、白色。
黑色
:标识对象已经被收集器访问过,并且该对象引用的对象都已经被扫描,黑色对象是安 全存活的。
灰色
:标识对象已经被收集器访问过,但是该对象引用的对象还未完全扫描完成。
白色
:标识对象还未被扫描过,GC刚开始阶段所有对象都是白色,在可达分析之后如果对 象仍然是白色,则会被当做垃圾回收。
7.3.1 多标问题(浮动垃圾)
如果对象原来经过扫描变成了黑色不会被回收,但是在并发标记和并发清理阶段将对象的引用断开,此时C对象已经是垃圾,但是它是黑色的无法被回收;但是这种问题JVM能够容忍,因为对程序并不会造成问题,只是占用了一小部分的内存空间,等下一次GC就会回收这些浮动垃圾。
7.3.2 漏标问题
但是,如果原本是正常的对象结果由于引用关系的断开没有被标记成功,后来又有引用关联,那么这个白色对象D就会被误删除,为了解决这个BUG,JVM提供了2套方案:增量更新、SATB(原始快照)技术来解决漏标问题,CMS采用了增量更新,G1采用了SATB,因为G1的分代模型是分为很多个region,如果使用了增量更新那么得重新扫一遍灰色对象效率变低。
7.3.3 增量更新 incremental update
当有黑色对象指向新的白色对象时,将新的引用记录存下来,等扫描完成后将这些记录引用的黑色节点为根重新扫描一遍,也就是说一旦有对象指向白色对象,那么该对象就变灰色。
7.3.4 原始快照 SATB
当某个灰色对象指向白色对象的引用断开时,将这个引用记录存下来,等扫描完成后将记录中的灰色对象再扫描一遍。
7.4 写屏障
三色标记中的增量更新和原始快照,甚至包括记忆集和卡表的实现不可能硬编码在JVM代码里,JVM提供了写屏障
的机制来完成。
写屏障
:简单理解就是JVM层面对引用类型字段复制时后的AOP切面,就是说在引用类型赋值之前和之后都会触发特定的代码。在复制之前执行指令叫写前屏障
,反之叫写后屏障
。
7.5 记忆集和卡表
JVM为了处理跨代引用的问题,引入了记忆集 R Set
的概念,来记录非收集去到收集区的引用。
卡表 CardTable
:Hotspot将卡表作为Rset的实现手段,卡表是用一个字节数组实现,每一个元素对应了一块固定大小的内存区域卡页
,卡页中可以存在多个对象,只要有对象存在跨代引用,卡表元素就变为1。
而卡表的变更维护是通过写屏障
技术来实现。CMS只需要一份即可,但是G1由于有了region结构,所以每一个region都有一份卡表,所以G1为此付出的内存代价更高。
7.6 F-Queue
在对对象第一次标记的过程中,如果对象覆盖了finalize()
方法,那么对象是有可能拯救自己的。此时对象将被垃圾收集器放入一个队列F-Queue
中,JVM开启一个低优先级的后台线程来依次执行它们的finalize()方法,但是这个执行并不一定保证都执行完毕,因为该方法有可能死循环或者很耽误时间,如果finalize()方法能够让对象拯救自己,那么垃圾收集器将不会回收它们。
8.Garbage First(G1垃圾收集器) (革命性)
G1收集器是JVM一款革命性的垃圾收集器,它打破了传统的物理内存分代的概念,将堆内存化整为零,分为多个相同大小的区域Region
,每一个区域扮演者不同的分代角色,并且区域的角色会跟随GC而改变,每一种角色区域都有不同的策略去回收。目的是进一步减少大内存GC的STW时间,JVM可以通过-XX:UseG1GC
来开启G1。
G1规定,堆内存最多可分为2048个Region
,可以通过-XX:G1HeapRegionSize
调整region大小。
新生代默认占有总内存的5%,可以随着程序运行动态变化,但最多不会超过总内存的60%,伊甸园区和幸存区的比例仍然遵守8:1:1的默认原则。
Humongous
表示大对象的存放区域,一个对象的大小超过了Region的50%就认为是大对象,如果对象过于庞大,则使用连续的Humongous区域存放。
G1的垃圾回收和CMS大体一致一共分为4个阶段:
8.1 阶段一:初始标记
初始标记
阶段短暂暂停用户线程,记录GC Roots直连的对象,STW短暂。
8.2 阶段二:并发标记
并发标记
阶段和用户线程并发执行,和CMS一样,将GC Roots直连的对象最为根节点通过可达性算法和三界标记手段来进行并发标记,此过程较长,但是没有STW,和CMS一样存在多标和漏标问题。
8.3 阶段三:最终标记
最终标记
阶段和CMS一样,处理并发标记阶段导致引用链变化的情况,此阶段会短暂STW,但是漏标的问题和CMS处理手段不一样,CMS使用的是增量更新,G1使用的是原始快照。
8.4 阶段四:筛选回收
筛选回收
阶段是对各个Region区域做回收的成本分析,根据用户设置的期望GC停顿时间-XX:MaxGCPauseMillis
来制定回收计划。回收阶段针对Region使用的是复制算法,将标记了的对象复制到临近的空闲区域,从宏观上来说类似于G1采用了标记-整理法,这种模式相对于CMS而言减少了内存碎片的问题。而选择哪些Region最为回收区域,G1会给每一个Region维护一个优先级列表来控制。
8.5 G1的回收分类
8.5.1 YoungGC
G1的YoungGC并不是Eden区满了就回收,而是会评估现在回收Eden的所有Region成本和用户设定的停顿时间的比值,如果时间充裕便不会进行GC,而是开辟新的空闲区域作为Eden。
8.5.2 MixedGC
MixedGC并不是FullGC,当老年代所有的Region总和超过了设定的堆内存的阈值,则回收所有的Young区Region和一部分Ola区Region,采用复制算法,将存活的对象拷贝到临近的Region去,如果过程中没有足够的空闲Region则触发一次G1的FullGC。
8.5.3 FullGC
类似于CMS的并发回收失败,G1会暂停用户线程,采用单线程的Serial模式来进行标记清理。
8.6 G1的特点和参数
- -XX:G1HeapRegionSize 指定Region的大小,必须是2的幂次方。
- -XX:MaxGCPauseMillis 指定最大的GC暂停时间。
- -XX:G1NewSizePercnet 指定新生代占比,默认是5%。
- -XX:InitiatingHeapOccupancyPercent 指定老年代占堆的阈值
- G1适合>8G堆内存的JVM,<8G有时候反而不如CMS+ParNew组合,因为复杂的底层。
- G1适合对象分配和晋升速度快的场景。
- G1适合对STW要求高的场景。
- G1从整体宏观上表现为标记-整理算法,但微观Region是复制算法,且是整堆回收。
9.ZGC(TB级10ms停顿,JDK11推出)
9.1 ZGC的内存布局
ZGC是JDK11推出的一款追求极致停顿的垃圾收集器,和G1一样他将堆内存划分为多个区,但是抹掉了分代的概念,转而替代的是分区的概念,ZGC中称之为区域Region
。ZGC中一共分为三种区域:小区域、中区域、大区域。ZGC也是通过标记-整理算法实现。-XX:+UseZGC
可以开启。
ZGC对于不同的页面回收的策略也不相同,小页面优先回收,中页面和大页面尽量不回收
的原则。
9.2 标准大页和NUMA
9.2.1 标准大页
标准大页
是Linux推出,目的是通过大页内存来取代传统的4KB内存页面,以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面功能。标准大页有2种格式:2M和1G,2MB页面大小适合GB大小的内存管理,1GB大小页面适合TB级别的内存管理,标准大页默认2MB。
9.2.2 NUMA(非统一内存访问)
NUM统一内存访问是多个CPU共享一个内存,这样存在带宽、竞争等问题,效率不高。后来X86架构提出了NUMA,CPU访问本地内存肯定要比非本地内存要快。
ZGC是支持NUMA的,在进行小区域分配时会优先从小区域分配,当不能分配时才会从远端内存分配。ZGC这样设计,对于小区域存放的都是小对象,从本地内存分配;中区域和大区域需要更大空间,如果也从本地内存分配,可能造成内存使用不均衡、影响性能。
9.3 染色指针 Colored pointer技术
染色指针
又称指针着色
技术,它是ZGC的核心概念,它必须在64位的操作系统上才能生效,所以ZGC不能够进行指针压缩。
ZGC的染色指针技术有三大优势:
- 染色指针可以让某个Region存活的对象被转移走后,这个Region立即就能够被释放出来,以供使用。
- 染色指针可以大幅度减少写屏障的使用数量,因为对象引用变动的信息可以维护在64位的指针中。
- 染色指针可以作为一种可拓展的存储结构来记录更多与对象标记、重定位过程相关的数据。
9.4 ZGC工作流程
ZGC的工作流程主要分为四个阶段:
9.4.1 阶段一:并发标记阶段
并发标记阶段和G1一样,通过初始标记、并发标记、最终标记的流程,不同的是ZGC的标记更新的是对象的指针里Marked0和Marked1标志位,每轮的标记主色只有一个,每轮之间颜色交替标记,如果在并发标记阶段发现有上一次标记了的对象,说明这些对象还没有完成重定位,需要将对象新的正确的指针更新并且在Region中的转发表移除,这两步是一个原子操作。
9.4.2 阶段二:并发预备重分配
该过程根据一定的算法得出本次收集过程中需要清理哪些Region,将这些要回收的Region组成一个重分配集合Relocation Set
。
9.4.3 阶段三:并发重分配
该阶段是将重分配集合中存活的对象复制到新的Region中,并为重分配集合中的每一个Region维护一个转发表 Forward Table
,记录从旧对象到新对象的转向关系。如果此时用户线程访问了重分配集合中的对象,那么这次访问会被读屏障
所捕获,根据转发表的记录将访问转发到新复制的对象上,同时修正更新该引用地址,使其直接引用新对象,ZGC将其这种行为称之为自愈 Self-Healing
。一旦重分配集合中某个Region复制转移完毕那么这个Region就可以立即释放,但是这个转发表暂时还不能释放。
9.4.4 阶段四:并发重映射
修正整个堆中指向重分配集合中旧对象的引用,但是ZGC不急于处理,因为三阶段中ZGC持有转发表,可以自愈。这样ZGC巧妙地把阶段四的工作交到下一次GC时候的并发标记阶段里去。
9.5 ZGC中的读屏障
在并发重分配阶段,还没有来得及做重定位的对象,当应用程序读取对象引用的时候,在此之前类似写屏障一样的AOP方式,将对象重定位+删除转发表中的记录这俩原子操作执行。
9.6 ZGC存在的问题
ZGC最大的问题就是浮动垃圾
,ZGC的停顿可以控制在10ms内,但是ZGC的执行时间还是很长的,在大流量场景中很多可回收对象都是放到下一次GC进行回收,这些就是浮动垃圾,因为ZGC内有分代的概念,所以每次都是整堆扫描,导致一些短生命周期的对象不能即时回收。
9.7 ZGC触发的时机
ZGC目前有4种机制触发GC:
- 定时触发,默认不开启。
- 预热触发,最多三次,在堆内存达到10%,20%,30%时触发。
- 基于分配速率的自适应:这是最主要的GC触发方式也是默认方式。
- 主动触发
- 阻塞内存分配请求触发:当垃圾来不及回收,垃圾即将占满堆时,导致部分线程阻塞。
- 元数据分配触发:元数据去不足时导致。
9.8 ZGC的生产注意事项和应用场景
- RSS内存异常现象,Linux统计内存的算法问题,因为ZGC使用了内存映射,在解析阶段会转发多次,LInux就会误统计。
- 共享内存调整,因为ZGC需要在进程的共享内存空间建立一个文件来映射物理地址。
- mmap节点上限的调整,ZGC使用了内存映射技术所以需要调大Linux的mmap映射数目上限。
- 应用场景:
- 超大堆内存应用,因为ZGC采用分区模型加上动态的成本分析算法来降低STW。
- 项目需要提供高级别协议,例如响应时间不能超过10ms等场景。