JVM 入门:Java 程序住在哪里
面试必问、线上排障必备。你不用手写 JVM,但要知道对象住哪、垃圾谁收、类怎么加载这三件事。理解了它们,遇到"内存溢出 OutOfMemoryError"才不会两眼一抹黑。
运行时内存区域:对象都住哪个房间
| 区域 | 存什么 | 大白话 & 常见错误 |
|---|---|---|
| 堆 Heap | new 出来的对象 | 所有线程共享。OutOfMemoryError: Java heap space 就是这儿爆了——对象太多没回收掉。 |
| 栈 Stack | 方法内局部变量、调用栈 | 每个线程一个。方法调用太深(无限递归)会 StackOverflowError。 |
| 方法区 / 元空间 Metaspace | 类信息、常量池 | JDK 8 后用本地内存,OutOfMemoryError: Metaspace 常见于动态生成类太多。 |
| 程序计数器 | 当前执行到哪行字节码 | 最小的一块,几乎不会出问题。 |
垃圾回收 GC:Java 的"清洁工"
C++ 里你 new 了对象必须自己 delete,忘了就内存泄漏。Java 不用——GC(Garbage Collector)自动把"没人引用的对象"回收掉。你只管 new,JVM 负责扫垃圾。
常用 JVM 启动参数(入门够用版)
# 指定堆初始大小和最大值(生产环境强烈建议设成一样,避免来回伸缩)
java -Xms512m -Xmx512m -jar myapp.jar
# 打印 GC 日志,排查"偶发卡顿"时用
java -Xlog:gc* -jar myapp.jar
# 选 GC(JDK 21 默认就是 G1,显式写出来也无妨)
java -XX:+UseG1GC -jar myapp.jar
GC 日志怎么读:一条典型 G1 日志的字段含义
# JDK 9+ 统一日志格式,加 -Xlog:gc* 就会打印:
[0.123s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
20M->5M(100M) 3.456ms
# 逐字段翻译:
# [0.123s] JVM 启动后 0.123 秒
# GC(0) 第 0 次 GC
# Pause Young 这次是 Young GC(回收新生代)
# 20M->5M(100M) GC 前堆 20M,GC 后 5M,堆总大小 100M
# 3.456ms 这次停顿 3.4 毫秒
# 排查"偶发卡顿":找耗时最长的那行 Pause,看是不是 Mixed GC / Full GC
# 如果频繁出现 Full GC,说明堆不够大或者内存泄漏
生产环境 JVM 参数调优示例(入门够用版)
# 一个 4G 堆的 Spring Boot 应用启动参数参考
java \
-Xms4g -Xmx4g \ # 堆初始和最大设成一样,避免来回伸缩
-XX:+UseG1GC \ # JDK 9+ 默认就是 G1,显式写出来
-XX:MaxGCPauseMillis=200 \ # 期望 GC 停顿不超过 200ms
-XX:+HeapDumpOnOutOfMemoryError \ # OOM 时自动 dump 堆快照
-XX:HeapDumpPath=/var/log/dump/ \ # dump 文件存哪
-Xlog:gc*:file=/var/log/gc.log:time,tags \ # GC 日志写文件
-jar myapp.jar
# 排查 OOM 时:拿到 heap dump 用 Eclipse MAT 打开,看哪个对象占了大头
# 别上来就加 -Xmx:先搞清楚为什么内存涨上去,加堆只是续命
三种基础回收算法:G1/ZGC 都是它们的拼盘
面试官爱问"GC 算法有哪些",其实底层就三板斧。新生代老年代用不同组合,G1 也只是把这三板斧分区分片地用。
| 算法 | 人话 | 优缺点 |
|---|---|---|
| 标记-清除 Mark-Sweep | 先扫一遍标记哪些对象活着,再把没标记的清掉。 | 简单;但清完会产生碎片,东一块西一块,大对象放不下。 |
| 复制 Copying | 把活着的对象整体复制到另一半空区域,再把原区域一把清空。 | 没有碎片、快;但要浪费一半空间。新生代(对象朝生夕死)用它最划算。 |
| 标记-整理 Mark-Compact | 标记后,把活对象往一端挪,再把边界外的清掉。 | 无碎片、不浪费空间;但要挪对象,慢。老年代(活得久、存活率高)用它。 |
论分代收集 = 三板斧的拼盘
新生代对象死得快,用复制算法(只留少量存活对象复制走);老年代对象活得久、占空间大,用标记-整理(别浪费空间)。G1 把堆切成很多小块,每块按需选用这三板斧,所以能做到停顿可预测。知道"新生代复制、老年代整理"这一句,面试就够了。
常见 OutOfMemoryError:报错信息对号入座
| 报错信息 | 哪里爆了 & 大概原因 |
|---|---|
| Java heap space | 堆爆了:对象太多没回收,或一次性加载大集合。 |
| GC overhead limit exceeded | GC 花了 98% 时间却只回收一点点,基本等同于"快撑爆了"。 |
| Metaspace | 元空间爆了:动态生成类(CGLIB、反射)太多。 |
| unable to create new native thread | 线程开太多,操作系统扛不住——八成是没用到线程池。 |
论类加载机制:三个阶段
加载(Loading):把 .class 字节码读进内存。链接(Linking):验证格式、给静态变量赋默认值、把符号引用换成直接引用。初始化(Initialization):执行静态代码块、给静态变量赋真正的值。
面试常问"双亲委派模型":类加载请求先递给父加载器,父加载器找不到才自己加载——目的是防止核心类被篡改。工作中知道"类是按需加载、有层级委派"这个概念即可。
三层类加载器(从上到下):启动类加载器(加载 java.lang.* 等核心类)→ 平台类加载器(加载扩展模块)→ 应用类加载器(加载你自己写的 classpath 下的类)。加载 java.lang.String 时,永远先让最顶层去加载,就算你自己写了一个同名 String,也不会被用到——这就是双亲委派的安全意义。
1.(概念题)JVM 运行时数据区有哪些?对象实例存在哪?
查看答案
答案:堆(Heap,所有线程共享,new 的对象在这)、栈(Stack,每个线程一个,存局部变量和调用栈)、方法区/元空间(类信息、常量池)、程序计数器。对象实例在堆里。
2.(概念题)GC 怎么判断对象可以回收?
查看答案
答案:可达性分析:从 GC Roots(栈里的局部变量、静态变量、常量)出发,顺着引用链走,走不到的对象就是垃圾。不是引用计数法(解决不了循环引用)。
3.(概念题)类加载过程分哪几步?什么是双亲委派?
查看答案
答案:加载 → 验证 → 准备 → 解析 → 初始化。双亲委派:类加载请求先递给父加载器,父加载器找不到才自己加载,防止核心类(java.lang.String)被篡改。
4.(排错题)报 OutOfMemoryError: Java heap space,先做什么?
查看答案
答案:不是立刻加 -Xmx。先开 -XX:+HeapDumpOnOutOfMemoryError 拿到 dump,用 MAT 分析谁占内存最大。可能是一次性加载了大集合,或静态 Map 只加不清导致泄漏。
5.(选择题)JDK 9+ 默认 GC 是哪个?A. Parallel B. CMS C. G1 D. ZGC
查看答案
答案:C。G1 从 JDK 9 起是默认,面向大堆、可预测停顿。CMS 在 JDK 14 被移除。ZGC 是超低延迟选项。