
前面两篇我们学习了 JVM 运行时数据区、堆的分代模型。我们知道堆是存放对象的主战场也是 GC 工作的核心区域。 C/C 需要手动malloc申请内存、free释放内存一旦忘记释放就会造成内存泄漏。Java 最大的优势就是自带自动垃圾回收机制由 JVM 帮我们识别无效对象、回收内存。但是 GC 并不是一个黑盒面试非常爱问JVM 凭什么知道对象已经没用有哪些 GC 算法各个收集器有什么差别并发标记的时候业务线程还在跑怎么防止误回收存活对象这一篇我们从底层原理一步步拆解 GC是 JVM 调优和面试最重要的一环。回顾前置知识点 GC 主要回收Java 堆方法区元空间GC 条件苛刻只有 FullGC 才会尝试卸载类虚拟机栈、本地方法栈、程序计数器属于线程私有线程结束直接释放不需要 GC。一、GC 基础概念1.1 什么是 GCGC 全称Garbage Collection垃圾回收。 简单一句话JVM 自动识别堆中不再被使用的对象释放它占用的堆内存。1.2 STWStop-The-World世界暂停【重中之重】几乎所有 GC 都会触发 STW。 含义GC 工作线程开始执行回收时除 GC 自身线程以外所有业务用户线程全部暂停不执行任何业务代码GC 完成之后业务线程才恢复运行。STW 会带来业务停顿接口响应变长这也是 GC 调优的核心目标尽量降低 STW 的停顿时长减少对业务的影响。评价 GC 收集器两大核心指标吞吐量吞吐量 业务代码执行时间 /业务执行时间 GC 耗时 吞吐量越高代表 CPU 更多时间在执行业务而不是 GC。Parallel 系列收集器就是优先追求高吞吐量。停顿时间单次 GC 造成业务线程暂停的时长。CMS、G1、ZGC 这类收集器优先追求低停顿适合互联网对响应时间敏感的业务。注意吞吐量和低停顿天然存在制衡很难同时做到极致。想要更低停顿往往要牺牲一部分吞吐量。二、如何判断对象可以被回收GC 第一件事区分哪些对象还在使用存活哪些对象已经没有引用垃圾。有两种经典算法引用计数法、可达性分析算法。HotSpot 采用可达性分析算法。2.1 引用计数法原理给每一个对象维护一个引用计数器有引用指向这个对象计数器 1引用断开、赋值为 null计数器 - 1计数器 0判定为垃圾可以回收优点实现简单回收即时Python、OC 等语言使用。致命缺陷无法解决循环引用问题循环引用演示代码class Node { Node next; } public class RefCycleDemo { public static void main(String[] args) { Node n1 new Node(); Node n2 new Node(); n1.next n2; n2.next n1; // 切断外部引用 n1 null; n2 null; // n1和n2互相引用计数器都不为0引用计数算法识别不出这是垃圾 } }这就是 HotSpot 不选用引用计数法的根本原因。2.2 可达性分析算法HotSpot 使用核心思路定义一组起始根节点叫做GC-Root。从 GC-Root 出发沿着引用链遍历能遍历到达的对象存活对象不能回收遍历不到、引用链断裂的对象垃圾对象可以回收循环引用的对象只要没有 GC-Root 可达就会被判定为垃圾正常回收。必背哪些可以作为 GC-Root虚拟机栈里面栈帧局部变量表引用的对象方法内局部变量方法区中 static 静态变量引用的对象正在运行的活跃 Java 线程JNI 本地方法栈中 native 引用的对象举个例子public void test(){ User u new User(); }u在虚拟机栈局部变量表是 GC-Roottest 执行结束栈帧出栈这个引用消失User 对象失去 GC-Root 链路满足回收条件。三、三大基础 GC 算法算法是理论思想收集器是算法在 JVM 中的工程实现。3.1 标记 - 清除 Mark-Sweep两个阶段标记、清除标记从 GC-Root 遍历标记所有存活对象清除遍历堆释放所有未标记的垃圾对象优点逻辑简单缺点需要两次扫描堆效率一般产生大量内存碎片堆空闲内存总量足够但没有连续空间存放大对象依然会 OOM应用CMS 老年代底层就是标记清除算法3.2 复制算法 Copying原理将内存分成两块只使用其中一块GC 时把这块内存里存活对象复制到另一块空闲区域然后直接清空原来这块内存。优点复制完成后内存连续规整没有内存碎片分配对象速度快缺点理论上浪费一半内存如果对象存活率很高复制大量对象开销巨大HotSpot 新生代巧妙使用 Eden 两块 Survivor8:1:1规避 50% 内存浪费新生代对象朝生夕死、存活率低非常适合复制算法。3.3 标记 - 整理 Mark-Compact标记标记全部存活对象整理把存活对象全部向内存一侧移动、压缩边界以外的空间直接回收优点没有内存碎片不需要预留双倍内存缺点移动对象并且修改所有指向这个对象的引用地址对象存活率高的时候开销很大应用Serial Old、Parallel Old 老年代收集器3.4 分代收集算法分代并不是一种全新算法是上面三种算法的组合策略现代 JVM 的核心思想。 依据对象存活时长把堆分成新生代、老年代不同区域选用最合适算法新生代对象短命、存活率低 →复制算法老年代对象长寿、存活率高 →标记清除 / 标记整理四、Java 的四种引用面试高频Java 把引用划分成 4 个强度等级用来灵活控制对象什么时候被 GC 回收。4.1 强引用 StrongReference我们日常写代码默认都是强引用。只要强引用链存在GC 永远不会回收对象。User user new User();只有user null切断强引用对象才具备回收资格。4.2 软引用 SoftReference特点内存快要 OOM 之前才会回收软引用指向的对象内存充足就保留。 适合做内存敏感型缓存内存够就留着加速访问内存紧张自动释放。SoftReferenceUser softRef new SoftReference(new User()); User user softRef.get(); // 内存充足返回对象OOM前回收后返回null典型场景本地缓存框架。4.3 弱引用 WeakReference特点只要发生 GC不管内存够不够都会回收仅被弱引用关联的对象。WeakReferenceUser weakRef new WeakReference(new User()); User user weakRef.get();典型场景WeakHashMap比如 ThreadLocal 里面的 key 就是弱引用。4.4 虚引用 PhantomReference幻影引用特点调用 get () 永远返回 null拿不到对象本身。 唯一作用对象被 GC 回收的时候收到通知用来做资源后置清理。 必须配合ReferenceQueue引用队列使用。ReferenceQueueUser queue new ReferenceQueue(); PhantomReferenceUser phantomRef new PhantomReference(new User(), queue);典型场景NIO 堆外直接内存的回收监控。汇总表格表格引用类型回收时机典型场景强引用不会主动回收普通业务对象软引用OOM 之前回收内存敏感缓存弱引用只要 GC 就回收WeakHashMap、ThreadLocal key虚引用对象回收时入队列通知堆外内存资源监控五、主流垃圾收集器版本基线 JDK8 默认Parallel Scavenge Parallel Old JDK9G1 成为默认收集器 JDK14正式移除 CMS JDK15ZGC 转正5.1 Serial Serial Old串行收集器Serial新生代复制算法单线程 GCSerial Old老年代标记整理单线程 GC 全程 STW只有一个 GC 线程工作适合资源有限的客户端程序。 启动参数-XX:UseSerialGC5.2 ParNew新生代收集器Serial 的多线程版本复制算法。唯一能够和 CMS 老年代搭配的新生代收集器。 参数-XX:UseParNewGC、-XX:ParallelGCThreads指定 GC 线程数。5.3 Parallel Scavenge Parallel Old并行收集器JDK8 默认目标优先高吞吐量Parallel Scavenge新生代复制算法多线程回收Parallel Old老年代标记整理多线程回收 适合后台计算型任务、批处理服务优先榨干 CPU。 参数-XX:UseParallelGC区分并行和并发 并行多个 GC 线程一起工作业务线程全部暂停STW 并发GC 线程和业务用户线程同时运行5.4 CMS Concurrent Mark Sweep 并发标记清除定位老年代收集器追求低停顿底层标记清除算法JDK14 已经删除。一般搭配 ParNew 新生代。 CMS 四个阶段初始标记STW只标记 GC-Root 直接相连的对象速度很快并发标记GC 线程和业务线程并发运行遍历完整引用链重新标记STW修正并发阶段业务线程修改引用带来的标记错误并发清除GC 线程和业务线程并发清除垃圾优点大部分阶段并发停顿短缺点标记清除产生内存碎片并发阶段占用 CPU吞吐量下降需要预留一部分老年代空间预留不足会触发并发模式失败退化到 Serial Old长时间 FullGCCMS 解决并发标记漏标方案增量更新写屏障5.5 G1 Garbage-FirstJDK9 之后默认收集器用来替代 CMS。 核心设计把整个 Java 堆切分成很多大小相等的 Region1~32M2 的幂。Region 角色分为 Eden、Survivor、Old、Humongous 巨型对象区。Humongous 区对象大小超过 Region 一半直接放这里不走 Eden 分配。G1 四大特点支持并行、并发多核充分利用保留分代思想但新生代、老年代不再是连续整块是一批 Region 集合空间整合整体标记整理、局部复制几乎没有内存碎片可预测停顿用户设置预期最大停顿时间-XX:MaxGCPauseMillis200后台统计每个 Region 回收收益优先回收收益最高的 Region尽量满足停顿目标G1 回收四阶段初始标记STW标记 GC-Root 直达对象并发标记和业务线程并发做可达性分析最终标记STW处理 Remembered Set 日志筛选回收STW按收益排序选定一批 Region复制存活对象回收G1 解决漏标方案SATB 原始快照写屏障会产生浮动垃圾放到下一轮 GC 清理G1 常用参数-XX:UseG1GC -XX:G1HeapRegionSize4M -XX:MaxGCPauseMillis2005.6 ZGCJDK11 实验JDK15 转正。目标是亚毫秒级 STW 停顿支持 TB 级超大堆牺牲少量吞吐量换取极低延迟。 核心技术染色指针把标记信息直接存放在对象指针里不用频繁扫描对象头。适合超大堆、延迟极其敏感的业务。六、三色标记并发标记核心CMS、G1 在并发标记阶段业务线程还在跑不能全程 STW依靠三色标记完成可达性分析。 三种颜色代表对象扫描状态白色还没有扫描初始全部白色本轮结束白色就是垃圾灰色已经访问该对象但是它引用的子对象还没扫描完黑色对象本身 它所有引用的子对象全部扫描完毕并发标记带来两个经典问题浮动垃圾某个对象标记完成变黑之后业务代码切断它所有引用。本轮 GC 已经扫描完毕无法识别只能等到下一轮 GC 回收。浮动垃圾影响不大。漏标致命同时满足两个条件 ① 黑色对象新增引用指向白色对象 ② 灰色对象删除对这个白色对象的引用 结果白色对象仍然存活但是被当成垃圾回收引发空指针异常。两种解决方案CMS增量更新黑色对象新增白色引用时记录这个引用重新标记阶段再扫描破坏条件①G1SATB 原始快照引用删除的时候把原来被引用对象记录下来当作存活对象破坏条件②代价是产生浮动垃圾七、收集器选型总结客户端、小应用SerialJDK8 批处理、计算密集、追求高吞吐ParallelJDK8 延迟敏感业务ParNew CMS注意 JDK14 之后无 CMSJDK9 绝大多数线上业务G1平衡吞吐和停顿首选TB 级大堆、极致低延迟ZGC没有万能收集器一切以业务需求、JDK 版本、堆大小为基准。八、本篇面试真题汇总引用计数法的缺点可达性分析的 GC-Root 有哪些标记清除、复制、标记整理算法各自优缺点适用场景强、软、弱、虚引用区别和使用场景CMS 的四个阶段优缺点并发模式失败是什么G1 的 Region、Humongous 对象是什么可预测停顿怎么实现STW 是什么吞吐量、停顿时间的含义三色标记黑白灰含义浮动垃圾和漏标是什么增量更新和 SATB 分别是哪个收集器使用解决什么问题Parallel、CMS、G1、ZGC 怎么选型小贴士理解 GC 算法和收集器才能看懂 JVM 调优参数。下一篇进入实战线上故障排查CPU 飙高、死锁、OOMJDK 自带工具 Arthas 全套实战流程。