掌握 JVM 内存模型、垃圾回收、类加载机制与调优
本页作为 JVM 专题的总览页,覆盖 14 道高频问题,并给出到各子专题页的跳转入口。
JVM 运行时数据区分为线程共享和线程私有两大类:
面试话术:"堆和方法区是所有线程共享的,主要参与 GC;栈、本地方法栈和 PC 是线程私有的,负责方法调用和执行流程控制。"
public void foo() {
int a = 10; // 栈:基本类型局部变量
String str = "hello"; // 栈:引用,常量池:"hello"
Person p = new Person(); // 栈:引用 p,堆:Person 对象实例
// 方法结束后:
// - 栈帧自动弹出,a/str/p 引用消失
// - 堆中的 Person 对象等待 GC 回收
}易错点:"引用"本身在栈上,对象实例在堆上;基本类型的局部变量完全在栈上。
StackOverflowError(栈溢出):线程请求的栈深度超过虚拟机允许的最大深度。
常见原因:
// StackOverflowError 示例
public void recursion() {
recursion(); // 无限递归,没有终止条件
}
// 解决方案
public void recursion(int depth) {
if (depth > 1000) return; // 添加终止条件
recursion(depth + 1);
}OutOfMemoryError(内存溢出):堆、元空间或其他内存区域无法分配足够的内存。
常见原因:
// OutOfMemoryError 示例
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 不断往堆里塞对象
}
// 解决方案
// 1. 增加堆内存:-Xmx2g
// 2. 检查内存泄漏
// 3. 优化代码,及时释放资源-Xss(栈大小),OOM 关注 -Xmx(堆最大值)和 -XX:MaxMetaspaceSize(元空间)。垃圾回收的核心是"如何找到垃圾"和"如何回收垃圾",主要有以下四种算法:
现代 JVM 基本都采用分代收集,根据对象存活时间选择不同算法,兼顾效率和空间利用率。
垃圾收集器是 GC 算法的具体实现,不同收集器适用于不同场景:
选择建议:
# G1 配置示例(推荐)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
# ZGC 配置示例(JDK 11+)
-XX:+UseZGC
-XX:ZCollectionInterval=120面试话术:"JDK 9 之后默认用 G1,它通过分 Region 管理堆,可以设置最大停顿时间目标,适合大多数服务端应用。"
Minor GC(Young GC):
Major GC:
Full GC:
观察 GC 日志时:Minor GC 频繁是正常的,但如果 Full GC 频率高(如每分钟多次)或单次停顿超过 1 秒,就需要调优了。
类加载分为加载、连接、初始化三大阶段,连接又细分为验证、准备、解析:
public class Test {
// 准备阶段:value = 0(默认值)
// 初始化阶段:value = 123(显式赋值)
public static int value = 123;
static {
// 初始化阶段执行
System.out.println("Static block");
}
}双亲委派模型:类加载器收到加载请求时,先委派给父加载器,父加载器无法加载时才自己加载。
设计目的:
什么时候需要打破双亲委派?
JVM 参数分为三类:标准参数(-)、非标准参数(-X)、不稳定参数(-XX)。
# 堆内存设置
-Xms2g # 初始堆大小
-Xmx4g # 最大堆大小
-Xmn1g # 新生代大小
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1
# 栈内存设置
-Xss256k # 每个线程栈大小
# 元空间设置(JDK 8+)
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
# GC 相关
-XX:+UseG1GC # 使用 G1 收集器
-XX:MaxGCPauseMillis=200 # 最大 GC 停顿时间目标
-XX:+PrintGCDetails # 打印 GC 详情(JDK 8)
-Xlog:gc*:file=gc.log # GC 日志(JDK 9+)
-XX:+HeapDumpOnOutOfMemoryError # OOM 时生成堆转储
-XX:HeapDumpPath=/tmp/dump.hprof
# 性能调优
-XX:+DisableExplicitGC # 禁用 System.gc()
-XX:+UseStringDeduplication # 字符串去重(G1)OOM 排查的标准流程:
# 启动时配置自动生成
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
# 手动生成
jmap -dump:format=b,file=heapdump.hprof <pid># 实时监控 GC
jstat -gcutil <pid> 1000 # 每秒打印一次
# 分析 GC 日志
# 关注 Full GC 频率、老年代使用率趋势内存泄漏(Memory Leak):程序已经不再使用某些对象,但这些对象依然有引用链可达,JVM 无法回收它们,导致堆占用越来越高,最终可能抛出 OutOfMemoryError。
在 JVM 中常见的几种场景:
static List/Map 持有大量对象,但从不清空,随着请求增多集合只增不减。WeakHashMap 等。set 不 remove,线程长期存在时其 ThreadLocalMap 条目也一直存活。避免策略:控制集合大小并显式清理;合理使用弱引用/软引用做缓存;为监听器提供明确的注销逻辑;在线程池场景中使用完 ThreadLocal 后立刻 remove();对所有外部资源使用 try-with-resources 自动关闭。
从 GC 回收的“优先级”来看:强引用 > 软引用 > 弱引用 > 虚引用。
// 强引用(默认)
Object strong = new Object();
// 软引用:内存吃紧时 GC
SoftReference<byte[]> soft = new SoftReference<>(new byte[1024]);
// 弱引用:下一次 GC 一定会回收
WeakReference<Object> weak = new WeakReference<>(new Object());
// 虚引用:无法通过 get() 取得对象,只能配合 ReferenceQueue 监控回收
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);面试话术:先给出一个表格,再补一句“软引用常用于内存敏感缓存、弱引用常用于 Map 键自动过期、虚引用主要做资源清理和监控”。
一套通用的 JVM 调优 checklist:
-Xlog:gc* 或 -XX:+PrintGCDetails),同时监控 CPU、内存、线程数和业务 RT/QPS。经验总结:优先优化代码,其次调 JVM 参数,最后再考虑加机器。不要一上来就堆硬件。
命令行工具:
jps:列出当前 Java 进程,排查 PID。jstat:查看 GC/内存使用(jstat -gcutil pid 1000)。jmap:导出 heap dump、查看堆结构。jstack:打印线程栈,定位死锁和 CPU 飙高线程。jinfo:查看/修改 JVM 参数。jcmd:综合诊断工具,新版很多功能只在它里面提供。图形/在线工具:
面试时可以拿一个真实排查案例(例如“线程暴涨 + Full GC 频繁”)简单讲一下你是如何配合这些工具一步步定位问题的,会非常加分。