【Java虚拟机】JVM垃圾回收器详解
1.什么是垃圾收集器
- 垃圾回收算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现
- 目前Java规范中并没有对垃圾收集器的实现有任何规范
- 不同的厂商、不同的版本的虚拟机提供的垃圾收集器是不同的,主要讨论的是HotSpot虚拟机
- 为什么要有很多收集器?
- 因为Java的使用场景很多,移动端,服务器等,然后内存里面对象存活时间不一样
- 需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能
2.垃圾收集器分类
(1)新生代垃圾回收器
- Serial 串行垃圾回收器
- ParNew 年轻代并发垃圾回收器
- Parallel并行垃圾回收器
(2)老年代垃圾回收器
- Serial Old 串行老年代垃圾器
- Parallel Old 老年代的并行垃圾回收器
- CMS (ConcMarkSweep)并发标记清除
(3)整堆收集器:G1、ZGC
2.图解分配垃圾收集器的组合
- JDK8中默认使用: Parallel Scavenge GC + ParallelOld GC
- JDK14 弃用了: Parallel Scavenge GC + Parallel OldGC
- JDK9默认是用G1为垃圾收集器
- JDK14 移除了 CMS GC
- 年轻代与年老代的垃圾回收器组合
3.垃圾收集器关注的核心指标
- 吞吐量
- 运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
- 例子:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
- 暂停时间
- 执行垃圾收集时,程序的工作线程被暂停的时间
- 一个时间段内应用程序线程暂停,让GC线程执行的状态
- GC期间100毫秒的暂停时间,说明在这100毫秒期间内没有应用程序线程是活动的
- 收集频率
- 指垃圾回收器多长时间会运行一次。一般来说,垃圾回收器的频率应该是越低越好。
4.Serial收集器详解
- Serial是最简单的垃圾收集器,使用单线程进行垃圾收集,暂停所有应用程序线程, 在单核CPU环境来说,Serial收集器更高效
- Serial Old是Serial收集器的老年代版本,在jdk1.5之前的版本与Parallel收集器搭配使用,或者作为CMS的备选方案
- 适用于小型应用程序和客户端应用程序,一般javaweb、springboot项目不会采用这类收集器
- 新生代采用复制算法,老年代采用标记整理算法
- 相关命令参数使用
- 同时指定年轻代和老年代都使用串行垃圾收集器
-XX:+UseSerialGC
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
- 同时指定年轻代和老年代都使用串行垃圾收集器
//参数
-XX:+UseSerialGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
//输出
-XX:InitialHeapSize=33554432 -XX:MaxHeapSize=33554432 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
5.ParNew收集器详解
- 工作在年轻代上的,只是将串行的垃圾收集器改为了并行,其他基本和Serial一样,使用多个线程进行垃圾回收的
- 适用于大型应用程序和多核处理器,以及在服务端应用程序中使用,单核上效率比Serial低
- 和下集讲Parallel收集器类似,但Parallel收集器不兼容CMS,除了它只有Serial收集器可以和CMS收集器配合工作
- 新生代采用复制算法,老年代采用标记整理算法
- 相关命令参数使用
- 年轻代使用ParNew回收器,老年代使用串行收集器
-XX:+UseParNewGC
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
- 年轻代使用ParNew回收器,老年代使用串行收集器
//参数
-XX:+UseParNewGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
//输出
-XX:InitialHeapSize=33554432 -XX:MaxHeapSize=33554432 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParNewGC
6.Parallel收集器详解
Parallel全称 Parallel Scavenge 是一种多线程垃圾收集器,和ParNew收集器类似,是一个新生代收集器
默认线程数和cpu核数一样,用于大型应用程序和服务器应用程序,比如大批量数据处理,后台计算任务等
Parallel Old是Parallel Scavenge收集器的老年代版本,JDK8默认使用Parallel Scavenge收集器
算法:新生代采用复制算法,老年代采用标记整理算法
Parallel对比ParNew
- -XX:+UseParallelGC 仅对年轻代有效,不可以和CMS收集同时使用
- -XX:+UseParNewGC 设置年轻代为多线程收集,可以和CMS收集同时使用
相关命令参数使用
年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器
-XX:+UseParallelGC
年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器
-XX:+UseParallelOldGC
查看命令行相关参数
-XX:+PrintCommandLineFlags
//参数
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
//输出
-XX:InitialHeapSize=33554432 -XX:MaxHeapSize=33554432 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC -XX:+UseParallelOldGC
7.CMS收集器详解
- CMS 全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器
- 老年代中的对象生命周期较长,垃圾回收频率较低,目标是获取最短垃圾收集停顿时间,针对 老年代 垃圾的收集器
- 停顿时间较短,适合对响应时间要求较高的应用程序,如Web应用程序、电子商务等高并发场景
- 整个过程分4步**(初始标记 和 重新标记 需要stopTheWorld,并发标记与并发清除阶段不需要暂停用户线程)**
- 初始标记: 标记GC Root直接关联对象,会导致stopTheWorld
- 并发标记: 与用户线程同时运行
- 重新标记:会导致stopTheWorld
- 并发清除:与用户线程同时运行
相关命令参数使用
- 年轻代使用ParNew垃圾回收器,老年代使用CMS回收器
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
- 年轻代使用ParNew垃圾回收器,老年代使用CMS回收器
//输入
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
//输出
-XX:InitialHeapSize=33554432 -XX:MaxHeapSize=33554432 -XX:MaxNewSize=11190272 -XX:MaxTenuringThreshold=6 -XX:NewSize=11190272 -XX:OldSize=22364160 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC
8.G1收集器详解
- Garbage First 垃圾收集器是JDK7版本之后引入的一种垃圾回收器,jdk9中将G1变成默认的垃圾收集器
- 可以在不同的内存区域中分配垃圾回收的工作,提高了垃圾回收效率
- JDK11中查看默认垃圾收集器
-XX:+PrintCommandLineFlags
查看命令行相关参数(包含使用的垃圾收集器)
-XX:G1ConcRefinementThreads=9 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
核心原理
保留了分代思想,把内存划分为多个独立的区域Region,区域中包含逻辑上的年轻代、老年代区域
取消了年轻代、老年代的物理划分,不用单独对每个年代空间进行设置
Region的区域类型是动态变化的,可能之前是年轻代,经过了垃圾回收之后就变成了老年代,实现更加精细化的垃圾回收
整体采用标记整理算法, 局部是采用复制算法,不会产生内存碎片
把整个Java堆划分成约2048个独立Region块,每个Region块大小根据堆空间的大小而定,为2的N次幂,1MB~32MB
- 每个Region的大小可通过参数
-XX:G1HeapRegionSize
配置
- 每个Region的大小可通过参数
新增加一种叫Humongous内存区域,用于存储大对象
如果超过1.5个region,就是巨型对象,就放到H区,默认直接会被分配在老年代,一般被视为老年代.图中的H块
如果一个H区装不下一个巨型对象,G1会寻找连续的H区来存储,为了能找到连续的H区,有时需要启动Full GC
G1提供三种模式垃圾回收模式
- Young GC
- G1与之前垃圾收集器的
Young GC
不同,不是当新生代的Eden区放满了就进行垃圾回收 - G1会计算当前Eden区回收大概需要多久时间,如果接近参数
-XX:MaxGCPauseMills
设定的值,会触发Young GC - 回收过程也是将Eden区和Survivor区中的存活对象复制到空闲的Survivor区,并清空Eden区和原来的Survivor区。
- 如果Survivor区也满了,那么会将存活对象复制到Old区。在Young GC期间,应用程序会被暂停
- G1与之前垃圾收集器的
- Mixed GC
- 多数对象晋升到老年代old region时,为了避免堆内存被耗尽问题,会触发混合的GC
- 回收整个Young Region,还会回收一部分的Old Region区域,注意不是全部Old Region区域
- 触发条件
- 参数 -XX:InitiatingHeapOccupancyPercent=n 决定
- 默认:45%,即 当老年代大小占整个堆大小百分比达到该阀值时触发
- Full GC
- 单个线程会对整个堆的所有代中所有分区做标记、清除以及压缩动作,非常耗时
- 总结
- 在Young GC和Mixed GC中,G1垃圾收集器都会对每个Region的存活对象数量进行统计,
- 根据存活对象数量和空闲Region的数量,动态地决定垃圾收集的区域和顺序
- 这种动态的垃圾收集策略,可以避免Full GC的发生,提高了应用程序的响应速度
- Young GC
G1的MixGC垃圾收集分为下面几个步骤
初始标记(STW)
- 记录下GC Roots能直接引用的对象,并标记所有存活的对象,会执行一次年轻代GC,需要暂停所有线程,速度很快
并发标记
- 与应用线程一起工作,进行可达性分析
- g1收集器会对堆内存进行并发标记,找出所有存活的对象,并记录它们所在的Region
最终标记(STW)
- 修正并发标记期间, 部分因程序运行导致发生变化的那一部分对象,根据算法修复一些引用的状态
筛选回收(STW)
- 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间,即
-XX:MaxGCPauseMillis
制定计划 - 成本排序案例
- 现在有Region1、Region2和Region3三个区域
- Region1预计可以回收1.5MB内存,预计耗时2MS,投产比ROI=1.5/2
- Region2预计可以回收1MB内存,预计耗时1MS,投产比ROI=1/1
- Region3预计可以回收0.5MB内存,预计耗时1MS,投产比ROI=0.5/1
- 那Region1、Region2和Region3各自的回收价值与成本比值分别是:0.75、1和0.5,
- 比值越高说明同样的付出收益越高,如果此时只能回收一个Region的内存空间,G1就会选择Region2进行回收
- 保证了G1收集器在有限的时间内尽可能地提高收集效率
- 现在有Region1、Region2和Region3三个区域
- 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间,即
配置G1收集器的相关参数
- -XX:+UseG1GC
- 启用G1垃圾收集器。
- -XX:G1HeapRegionSize=n
- Java 堆大小划 分出约 2048 个区域,默认是堆内存的1/2000;配置需要为2的N次幂,1MB~32MB
- 使用G1垃圾回收器最小堆内存应为 1MB*2048=2GB ,低于这个的建议使用其它垃圾回收器。
- -XX:MaxGCPauseMillis=n
- 设置最大停顿时间,单位为毫秒,默认为200毫秒(JVM会尽力实现,但不能保证达到)
- -XX:ParallelGCThreads=n
- 设置 STW 工作线程数的值。一般设置为逻辑处理器的数量,最多为 8
- 是在STW阶段,并行执行【垃圾收集动作】的线程数
- -XX:ConcGCThreads=n
- 在【并发标记】阶段,并发执行标记的线程数,一般将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4
- -XX:InitiatingHeapOccupancyPercent=n
- 设置G1 Mix垃圾回收的触发阈值,默认为45%
- -XX:+UseG1GC
参数测试
//输入
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags
//输出
-XX:G1ConcRefinementThreads=9 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=549453824 -XX:MaxGCPauseMillis=100 -XX:MaxHeapSize=549453824 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
G1收集器应用场景
- 大型应用程序:可以将堆内存划分为多个区域,以实现更加精细化的垃圾回收。
- 高并发、低延迟:对响应时间要求较高的应用程序,如Web应用程序、电子商务等高并发场景
- 大内存应用:可以在垃圾回收过程中释放大量的空间,提高了内存的利用率。
使用G1垃圾收集器注意事项
不手工设置年轻代大小
- 比如使用 -Xmn 选项或 -XX:NewRatio 等设置年轻代大小
暂停时间的目标不要太小
- G1 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间
- 如果把停顿时间调得非常低, 如设置为10毫秒, 很可能出现的结果就是由于停顿目标时间太短
- 导致每次回收内存只占堆内存很小的一部分, 收集器收集的速度跟不上分配器分配的速度, 导致垃圾慢慢堆积
- 应用运行时间一长就占满堆引发Full GC反而降低性能, 通常把期望停顿时间设置为一两百毫秒是比较合理的
避免存活时间短的大对象
- G1垃圾收集器对程序的代码质量要求较高,需要对程序的内存使用情况进行精细化管理,对应用程序的代码进行优化和调整
9.ZGC收集器详解
- Z Garbage Collector 是Oracle公司开发一种可伸缩、低停顿时间的垃圾收集器,标记-复制算法(进行了改进)
- 垃圾回收过程几乎全部是并发,实际STW停顿时间极短,停顿时间控制10ms内,主要采用的染色指针和读屏障技术
- 在 JDK 11 中是实验性的特性引入,在 JDK 15 中 ZGC 可以正式投入生产使用,使用 –XX:+UseZGC 启用
- ZGC 的堆内存也是基于 Region 来分布,和G1类似,不区分新生代老年代的,Region 支持动态地创建和销毁,大小不是固定
- 三种类型的 Region
- 小型页面 Small Region:容量固定2MB,主要用于放置小于 256 KB 的小对象。
- 中型页面Medium Region:容量固定32MB,主要用于放置大于等于 256 KB 小于 4 MB 的对象。
- 大型页面Large Region
- 容量不固定 为N * 2MB, Region 是可以动态变化的,但必须是 2MB 的整数倍,最小支持 4 MB
特点
- 低停顿时间
- ZGC最大的特点是在不增加延迟的情况下,能够处理非常大的内存数据
- 可以将停顿时间限制在10ms以内,对于需要快速响应的应用程序来说是非常重要
- 可伸缩性
- 可以处理非常大的内存数据,适应不同规模的应用程序,从小型应用程序到大型企业级应用程序
- 不需要分代
- 不需要将内存分为新生代和老年代,不需要复杂的内存回收算法
- 并发处理
- 采用了并发处理的方式来进行垃圾回收可以在应用程序运行的同时进行垃圾回收
- 低停顿时间
工作流程
初始标记(STW):找 GC Roots 直接引用的对象,处理时间和GC Roots的数量成正比,停顿时间不随着堆的大小而增加。
并发标记(没有STW):扫描剩余的所有对象,处理时间比较长,业务线程与GC线程同时运行,但这个阶段会有漏标问题
再标记(STW):通过算法解决漏标对象,和G1中的解决漏标的算法类似
并发转移准备(没有STW) :分析最有回收价值GC分页,即ROI计算
初始转移(STW):转移初始标记的存活对象和做对象重定位,时间和GC Roots的数量成正比,时间不随堆的大小而增加。
并发转移(没有STW):对转移并发标记的存活对象做转移
平台支持说明
- 部分版本里面是实验性参数,需要加
-XX:+UnlockExperimentalVMOptions
才可以使用
- 部分版本里面是实验性参数,需要加
是否支持 | 平台 | 支持版本 |
---|---|---|
是 | Linux/x64 | JDK 15 (Experimental since JDK 11) |
是 | Linux/AArch64 | JDK 15 (Experimental since JDK 13) |
是 | macOS/x64 | JDK 15 (Experimental since JDK 14) |
是 | Windows/x64 | JDK 15 (Experimental since JDK 14) |
是 | Windows/AArch64 | JDK 16 |
是 | macOS/AArch64 | JDK 17 |
是 | Linux/PowerPC | JDK 18 |
- JDK17环境下验证参数
参数: -XX:+UseZGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
输出结果
-XX:InitialHeapSize=33554432 -XX:MaxHeapSize=33554432 -XX:MinHeapSize=33554432 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:-UseCompressedOops -XX:+UseZGC
总结:ZGC业界还没大规模使用,更多再实验性观望阶段,还存在变动和争议阶段,如果可能则预计26年~28年成为主流,当下我们开发的采用的垃圾收集器是G1收集器,23~25年会是主流。
- 点赞
- 收藏
- 关注作者
评论(0)