
文章目录1.JVM内存区域1.1 代码演示1.1.1 基本类型内存演变1.1.2 对象类型内存演变2.GC算法与过程2.1 GC Roots 与可达性分析2.2 回收算法2.2.1 标记清除2.2.2 标记整理2.2.3 复制算法2.3 经典分代 GC 过程3.垃圾收集器3.1 常见垃圾收集器3.2 Serial 收集器3.3 Parallel 收集器3.4 CMS 收集器3.4.1 CMS执行过程1.初始标记2.并发标记3.重新标记4.并发清除3.5 G1垃圾收集器3.5.1 Region3.5.2 为什么叫Garbage First3.5.3 G1的Young GC3.5.4 G1并发标记3.5.5 Mixed GC3.5.6 Full GC3.5.7 G1停顿时间目标3.6 ZGC垃圾收集器3.7 垃圾收集器如何选择3.8 通过代码观察GC3.8.1 指定垃圾收集器3.8.2 查看当前垃圾收集器3.8.3 JDK8查看GC日志3.8.4 JDK9及以上查看GC日志3.8.5 GC日志主要看什么3.9 常见垃圾收集器总结1.JVM内存区域网上随手抓的两张图下图为较早期 HotSpot JVM 内存结构示意其中混合了运行时数据区和具体垃圾收集器的堆结构仅用于建立整体概念。区域与代码对应关系图1.1 代码演示1.1.1 基本类型内存演变执行以下代码最后会输出10publicclassTestJvm{publicstaticvoidmain(String[]args){inta10;newTestJvm().add(a);System.out.println(a);}publicvoidadd(inta){intb10;System.out.println(ab);a11;}}演变过程1.1.2 对象类型内存演变publicclassTestJvm{staticIntegerc5;publicstaticvoidmain(String[]args){inta10;newTestJvm().add(a);System.out.println(a);}publicvoidadd(inta){intb10;PersonpersonnewPerson();person.id1;person.namelisi;System.out.println(ab);a11;}}classPerson{intid;Stringname;}测试引用类型参数传递本质仍是值传递实验 1修改对象内容实验 2修改形参指向2.GC算法与过程2.1 GC Roots 与可达性分析JVM 判断对象是否存活时通常采用可达性分析。从一组称为 GC Roots 的对象或引用出发向下搜索能够从 GC Roots 到达的对象认为仍然存活无法从任何 GC Roots 到达的对象才成为垃圾回收候选。常见 GC Roots 包括当前线程栈帧局部变量引用的对象、类静态字段引用的对象、JNI 引用以及 JVM 内部的一些引用等。2.2 回收算法常见的基础垃圾回收算法主要有三种标记-清除、标记-整理、复制算法。2.2.1 标记清除2.2.2 标记整理2.2.3 复制算法经典复制算法将内存划分为两个区域每次主要使用其中一个区域。GC 时只将存活对象复制到另一区域并连续排列随后原区域整体重新变为空闲。其优点是不产生内存碎片缺点是需要预留额外空间并存在对象复制开销。2.3 经典分代 GC 过程接下来以传统分代堆结构为例说明 Eden、Survivor 和 Old 之间的对象流转。不同垃圾收集器的实际堆布局和回收过程有所不同例如 G1 采用 Region 化管理并非固定连续的 Eden、S0、S1、Old 布局。实际的 GC 过程比较复杂不同垃圾收集器会综合运用复制、标记-整理等不同回收思想。在经典分代模型中Young GC 主要回收年轻代Full GC 通常涉及整个堆或更大范围的内存回收不能简单理解为“老年代 GC”。不同垃圾收集器还可能存在其他回收类型例如 G1 中的 Mixed GC。经典教学模型通常将年轻代画成 Eden、S0、S1并以 Eden:S0:S1≈8:1:1 帮助理解复制过程S0 和 S1 轮流作为 Survivor 区。需要注意这只是经典示意实际比例以及年轻代、老年代大小会受到 JVM 版本、垃圾收集器和运行时参数影响并不是固定不变的默认布局。初始空间如下图首次GC第二次GC对象每经过一次 Young GC 并继续存活年龄通常会增加。当达到晋升年龄阈值或满足其他晋升条件时会进入老年代。实际晋升年龄并不一定固定为某个数字。3.垃圾收集器前面介绍的标记-清除、标记-整理、复制算法属于垃圾回收的基础算法而垃圾收集器Garbage Collector则是这些算法在 JVM 中的具体实现。不同垃圾收集器追求的目标并不完全相同主要需要在以下几个方面进行权衡吞吐量Throughput程序运行过程中真正用于执行用户代码的时间占比。停顿时间Pause Time垃圾回收导致应用线程暂停的时间。内存占用Footprint垃圾收集器自身以及运行过程中需要消耗的额外内存。通常不存在一个垃圾收集器在所有场景下都是最优的需要根据具体业务进行选择。早期 HotSpot 中年轻代和老年代经常使用不同的垃圾收集器组合例如Serial Serial Old Parallel Scavenge Parallel Old ParNew CMS而现代常用的 G1、ZGC 等垃圾收集器内部结构已经更加复杂不再适合简单理解为“年轻代一个收集器 老年代一个收集器”。3.1 常见垃圾收集器垃圾收集器主要目标主要特点当前定位Serial简单、低额外开销单线程垃圾回收小堆、资源有限场景Parallel高吞吐量多线程并行回收吞吐量优先CMS低停顿并发标记清除历史重要JDK14 已移除G1吞吐量与停顿平衡Region、并发标记、增量回收现代 HotSpot 主流ZGC极低停顿大量 GC 工作与应用线程并发执行延迟敏感场景CMS 虽然已经被移除但历史上使用非常广泛而且仍然是 JVM 面试中比较常见的知识点因此这里仍然保留介绍。3.2 Serial 收集器Serial 是比较简单的垃圾收集器。发生垃圾回收时只使用一个 GC 线程执行垃圾回收并暂停其他应用线程。也就是会产生Stop-The-WorldSTW可以通过下面参数指定 Serial GC-XX:UseSerialGC简单理解应用线程──────┐ ┌────── │ 暂停 │ GC线程 └── GC ─────┘Serial 的主要特点单线程执行垃圾回收实现简单没有多 GC 线程之间的同步开销GC 时需要暂停应用线程因此 Serial 并不是简单理解为“性能差所以没人使用”。对于堆内存较小CPU 核心数较少小型工具程序对资源占用比较敏感等场景Serial 仍然可能是合适的选择。3.3 Parallel 收集器Parallel Collector 的核心目标是尽可能提高系统整体吞吐量。可以通过-XX:UseParallelGC启用。Serial GCGC线程1 ─────────────→Parallel GCGC线程1 ──────→ GC线程2 ──────→ GC线程3 ──────→ GC线程4 ──────→多个 GC 线程同时进行垃圾回收因此在多核 CPU 环境中能够更快完成一次 GC。但需要注意Parallel 虽然垃圾回收速度快但 GC 期间应用线程仍然需要暂停。因此它更关注整体吞吐量而不是单次 GC 停顿尽可能短适合的典型场景后台计算任务批处理大数据计算对吞吐量要求高对偶尔出现较长停顿不敏感3.4 CMS 收集器CMS 全称Concurrent Mark Sweep即并发标记清除垃圾收集器。CMS 的主要设计目标是尽可能降低垃圾回收导致的应用停顿时间。CMS 曾经是非常重要的低停顿垃圾收集器。需要注意JDK 9CMS 被标记为废弃 JDK 14CMS 正式被移除因此现代项目已经不会再选择 CMS但它对理解低停顿垃圾收集器以及 G1 的发展仍然很有帮助。3.4.1 CMS执行过程CMS 的主要过程可以简化为初始标记 ↓ 并发标记 ↓ 重新标记 ↓ 并发清除1.初始标记从 GC Roots 出发快速标记与 GC Roots 直接关联的对象。这一阶段需要 STW但由于处理的数据相对较少因此通常持续时间比较短。2.并发标记继续沿对象引用关系进行可达性分析。此时GC线程 → 进行对象标记 应用线程 → 继续运行两者可以同时进行。这也是 CMS 中Concurrent的主要体现。3.重新标记由于并发标记阶段应用线程仍然在继续运行对象之间的引用关系可能发生变化。因此需要再次暂停应用线程对并发标记阶段产生变化的引用关系进行修正。这一阶段需要 STW4.并发清除清除已经确定为垃圾的对象。这一阶段可以与应用线程并发执行。CMS 使用的是标记-清除Mark-Sweep思想因此一个明显的问题就是容易产生内存碎片。另外在并发标记和并发清除期间应用线程仍然会继续创建和修改对象。因此可能产生浮动垃圾Floating Garbage即本轮 GC 过程中刚刚变成垃圾但没有来得及被本轮 GC 回收的对象需要等待下一轮 GC。CMS 可以简单总结为优点 低停顿 缺点 并发阶段占用 CPU 可能产生浮动垃圾 标记-清除可能产生内存碎片3.5 G1垃圾收集器G1 全称Garbage First Garbage CollectorG1 是现代 HotSpot 中非常重要的垃圾收集器。从 JDK 9 开始G1 成为 Server 模式下默认的垃圾收集器。G1 的主要目标是在保持较高吞吐量的同时尽可能控制垃圾回收停顿时间。前面介绍经典分代模型时我们画的是Young Generation ┌───────────────┐ │ Eden │ S0 │ S1│ └───────────────┘ Old Generation ┌───────────────┐ │ Old │ └───────────────┘这种图很适合理解传统分代模型。但是 G1 的内存布局发生了比较大的变化。3.5.1 RegionG1 将整个 Java 堆划分为大量大小相同的Region可以简单理解为┌────┬────┬────┬────┬────┬────┐ │ E │ O │ S │ E │ O │ E │ ├────┼────┼────┼────┼────┼────┤ │ O │ E │ O │ S │ O │ E │ └────┴────┴────┴────┴────┴────┘其中E Eden Region S Survivor Region O Old Region另外还有用于存放大对象的Humongous Region所以需要特别注意G1 仍然存在年轻代和老年代的逻辑概念。只是这些区域物理内存上不要求连续也就是说传统理解 Eden | Survivor | Old通常画成几个连续的大块。而 G1 中Eden Region Survivor Region Old Region可以散布在整个堆中。3.5.2 为什么叫Garbage FirstG1 会统计不同 Region 中垃圾对象的数量 可回收空间 回收成本并优先选择垃圾较多、回收收益比较高的 Region。也就是Garbage First垃圾优先。因此 G1 不一定每次都把整个老年代全部处理一遍而是可以优先处理收益较高的一部分 Region。3.5.3 G1的Young GC正常情况下新对象主要分配在Eden RegionEden 空间不足时会触发Young GC存活对象可能Eden ↓ Survivor或者满足晋升条件后Eden / Survivor ↓ Old因此G1 虽然使用 Region 管理堆但对象的年轻代存活、年龄增加和晋升老年代等思想依然存在。3.5.4 G1并发标记随着 Old Region 越来越多G1 会在满足一定条件后启动并发标记。简化理解Young GC ↓ Concurrent Start ↓ 并发标记 ↓ Remark ↓ Cleanup其中大部分标记工作可以GC线程 应用线程并发运行从而减少长时间 STW。3.5.5 Mixed GC完成并发标记后G1 已经知道不同 Old Region 中大概有多少垃圾。随后可以进行Mixed GCMixed GC 与普通 Young GC 最大的区别是Young GC 主要回收年轻代 Region Mixed GC 年轻代 Region 部分 Old Region因此Mixed GC 可以同时回收年轻代和部分老年代。这也是 G1 非常重要的一个特点。3.5.6 Full GC如果正常的 Young GC、并发标记和 Mixed GC 无法及时腾出足够空间G1 可能退化为Full GCFull GC 通常涉及更大范围的堆内存回收并且会产生较长时间的 STW。所以需要注意Young GC ↓ Mixed GC属于 G1 正常运行过程中的重要组成部分。而Full GC更多可以理解为正常回收无法满足内存需求后的兜底手段。因此线上如果出现频繁 Full GC通常就值得进行进一步排查。3.5.7 G1停顿时间目标G1 可以设置期望的最大 GC 停顿目标-XX:MaxGCPauseMillis200默认目标通常为200ms但需要注意200ms 是 JVM 尽量达到的目标而不是绝对保证。G1 会根据之前的 GC 情况动态调整Young Generation 大小 本次回收多少 Region尽可能接近设定的停顿目标。因此-XX:MaxGCPauseMillis更准确的理解应该是GC 停顿时间期望值。而不是GC 一定不能超过这个时间。3.6 ZGC垃圾收集器ZGC 是面向极低停顿时间设计的垃圾收集器。可以通过-XX:UseZGC启用。ZGC 的一个重要特点是将大量耗时的垃圾回收工作与应用线程并发执行。因此能够尽可能缩短Stop-The-World时间。可以简单对比Parallel GC 重点 高吞吐量 G1 重点 吞吐量与停顿时间之间取得平衡 ZGC 重点 极低延迟ZGC 比较适合堆内存较大在线服务对响应时间非常敏感无法接受长时间 GC 停顿的场景。当然低延迟通常也需要付出一定 CPU、内存以及垃圾回收复杂度方面的代价。因此也不能理解成ZGC 一定比 G1 好最终仍然需要结合实际业务进行选择。3.7 垃圾收集器如何选择可以先通过下面的方式帮助记忆场景可以优先考虑堆较小、程序简单Serial追求高吞吐量Parallel普通服务端 Java 应用G1对低延迟要求非常高ZGCCMS历史学习、面试但生产环境中不能只根据这张表直接决定垃圾收集器。真正选择时还应该关注堆大小 CPU核心数 Young GC频率 Full GC频率 GC停顿时间 系统吞吐量 业务允许的最大延迟如果系统运行本身没有明显 GC 问题通常没有必要一开始就进行大量 JVM 参数调优。只有当 GC 指标明显不满足业务需求时再结合 GC 日志进行针对性优化。3.8 通过代码观察GC垃圾收集器只看概念比较抽象可以通过简单代码实际制造对象分配并观察 GC 日志。下面写一个简单测试importjava.util.ArrayList;importjava.util.List;publicclassGcTest{publicstaticvoidmain(String[]args)throwsException{Listbyte[]listnewArrayList();for(inti0;i200;i){// 每次分配约256KB对象list.add(newbyte[256*1024]);// 保留一部分对象后再释放引用if(list.size()20){list.clear();}Thread.sleep(20);}}}这里不断创建byte[]对象。为了更容易触发 GC可以把 Java 堆设置得小一些。例如-Xms32m -Xmx32m3.8.1 指定垃圾收集器Serial-XX:UseSerialGCParallel-XX:UseParallelGCG1-XX:UseG1GCZGC-XX:UseZGC不同 JDK 版本支持的垃圾收集器有所不同。例如 CMS 在现代 JDK 中已经不存在因此-XX:UseConcMarkSweepGC只适用于历史版本。3.8.2 查看当前垃圾收集器可以执行java-XX:PrintCommandLineFlags-version观察 JVM 最终使用的参数。如果看到-XX:UseG1GC就说明当前使用的是 G1。3.8.3 JDK8查看GC日志因为本文前面的部分测试使用的是 JDK8所以如果继续使用 JDK8可以添加-XX:PrintGCDetails -XX:PrintGCTimeStamps例如-Xms32m -Xmx32m -XX:UseParallelGC -XX:PrintGCDetails -XX:PrintGCTimeStamps运行后可以在控制台看到 GC 信息。3.8.4 JDK9及以上查看GC日志从 JDK9 开始JVM 使用统一日志系统。可以使用-Xlog:gc*例如java-Xms32m-Xmx32m-XX:UseG1GC-Xlog:gc* GcTest运行后可能看到Pause Young Concurrent Mark Remark Cleanup等 GC 信息。3.8.5 GC日志主要看什么刚开始学习 GC 日志时不需要马上分析每一个字段。可以先重点关注发生了什么类型的GC GC前占用了多少内存 GC后剩余多少内存 堆总容量 本次GC耗时 Young GC是否过于频繁 Full GC是否频繁例如看到100M - 30M (256M)可以简单理解为GC前 100MB GC后 30MB 当前堆容量 256MB如果线上出现Young GC非常频繁 Full GC频繁 单次GC停顿突然很长 GC完成后内存仍然降不下来就需要进一步排查对象创建速度是否过快 是否存在大量长期存活对象 是否存在内存泄漏 堆大小是否设置合理 是否存在大量大对象 垃圾收集器是否适合当前业务后续可以再结合jstat jmap jstack Arthas GC日志分析工具进一步分析 JVM 状态。3.9 常见垃圾收集器总结收集器GC方式主要目标主要特点Serial单线程简单小堆、资源较少Parallel多线程并行高吞吐量GC速度快但停顿可能较长CMS并发低停顿历史收集器会产生碎片G1并行 并发平衡吞吐量和停顿Region、Young GC、Mixed GCZGC高度并发极低延迟大部分GC工作与应用线程并发可以简单记忆Serial ↓ 简单 Parallel ↓ 吞吐量 CMS ↓ 低停顿历史 G1 ↓ 吞吐量 停顿平衡 ZGC ↓ 极低延迟实际开发中最重要的并不是把每一个垃圾收集器的所有阶段全部背下来而是首先理解为什么要有不同垃圾收集器 不同收集器解决什么问题 吞吐量和停顿时间之间如何权衡 Young GC、Mixed GC、Full GC分别代表什么 发生频繁Full GC时应该如何排查理解这些核心问题以后再继续深入垃圾收集器的具体实现会容易很多。