《Java 面试八股精讲》系列第 8/14 篇。本系列是我对照开源项目 JavaGuide(https://github.com/Snailclimb/JavaGuide)复习时亲手整理的面试笔记,力求把高频考点压成“能背、能讲、能画”的密度。如有错漏,欢迎评论区指出。
内存分配与回收原则
- 对象优先在 Eden 区分配:大多数情况下,对象在新生代 Eden 区分配
- 大对象直接进入老年代:大对象是需要大量连续内存空间的对象(如长字符串、大数组)
- 长期存活的对象将进入老年代
死亡对象判断方法
引用计数法
给对象添加一个引用计数器:每当有一个地方引用它,计数器加 1;引用失效,计数器减 1;计数器为 0 的对象就是不可能再被使用的。
⚠️ 这个方法实现简单,但主流 JVM 并没有选择它,重要原因之一是它无法单独解决对象之间循环引用的问题。
可达性分析算法
基本思想:通过一系列称为“GC Roots”的对象作为起点向下搜索,走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,证明此对象不可用,需要被回收。
四种引用类型
| 引用类型 | 类比 | 回收时机 | 特点 |
|---|---|---|---|
| 强引用 | 必不可少的生活用品 | 内存不足宁抛 OOM 也不回收 | 最普遍,如String s = new String("abc") |
| 软引用 | 可有可无的生活用品 | 内存压力大时可能回收;抛 OOM 前一定清理完所有仅软可达对象 | 适合内存敏感的高速缓存 |
| 弱引用 | 更脆弱的用品 | 下次 GC 时回收(不保证立刻) | 生命周期比软引用更短暂 |
| 虚引用 | 形同虚设 | 不阻止回收,get()恒返回null | 必须配合引用队列,用于接收可达性变化通知、安排清理工作 |
废弃常量与无用类的判定
废弃常量:假如字符串常量池中存在 “abc”,当前没有任何 String 对象引用它,它就是废弃常量。发生内存回收且有必要时,“abc” 会被清理出常量池。
无用的类(需同时满足三个条件):
- 该类所有实例都已被回收,Java 堆中不存在该类的任何实例
- 加载该类的
ClassLoader已被回收 - 该类对应的
java.lang.Class对象没有任何地方被引用,无法通过反射访问该类的方法
垃圾收集算法
| 算法 | 原理 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 标记-清除 | 标记存活对象,统一回收未标记对象 | 最基础 | 效率低;产生大量内存碎片 | — |
| 复制 | 内存分两块,用完一块就把存活对象复制到另一块,一次清理掉旧块 | 无碎片、高效 | 可用内存减半;存活对象多时复制性能差 | 新生代 |
| 标记-整理 | 标记后让存活对象向一端移动,清理端边界外内存 | 无碎片 | 多了整理步骤,效率不高 | 老年代 |
| 分代收集 | 按对象存活周期分代,各代选合适算法 | 因地制宜 | 并非所有收集器都分代(早期 ZGC 非分代) | HotSpot 经典设计 |
延伸面试问题:HotSpot 为什么要分新生代和老年代?
本质上是让垃圾回收算法与对象的生命周期特征相匹配:
- 新生代= 高死亡率 + 低存活率 →标记-复制算法(高效清理大量垃圾)
- 老年代= 低死亡率 + 高存活率 →标记-整理算法(稳定管理长期存活对象)
这种"因地制宜"的策略,使 HotSpot 能在保证回收正确性的前提下,最大化平衡吞吐量与暂停时间。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。
| 收集器 | 线程模式 | 特点 | 适用目标 |
|---|---|---|---|
| Serial | 单线程 | 最基础最悠久,GC 时暂停所有工作线程(Stop The World) | 客户端模式、小内存 |
| ParNew | 多线程 | Serial 的多线程版本,其余行为与 Serial 完全一样 | 低延迟,CMS 的最佳搭档 |
| Parallel Scavenge | 多线程 | 标记-复制算法,"唯吞吐量论"收集器 | 高吞吐量 |
| CMS | 并发 | 第一款真正意义上的并发收集器,目标最短停顿 | 注重用户体验的应用 |
| G1 | 并行+并发 | 面向服务器,大内存多核,可预测停顿(软目标) | 兼顾停顿与吞吐 |
| ZGC | 并发 | 停顿控制在几毫秒内,不受堆大小影响,最大支持 16TB | 超大堆、极低延迟 |
CMS 收集器(重点)
CMS(Concurrent Mark Sweep)是以获取最短回收停顿时间为目标的收集器,是 HotSpot 第一款真正意义上的并发收集器——第一次实现了垃圾收集线程与用户线程(基本上)同时工作。
四个步骤:
- 初始标记:短暂停顿(STW),标记直接与 GC Roots 相连的对象
- 并发标记:GC 线程与用户线程同时运行,用闭包结构记录可达对象。因用户线程可能不断更新引用域,无法保证实时性,会跟踪记录发生引用更新的地方
- 重新标记:修正并发标记期间因用户程序运行导致标记变动的那部分记录。停顿一般比初始标记稍长,远短于并发标记
- 并发清除:开启用户线程,同时 GC 线程对未标记区域做清扫
优点:并发收集、低停顿。三个明显缺点:
- 对 CPU 资源敏感
- 无法处理浮动垃圾
- 使用的"标记-清除"算法会产生大量空间碎片
G1 收集器(重点)
G1(Garbage-First)是面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
四个特点:
- 并行与并发:充分利用多核硬件优势缩短 STW;部分原本需要停顿 Java 线程的 GC 动作,G1 可并发执行
- 分代收集:可独立管理整个 GC 堆,仍保留分代概念
- 空间整合:整体基于"标记-整理",局部基于"标记-复制",无碎片
- 可预测的停顿:根据用户设置的停顿目标建立预测模型并选择回收集合(软目标,不保证每次都不超标)
运作步骤:
- 初始标记:短暂 STW,标记 GC Roots 直接可达的对象
- 并发标记:与应用并发运行,标记所有可达对象,可能持续较长时间
- 最终标记:短暂 STW,处理并发标记结束后残留的少量引用变更
- 筛选回收:选择回收价值高的区域,复制存活对象到新区域,回收旧区域(含一个或多个 STW)
ZGC 收集器
与 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,但做了重大改进:暂停时间可控制在几毫秒以内,且不受堆内存大小影响,STW 出现更少;代价是牺牲一些吞吐量。ZGC 最大支持16TB堆内存。
《Java 面试八股精讲》系列目录(加粗为本篇):
- Java 基础(一):JDK、JRE、JVM、JIT 与 AOT
- Java 基础(二):equals、hashCode 与 String
- Java 基础(三):异常、反射、代理与序列化
- Java 基础(四):IO 模型、泛型擦除与值传递
- 集合框架:List、Set、Queue、Map 全景
- JVM(一):内存区域与对象创建
- JVM(二):Class 文件与类加载
8. JVM(三):垃圾回收算法与收集器(本篇) - Spring(一):基础与 IoC
- Spring(二):Bean 的声明、注入与生命周期
- Spring(三):AOP、MVC 与循环依赖
- Spring(四):事务详解与失效场景
- Spring(五):Spring Boot 与 Web 注解
- 数据库基础与系统设计规范