【JVM】垃圾回收 你这段整体思路是对的已经很像面试回答了。可以按“什么是垃圾 → 怎么找垃圾 → 怎么回收垃圾 → JVM 为什么分代 → 常见垃圾回收器 → GC 调优”这个顺序理解。我帮你重新解释一遍并顺便指出几个容易说错的点。一、什么是垃圾在 Java 中垃圾对象指的是程序后续不可能再使用到的对象。比如UserusernewUser();usernull;原来的new User()对象已经没有变量引用它了后续无法再访问它就可能成为垃圾。二、怎么判断一个对象是不是垃圾1. 引用计数法早期思想是每个对象维护一个引用计数器。UserusernewUser();这个对象被user引用引用计数 1。usernull;引用断开引用计数 -1。如果引用计数变成 0就认为对象是垃圾。但是它有一个大问题循环引用。classA{Bb;}classB{Aa;}假设AanewA();BbnewB();a.bb;b.aa;anull;bnull;虽然外部已经无法访问 A 和 B 了但 A 和 B 彼此引用引用计数都不是 0。所以引用计数法会误以为它们不是垃圾从而无法回收造成内存泄漏。2. 根可达性分析算法Java 主流 JVM 使用的是GC Roots 可达性分析算法。它的核心思想是从一组特殊对象 GC Roots 出发沿着引用链往下找能找到的对象就是存活对象找不到的对象就是垃圾对象。可以理解成这样GC Roots | v 对象 A | v 对象 BA、B 能从 GC Roots 访问到所以它们不是垃圾。如果某个对象完全不在这条引用链上就说明程序已经没办法访问它了它就是垃圾。常见的 GC Roots 有1. 虚拟机栈中引用的对象 2. 方法区中静态变量引用的对象 3. 方法区中常量引用的对象 4. 本地方法栈 JNI 引用的对象 5. 正在运行的线程对象比如publicstaticUserstaticUsernewUser();publicvoidtest(){UserlocalUsernewUser();}staticUser引用的对象可以作为静态变量关联对象存活。localUser引用的对象在方法执行期间也可以通过虚拟机栈中的局部变量表找到所以暂时不是垃圾。三、垃圾回收算法垃圾找出来之后JVM 要考虑怎么回收。常见算法主要有三类。1. 标记-清除算法流程第一步标记存活对象或垃圾对象 第二步清除垃圾对象示意回收前 [存活][垃圾][存活][垃圾][存活] 回收后 [存活][空闲][存活][空闲][存活]优点实现相对简单 不用移动对象 回收速度相对较快缺点会产生内存碎片所谓内存碎片就是虽然总的空闲空间够但不连续。比如现在有三个空闲块2MB 3MB 4MB 9MB但是如果你要分配一个 6MB 的大对象可能找不到连续空间还是会触发 GC甚至 OOM。所以你说的“会产生内存碎片造成内存浪费和溢出”是对的。2. 标记-整理算法流程第一步标记存活对象 第二步把存活对象向一端移动 第三步清理边界外的垃圾示意回收前 [存活][垃圾][存活][垃圾][存活] 整理后 [存活][存活][存活][空闲][空闲]优点不会产生内存碎片 适合老年代缺点需要移动对象 效率比标记-清除低因为对象移动之后对象的引用地址也需要更新成本比较高。3. 复制算法流程把内存分成两块每次只使用其中一块。From 区正在使用 To 区空闲GC 时把存活对象复制到 To 区然后清空 From 区。示意From 区 [存活][垃圾][垃圾][存活] 复制到 To 区 [存活][存活][空闲][空闲] 然后 From 区整体清空优点不会产生内存碎片 回收效率高缺点浪费一部分内存空间 如果存活对象很多复制成本会很高所以复制算法非常适合年轻代。因为年轻代对象大多是“朝生夕死”垃圾多存活对象少需要复制的对象少效率就很高。四、为什么要分代垃圾回收Java 堆中的对象生命周期差异很大。有些对象newUser();可能方法执行完就没用了。有些对象比如缓存、单例、Spring Bean可能会存活很久。所以 JVM 使用了分代思想年轻代存放生命周期短的对象 老年代存放生命周期长的对象这样可以做到年轻代频繁回收 老年代较少回收 不同区域使用不同回收算法五、年轻代和老年代怎么回收年轻代年轻代通常分为Eden 区 Survivor From 区 Survivor To 区大多数新对象会先分配到 Eden 区。当 Eden 区满了会触发 Minor GC。Minor GC 时Eden 和 From 中存活的对象复制到 To 区 然后清空 Eden 和 From From 和 To 角色互换对象每熬过一次 Minor GC年龄通常会 1。当对象年龄达到一定阈值比如默认最大年龄 15 左右就可能晋升到老年代。所以年轻代一般使用复制算法。老年代老年代对象存活时间长回收频率低。如果老年代也用复制算法就会很亏因为老年代存活对象多复制成本高。所以老年代通常使用标记-清除 标记-整理 或者两者结合六、CMS 垃圾回收器CMS 全称是Concurrent Mark Sweep。它主要是针对老年代的垃圾回收器。它的目标是尽量减少 STW 停顿时间追求低延迟。STW 是 Stop The World意思是垃圾回收时暂停所有用户线程。CMS 的大致过程1. 初始标记 2. 并发标记 3. 重新标记 4. 并发清除其中初始标记、重新标记需要 STW 并发标记、并发清除可以和用户线程一起执行所以 CMS 停顿时间比较短。但是 CMS 有几个问题1. 使用标记-清除算法会产生内存碎片 2. 并发执行时会占用 CPU 资源 3. 并发清理过程中用户线程还在运行可能产生浮动垃圾你原文中说CMS 采用标记清除算法优点是速度快并发标记和并发清理允许用户线程和 GC 线程同时工作。这个说法是可以的。不过要补充一句CMS 的缺点是容易产生内存碎片所以后来逐渐被 G1 替代。七、G1 垃圾回收器G1 全称是Garbage First。它是一个面向服务端应用的垃圾回收器特点是可以设置期望的最大停顿时间并尽量在这个时间内完成 GC。比如-XX:MaxGCPauseMillis200意思是希望每次 GC 停顿尽量控制在 200ms 以内。注意这是目标不是绝对保证。G1 的核心思想传统分代是物理连续的年轻代一整块 老年代一整块G1 把整个堆切成很多大小相等的 RegionRegion 1 Region 2 Region 3 Region 4 ...每个 Region 可以扮演不同角色Eden Region Survivor Region Old Region Humongous RegionG1 会统计每个 Region 的垃圾比例和回收收益。回收时优先回收垃圾最多、收益最高的 Region。这就是 Garbage First 的含义垃圾最多的区域优先回收。G1 为什么能控制停顿时间因为它不是每次都回收整个老年代而是挑一部分 Region 回收。比如它估算回收 20 个 Region 可能需要 180ms 回收 30 个 Region 可能需要 260ms如果目标停顿时间是 200ms它就可能只回收 20 个 Region。所以 G1 可以做到相对可预测的停顿时间。八、GC 调优怎么说GC 调优本质上不是盲目调参数而是根据业务场景在吞吐量、延迟、内存占用之间做权衡。常见目标有两个1. 吞吐量优先适合后台任务、批处理、计算任务。比如任务可以接受短暂停顿 但希望整体处理速度快这种场景可以考虑 Parallel GC。JDK 8 默认常见就是 Parallel GC。2. 低延迟优先适合接口服务、交易系统、实时系统。这种场景希望单次 GC 停顿尽量短 请求响应时间稳定JDK 8 可以考虑 CMS 或 G1。更现代的 JDK 里可以考虑 G1、ZGC、Shenandoah。但如果你面试说 JDK 8重点说 CMS 和 G1 就够了。九、你这段里有一个点需要修正你原文说JDK7 到 JDK8 的变化是把方法区的实现从堆内存移到了本地内存。这个大方向是对的但表达可以更准确方法区是 JVM 规范中的概念。JDK 7 及以前HotSpot 使用永久代 PermGen 来实现方法区JDK 8 移除了永久代改用元空间 Metaspace 实现方法区而元空间使用的是本地内存。也就是说方法区规范概念 永久代JDK 8 之前 HotSpot 对方法区的实现 元空间JDK 8 之后 HotSpot 对方法区的实现为什么要移除永久代因为永久代大小受 JVM 参数限制容易出现java.lang.OutOfMemoryError: PermGen space尤其是大量动态生成类、频繁部署应用、反射、代理、框架较多的场景。JDK 8 之后类元信息放到本地内存中的 Metaspace默认情况下会根据系统内存动态扩展所以更不容易因为类元信息太多导致永久代 OOM。但注意元空间也不是无限的也可以通过参数限制-XX:MaxMetaspaceSize十、逃逸分析和栈上分配为什么能减轻 GC 压力正常情况下对象大多分配在堆上。堆上的对象需要 GC 管理。但是 JVM 可以通过逃逸分析判断这个对象会不会逃出当前方法或当前线程如果对象没有逃逸JIT 编译器可能会进行优化比如栈上分配 标量替换 锁消除比如publicvoidtest(){UserusernewUser();user.nameTom;user.age18;}如果user没有返回也没有赋值给外部变量也没有被其他线程访问那么 JVM 可能认为它没有逃逸。这时 JVM 可能不真的在堆上创建完整对象而是把它拆成几个局部变量name Tom age 18这样对象都没进堆自然就减少了 GC 压力。不过面试里说的时候最好加一句逃逸分析是一种 JVM 优化手段不代表所有未逃逸对象一定会栈上分配具体要看 JIT 优化结果。面试版回答可以这样说你这段可以整理成下面这版Java 的垃圾回收主要解决两个问题第一判断哪些对象是垃圾第二如何回收这些垃圾对象。判断垃圾对象时早期有引用计数法但是引用计数法解决不了循环引用问题所以 Java 主流 JVM 使用的是 GC Roots 可达性分析算法。从 GC Roots 出发沿着引用链能够访问到的对象就是存活对象访问不到的对象就是垃圾对象。常见的 GC Roots 包括虚拟机栈中的引用、静态变量引用、常量引用、本地方法栈 JNI 引用等。找到垃圾之后常见回收算法有三种标记-清除、标记-整理和复制算法。标记-清除效率较高但会产生内存碎片标记-整理不会产生碎片但需要移动对象成本较高复制算法效率高、没有碎片但会浪费一部分空间适合存活对象少的区域。因为 Java 对象生命周期不同所以 JVM 采用分代回收思想。年轻代对象大多朝生夕死适合使用复制算法老年代对象存活时间长适合使用标记-清除或标记-整理算法。常见垃圾回收器中CMS 是针对老年代的低停顿垃圾回收器采用标记-清除算法并且在并发标记、并发清除阶段可以和用户线程一起执行因此停顿时间较短但缺点是会产生内存碎片。G1 是面向整个堆的垃圾回收器它把堆划分成多个 Region优先回收垃圾收益高的 Region并且可以设置期望的最大停顿时间适合大堆内存、低延迟场景。GC 调优方面通常会根据业务场景选择合适的垃圾回收器。如果追求吞吐量可以考虑 Parallel GC如果追求低延迟可以考虑 CMS、G1现代 JDK 还可以考虑 ZGC。除此之外还可以通过合理设置堆大小、年轻代比例、排查频繁 Full GC、减少对象创建、利用逃逸分析等方式降低 GC 压力。你原来的内容已经比较完整了。最需要注意的是这三点1. G1 的停顿时间是“尽量达到目标”不是绝对保证。 2. JDK8 不是“方法区没了”而是“方法区的实现从永久代变成元空间”。 3. 逃逸分析不等于一定栈上分配更常见的优化还有标量替换。