垃圾回收基础
什么是垃圾
内存中已经不再被使用到的内存空间就是垃圾
如何判断垃圾
判断方法有:引用计数法、根搜索算法
- 引用计数法:给对象添加一个引用计数器,有访问就加 1,引用失效就减 1
- 优点:实现简单、效率高
- 缺点:不能解决对象之间循环引用的问题,商用虚拟机都不采用该方法
- 根搜索算法:从根(GC Roots)节点向下搜索对象节点,搜索走过的路径称为引用链,当一个对象到根之间没有连通的话,则该对象不可用
- 可作为 GC Roots 的对象包括:虚拟机栈(栈帧局部变量)中引用的对象、方法区类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象、被同步锁持有的对象等
- 在大型应用程序中,对象的引用特别复杂,引用链特别长,每一个对象都使用根搜索算法将导致性能下降
- HotSpot 使用一组叫 OopMap 的数据结构(用于描述对象间引用关系的数据结构)达到准确 GC 的目的,就不用每次都使用根搜索算法,极大的提高了 GC 效率
- 使用 OopMap,JVM 可以很快的做完 GC Roots 枚举,但 JVM 并没有为每一条指令生成一个 OopMap
- 记录 OopMap 的“特定位置”被称为安全点,即当前线程执行到安全点后才允许暂停进行 GC
- 如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方开始 GC 都是安全的,那么这个区域称为安全区域
判断垃圾的步骤
- 根搜索算法判断不可用,不可用时成为垃圾的几率非常高
- 看是否有必要执行 finalize() 方法,对象第一次回收时调用 finalize() 方法
- 如果对象没有覆盖 finalize() 方法或 finalize() 方法已经被虚拟机调用过,就属于没有必要执行 finalize() 方法的情况
- 如果对象覆盖了 finalize() 方法,在方法中重新使用本对象,即对象自救,该对象还不能被垃圾回收
- 前两个步骤走完后,对象仍然没有被使用,那该对象就属于垃圾
判断类无用的条件
- JVM 中该类的所有实例都已经被回收
- 加载该类的 ClassLoader 被回收
- 没有任何地方引用该类的 Class 对象
- 无法在任何地方通过反射访问这个类
引用分类
- 强引用:通过 new 创建的对象,不能被回收
- 软引用:还有用但并不必须的对象,用 SoftReference 来实现
- 弱引用:非必须的对象,垃圾回收时会回收掉,用 WeakReference 来实现
- 虚引用:最弱的引用,垃圾回收时会回收掉,用 PhantomReference 来实现
跨代引用
- 一个代中的对象引用另一个代中的对象
- 跨代引用假说:跨代引用相对于同代引用来说只是极少数
- 隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的
记忆集(Remembered Set)
- 一种用于记录从非收集区域指向收集区域的指针集合(跨代引用的指针集合)的抽象数据结构
- 精度
- 字长精度:每个记录精确到一个机器字长,该字包含跨代指针
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针
- 卡表(Card Table):是记忆集的一种具体实现,定义了记忆集的记录精度和与堆内存的映射关系等
- 卡页(Card Page):卡表的每个元素都对应其标识的内存区域中一块特定大小的内存块,这个内存块被称为卡页
写屏障
- 写屏障,可以看成 JVM 对“引用类型字段赋值”这个动作的 AOP
- 通过写屏障来实现当对象状态改变后,维护卡表状态
GC 类型
- MinorGC/YoungGC:发生在新生代的收集动作
- MajorGC/OldGC:发生在老年代的 GC,目前只有 CMS 收集器会单独收集老年代
- MixedGC:收集整个新生代以及部分老年代,目前只有 G1 收集器会有这种行为
- FullGC:收集整个 Java 堆和方法区的 GC
Stop-The-World
- STW 是 Java 中一种全局暂停的现象,多半是由于 GC 引起的
- 全局暂停就是所有 Java 代码停止运行,native 代码可以执行,但不能和 JVM 交互
- 长时间服务停止,没有响应
- 对应 HA 系统来说,可能引起主备切换,严重危害生产环境
垃圾收集类型
- 串行收集:GC 单线程内存回收,会暂停所有的用户线程,如:Serial
- 并行收集:多个 GC 线程并发工作,会暂停所有的用户线程,如:Parallel
- 并发收集:用户线程和 GC 线程同时执行,不需要停顿用户线程,如:CMS
垃圾收集算法
标记清除法
- 标记清除法(Mark-Sweep)算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
- 优点
- 简单
- 缺点
- 标记和清除的效率都不高
- 标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发 GC
复制算法
- 复制算法(Copying):把内存分成两块完全相同的区域,每次使用其中一块区域,当一块使用完后就将该区域中存活的对象拷贝到另一块中,然后将该区域的内存全部清除
- 优点
- 简单
- 运行高效
- 不用考虑内存碎片问题
- 缺点
- 浪费内存
- JVM 实际实现中,新生代是将内存分成一块较大的 Eden 区和两块较小的 Survivor 区(from,to),每次使用 Eden 和一块 Survivor 区,回收时,把存活的对象复制到另一个 Survivor 区,然后清空 Eden 区和 from 区
- HotSpot 默认的 Eden 和 Survivor 比例是 8:1,也就是每次能使用 90% 的新生代空间
- 如果 Survivor 空间不够时,就要依赖老年代进行分配担保,把放不下的大对象直接进入老年代
标记整理法
- 标记整理算法(Mark-Compact):由于复制算法在存活对象比较多的时候,效率较低,且有空间浪费,因此老年代多采用标记整理算法
- 标记过程和标记清除算法一样,但不直接清除可回收对象,而是让所有存活对象都移向一端,然后直接清除边界以外的内存
分配担保(分代)
分配担保:当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的 GC 作空间分配担保,步骤如下:
- 在发生 MinorGC 前,JVM 会检查老年代的最大可用的连续空间,是否大于新生代所有对象的总空间,如果大于,可以确保 MinorGC 是安全的
- 如果小于,JVM 会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小
- 如果大于,则尝试一次 MinorGC,否则进行一次 Full GC
垃圾收集器
不同厂商,不同版本的虚拟机实现差别很大
HotSpot 中的收集器
- 新生代
- Serial
- ParNew
- Parallel Scavenge
- 老年代
- CMS
- Serial Old
- Parallel Old
- 新生代和老年代共有
- G1
串行收集器
- Serial(串行)收集器 / Serial Old 收集器,是一个单线程的收集器,在垃圾收集时会出现 Stop-the-world
- 优点:简单,对于单 CPU,由于没有多线程的交互开销,可能更高效。是默认的 Client 模式下的新生代收集器
- 使用 -XX:+UseSerialGC 来开启,会使用:Serial + Serial Old 的收集器组合
并行收集器
- ParNew(并行)收集器:使用多线程进行垃圾回收,在垃圾收集时会出现 Stop-the-world
- 在并发能力好的 CPU 环境里,它停顿的时间要比串行收集器短,对于单 CPU 或并发能力较弱的 CPU,由于多线程的交互、上下文切换等开销,可能比串行回收期更差
- 是 Server 模式下首选的新生代收集器,且能和 CMS 收集器配合使用
- JDK 5 之后不再使用 -XX:+UseParNewGC 来单独开启,使用 -XX:+UseConcMarkSweepGC 开启 CMS 收集器时默认开启 PerNew 收集器
- -XX:ParallelGCThreads:指定线程数,一般与 CPU 数量一致
- 新生代使用复制算法
新生代 Parallel Scavenge 收集器
- 新生代 Parallel Scavenge 收集器 / Parallel Old 收集器:是一个应用于新生代的、使用复制算法的、并行的收集器
- 和 ParNew 类似,但更关注吞吐量,能最高效率的利用 CPU,适合运行后台应用
- 使用 -XX:+UseParallelGC 来开启
- 使用 -XX:+UseParallelOldGC 来开启老年代使用 Parallel Old 收集器,使用 Parallel Scavenge + Parallel Old 的收集器组合
- -XX:+MaxGCPauseMillis:设置 GC 的最大停顿时间,设置过小可能导致 GC 不能一次完成,从而使得 GC 的频率过高
CMS 收集器
G1 收集器
新生代回收过程
老年代回收过程
ZGC 收集器
GC 性能指标
JVM 内存配置原则
Java 垃圾回收机制
1. 垃圾回收基础
1.1 什么是垃圾
垃圾是指内存中已经不再被程序使用的对象所占用的内存空间。
1.2 如何判断对象是垃圾
1.2.### 1.5 分代收集理论
1.5.1 分代假说
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
1.5.2 跨代引用
- 定义:新生代中的对象被老年代对象所引用,或老年代对象被新生代对象所引用
- 跨代引用假说:跨代引用相对于同代引用来说占极少数
- 隐含推论:存在互相引用关系的两个对象,应该倾向于同时生存或同时消亡
1.5.3 记忆集(Remembered Set)
- 定义:用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
- 目的:避免把整个老年代加进 GC Roots 扫描范围
记忆集精度级别:
- 字长精度:每个记录精确到一个机器字长
- 对象精度:每个记录精确到一个对象
- 卡精度:每个记录精确到一块内存区域
卡表(Card Table):
- 记忆集的一种具体实现
- 定义了记忆集的记录精度与堆内存的映射关系
- 每个卡表元素对应一个卡页(Card Page)
1.5.4 写屏障(Write Barrier)
- 定义:JVM 对"引用类型字段赋值"动作的 AOP 切面
- 作用:在对象引用关系发生变化时,维护卡表状态
- 类型:
- 写前屏障:在引用关系建立之前执行
- 写后屏障:在引用关系建立之后执行引用计数器:
- 原理:每当有一个地方引用它时,计数器值加 1;引用失效时,计数器值减 1
- 优点:实现简单、效率高
- 缺点:无法解决对象之间循环引用的问题,主流商用虚拟机都不采用此方法
1.2.2 可达性分析算法(根搜索算法)
通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索:
- 原理:搜索走过的路径称为引用链,当一个对象到 GC Roots 间没有任何引用链相连时,则证明此对象不可达
- GC Roots 对象包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
- JVM 内部的引用(如基本数据类型对应的 Class 对象)
- 被同步锁(synchronized 关键字)持有的对象
1.2.3 GC 优化技术
OopMap(Ordinary Object Pointer Map):
- HotSpot 使用 OopMap 数据结构来记录对象内什么偏移量上是什么类型的数据
- 避免每次 GC 都遍历整个执行上下文和全局的引用位置,大幅提高 GC 效率
安全点(SafePoint):
- 只有在特定位置才记录 OopMap 信息,这些位置称为安全点
- 程序执行时并非在所有地方都能停顿下来开始 GC,只有在到达安全点时才能暂停
安全区域(Safe Region):
- 在一段代码片段中,对象的引用关系不会发生变化
- 在这个区域中的任何位置开始 GC 都是安全的是垃圾 内存中已经不再被使用到的内存空间就是垃圾
如何判断垃圾
判断方法有:引用计数法、根搜索算法
- 引用计数法:给对象添加一个引用计数器,有访问就加 1,引用失效就减 1
- 优点:实现简单、效率高
- 缺点:不能解决对象之间循环引用的问题,商用虚拟机都不采用该方法
- 根搜索算法:从根(GC Roots)节点向下搜索对象节点,搜索走过的路径称为引用链,当一个对象到根之间没有连通的话,则该对象不可用
- 可作为 GC Roots 的对象包括:虚拟机栈(栈帧局部变量)中引用的对象、方法区类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象、被同步锁持有的对象等
- 在大型应用程序中,对象的引用特别复杂,引用链特别长,每一个对象都使用根搜索算法将导致性能下降
- HotSpot 使用一组叫 OopMap 的数据结构(用于描述对象间引用关系的数据结构)达到准确 GC 的目的,就不用每次都使用根搜索算法,极大的提高了 GC 效率
- 使用 OopMap,JVM 可以很快的做完 GC Roots 枚举,但 JVM 并没有为每一条指令生成一个 OopMap
- 记录 OopMap 的“特定位置”被称为安全点,即当前线程执行到安全点后才允许暂停进行 GC
- 如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方开始 GC 都是安全的,那么这个区域称为安全区域
1.3 对象的生命周期判断
1.3.1 对象死亡判定过程
- 第一次标记:通过可达性分析判断对象是否可达,不可达的对象被第一次标记
- finalize()方法判断:
- 如果对象没有覆盖 finalize()方法,或 finalize()方法已经被虚拟机调用过,则直接回收
- 如果对象覆盖了 finalize()方法且尚未被调用,对象会被放置在 F-Queue 队列中
- 第二次标记:执行 finalize()方法后再次判断对象是否可达
- 如果对象在 finalize()中重新建立引用链(对象自救),则移出回收集合
- 否则对象将被回收
注意:不建议使用 finalize()方法,它运行代价高昂且不确定性大
1.3.2 类的卸载条件
一个类要被卸载,需要同时满足以下条件:
- 该类所有的实例都已经被回收
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有在任何地方被引用
- 无法在任何地方通过反射访问该类的方法
1.4 引用类型
Java 将引用分为四种类型:
1.4.1 强引用(Strong Reference)
- 定义:通过
new
关键字创建的普通对象引用 - 特点:只要强引用存在,垃圾收集器永远不会回收被引用的对象
- 示例:
Object obj = new Object();
1.4.2 软引用(Soft Reference)
- 定义:用来描述一些还有用但非必需的对象
- 特点:在系统将要发生内存溢出异常前,会把这些对象列进回收范围进行第二次回收
- 实现:使用
SoftReference
类 - 应用场景:内存敏感的高速缓存
1.4.3 弱引用(Weak Reference)
- 定义:用来描述那些非必需对象,强度比软引用更弱
- 特点:只能生存到下一次垃圾收集发生为止
- 实现:使用
WeakReference
类 - 应用场景:ThreadLocal、WeakHashMap 等
1.4.4 虚引用(Phantom Reference)
- 定义:最弱的一种引用关系
- 特点:无法通过虚引用来取得一个对象实例,唯一目的是在对象被回收时收到一个系统通知
- 实现:使用
PhantomReference
类,必须和引用队列联合使用
跨代引用
- 一个代中的对象引用另一个代中的对象
- 跨代引用假说:跨代引用相对于同代引用来说只是极少数
- 隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的
记忆集(Remembered Set)
- 一种用于记录从非收集区域指向收集区域的指针集合(跨代引用的指针集合)的抽象数据结构
- 精度
- 字长精度:每个记录精确到一个机器字长,该字包含跨代指针
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针
- 卡表(Card Table):是记忆集的一种具体实现,定义了记忆集的记录精度和与堆内存的映射关系等
- 卡页(Card Page):卡表的每个元素都对应其标识的内存区域中一块特定大小的内存块,这个内存块被称为卡页
写屏障
- 写屏障,可以看成 JVM 对“引用类型字段赋值”这个动作的 AOP
- 通过写屏障来实现当对象状态改变后,维护卡表状态
1.6 垃圾收集相关概念
1.6.1 GC 类型
- Minor GC/Young GC:发生在新生代的垃圾收集
- Major GC/Old GC:发生在老年代的垃圾收集(目前只有 CMS 收集器会单独收集老年代)
- Mixed GC:收集整个新生代以及部分老年代(目前只有 G1 收集器有这种行为)
- Full GC:收集整个 Java 堆和方法区的垃圾收集
1.6.2 Stop-The-World(STW)
- 定义:Java 中一种全局暂停现象,主要由 GC 引起
- 表现:所有 Java 代码停止运行,native 代码可以执行但不能与 JVM 交互
- 影响:
- 导致长时间服务停止,无响应
- 对于 HA 系统可能引起主备切换
- 严重影响生产环境性能
1.6.3 垃圾收集器类型
- 串行收集:GC 单线程执行内存回收,会暂停所有用户线程(如 Serial 收集器)
- 并行收集:多个 GC 线程并发工作,会暂停所有用户线程(如 Parallel 收集器)
- 并发收集:用户线程与 GC 线程同时执行,不需要停顿用户线程(如 CMS 收集器)
2. 垃圾收集算法
2.1 标记-清除算法(Mark-Sweep)
2.1.1 算法原理
分为"标记"和"清除"两个阶段:
- 标记阶段:标记出所有需要回收的对象
- 清除阶段:在标记完成后,统一回收所有被标记的对象
2.1.2 优缺点
优点:
- 实现简单
- 基础性算法,后续算法都基于此思想
缺点:
- 执行效率不稳定(随着对象数量增长而降低)
- 内存空间碎片化问题严重
2.2 标记-复制算法(Mark-Copy)
2.2.1 算法原理
将内存分为大小相等的两块,每次只使用其中一块:
- 当一块内存用完时,将存活的对象复制到另一块内存上
- 清理已使用的内存空间
2.2.2 优缺点
优点:
- 实现简单,运行高效
- 没有内存碎片问题
- 分配内存时只需移动堆顶指针
缺点:
- 内存使用率只有 50%
- 对象存活率高时复制操作频繁,效率下降
2.2.3 HotSpot 新生代实现
- 内存划分:一个较大的 Eden 空间和两个较小的 Survivor 空间(From、To)
- 默认比例:Eden:Survivor:Survivor = 8:1:1
- 工作流程:
- 新对象分配在 Eden 区
- Eden 区满时,将 Eden 和 From Survivor 中存活对象复制到 To Survivor
- 清空 Eden 和 From Survivor
- 交换 From 和 To Survivor 的角色
- 分配担保:当 Survivor 空间不足时,依赖老年代进行分配担保
2.3 标记-整理算法(Mark-Compact)
2.3.1 算法原理
针对老年代特点设计的算法:
- 标记阶段:与标记-清除算法相同,标记所有需要回收的对象
- 整理阶段:让所有存活对象都向内存空间一端移动
- 清除阶段:清理掉端边界以外的内存
2.3.2 优缺点
优点:
- 没有内存碎片问题
- 不会像复制算法那样浪费内存空间
缺点:
- 移动大量对象并更新所有引用这些对象的地方,是一种极为负重的操作
- 停顿时间较长
2.4 分代收集算法
2.4.1 算法思想
根据对象存活周期的不同将内存划分为几块,不同块采用最适当的收集算法。
2.4.2 分代策略
- 新生代:对象朝生夕灭,使用标记-复制算法
- 老年代:对象存活率高,使用标记-清除或标记-整理算法
2.4.3 空间分配担保
当新生代进行垃圾回收后,如果 Survivor 区无法容纳存活对象,需要老年代进行分配担保:
- MinorGC 前检查:检查老年代最大可用连续空间是否大于新生代所有对象总空间
- 空间充足:如果大于,则 MinorGC 是安全的
- 空间不足:检查是否允许分配担保失败
- 允许:检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小
- 如果大于:尝试一次 MinorGC(虽然有风险)
- 如果小于或不允许担保:进行一次 FullGC
3. 垃圾收集器
注意:不同厂商、不同版本的虚拟机实现差别很大,以下主要介绍 HotSpot 虚拟机中的收集器。
3.1 HotSpot 收集器概览
3.1.1 新生代收集器
- Serial:单线程收集器
- ParNew:Serial 的多线程版本
- Parallel Scavenge:关注吞吐量的多线程收集器
3.1.2 老年代收集器
- Serial Old:Serial 的老年代版本
- Parallel Old:Parallel Scavenge 的老年代版本
- CMS:并发标记清除收集器
3.1.3 全堆收集器
- G1:面向服务端的低延迟收集器
- ZGC:超低延迟收集器
- Shenandoah:超低延迟收集器
3.2 Serial/Serial Old 收集器
3.2.1 基本特征
- 工作方式:单线程收集器,进行垃圾收集时必须暂停所有工作线程(Stop-The-World)
- 适用场景:Client 模式下的默认新生代收集器
- 算法:新生代使用标记-复制算法,老年代使用标记-整理算法
3.2.2 优缺点
优点:
- 简单高效,对于单 CPU 环境,没有线程交互开销
- 对于运行在 Client 模式下的虚拟机是很好的选择
缺点:
- 对于多 CPU 环境,停顿时间较长
3.2.3 参数配置
- 启用:
-XX:+UseSerialGC
(Serial + Serial Old 组合)
![Serial收集器运行示意图]
3.3 ParNew 收集器
3.3.1 基本特征
- 工作方式:Serial 收集器的多线程并行版本
- 适用范围:新生代收集器,能够与 CMS 收集器配合工作
- 算法:标记-复制算法
3.3.2 性能特点
- 多 CPU 环境:停顿时间比 Serial 收集器短
- 单 CPU 环境:由于多线程交互开销,可能比 Serial 收集器差
- Server 模式:JDK 7 之前首选的新生代收集器
3.3.3 参数配置
- 启用:
-XX:+UseConcMarkSweepGC
(开启 CMS 时自动启用 ParNew) - 线程数:
-XX:ParallelGCThreads
,一般设置为与 CPU 核数相等
![ParNew收集器运行示意图]
3.4 Parallel Scavenge/Parallel Old 收集器
3.4.1 基本特征
- 设计目标:达到一个可控制的吞吐量(Throughput)
- 吞吐量:CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值
- 适用场景:后台运算而不需要太多交互的分析任务
3.4.2 关键特性
- 自适应调节策略:虚拟机会根据当前系统运行情况收集性能监控信息,动态调整参数
- GC 自适应的调节策略:也是 Parallel Scavenge 与 ParNew 的重要区别
3.4.3 参数配置
- 启用新生代:
-XX:+UseParallelGC
- 启用老年代:
-XX:+UseParallelOldGC
- 最大停顿时间:
-XX:MaxGCPauseMillis
- 吞吐量大小:
-XX:GCTimeRatio
- 自适应调节:
-XX:+UseAdaptiveSizePolicy
注意:过小的停顿时间设置可能导致 GC 频率增加,反而降低吞吐量
![Parallel Scavenge收集器运行示意图]
3.5 CMS 收集器(Concurrent Mark Sweep)
3.5.1 基本特征
- 设计目标:获取最短回收停顿时间
- 工作方式:基于标记-清除算法实现
- 并发特性:大部分收集工作可以与用户线程并发执行
3.5.2 工作流程
- 初始标记:标记 GC Roots 能直接关联到的对象(需要 STW,时间很短)
- 并发标记:进行 GC Roots Tracing 的过程(耗时最长,可与用户线程并发)
- 重新标记:修正并发标记期间因用户程序运行导致标记变动(需要 STW,时间较短)
- 并发清除:清除死亡对象(可与用户线程并发)
3.5.3 优缺点
优点:
- 并发收集、低停顿
缺点:
- 对 CPU 资源敏感(占用部分线程资源)
- 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"
- 基于标记-清除算法,会产生内存碎片
3.5.4 参数配置
- 启用:
-XX:+UseConcMarkSweepGC
- 并发线程数:
-XX:ConcGCThreads
- 触发百分比:
-XX:CMSInitiatingOccupancyFraction
- 内存碎片整理:
-XX:+UseCMSCompactAtFullCollection
3.6 G1 收集器(Garbage First)
3.6.1 基本特征
- 设计目标:在延迟可控的情况下获得尽可能高的吞吐量
- 内存布局:将 Java 堆划分为多个大小相等的独立区域(Region)
- 收集范围:可以面向堆的任何部分来组成回收集进行回收
3.6.2 重要概念
- Region:G1 将堆划分为 2048 个大小相等的 Region
- Remembered Set:每个 Region 都有一个 Remembered Set
- Collection Set:记录了 GC 要收集的 Region 集合
3.6.3 运行流程
新生代回收过程:
- 选定所有新生代里的 Region
- 通过控制新生代的 Region 个数来控制新生代的大小
老年代回收过程:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
3.6.4 参数配置
- 启用:
-XX:+UseG1GC
- 最大停顿时间:
-XX:MaxGCPauseMillis
- Region 大小:
-XX:G1HeapRegionSize
3.7 ZGC 收集器
3.7.1 基本特征
- 设计目标:在任何堆内存大小下都能把停顿时间限制在 10 毫秒以内的低延迟垃圾收集器
- 内存管理:基于 Region 内存布局
- 并发特性:支持并发的标记、重定位等操作
3.7.2 关键技术
- 着色指针:直接将少量额外的信息存储在指针上
- 读屏障:在对象访问时加入额外的处理逻辑
- 并发重定位:支持并发的对象重定位
3.7.3 参数配置
- 启用:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
4. 垃圾收集器性能指标
4.1 关键指标
- 吞吐量(Throughput):CPU 用于运行用户代码时间与 CPU 总消耗时间的比值
- 停顿时间(Pause Time):执行垃圾收集时,程序的工作线程被暂停的时间
- 内存占用(Memory Footprint):Java 堆区所占的内存大小
4.2 性能权衡
- 吞吐量和停顿时间是一对相互竞争的目标
- 内存占用越小,吞吐量和停顿时间表现越好
- 在实际应用中需要根据具体需求选择合适的收集器
5. JVM 内存配置原则
5.1 堆内存配置
- 堆大小:
-Xms
和-Xmx
建议设置为相同值,避免堆自动扩展 - 新生代比例:
-XX:NewRatio
控制老年代与新生代的比例 - Eden 与 Survivor 比例:
-XX:SurvivorRatio
控制 Eden 与 Survivor 的比例
5.2 垃圾收集器选择
- 低延迟需求:选择 G1、ZGC 或 Shenandoah
- 高吞吐量需求:选择 Parallel 收集器
- 内存较小应用:选择 Serial 收集器
5.3 调优建议
- 监控为先:建立完善的 GC 日志和监控
- 渐进调优:逐步调整参数,避免激进修改
- 压测验证:在生产环境前进行充分的压力测试
- 持续优化:根据实际运行情况持续优化参数