内存模型 · GC算法 · 垃圾收集器 · 调优参数 · OOM排查 · 类加载
JVM 内存 = 房间空间:堆=操作台(大家共用,做大物件)| 方法区=配方手册(共享知识)| 栈=每个员工的私人工具箱(各用各的)| PC=每个员工做到哪一步的标记
对象晋升老年代的条件:
• 年龄达到阈值(默认 -XX:MaxTenuringThreshold=15)
• Survivor 区空间不足,动态年龄判断
• 大对象直接进入老年代(-XX:PretenureSizeThreshold)
| 版本 | 实现 | 位置 | OOM 风险 |
|---|---|---|---|
| JDK 7 及之前 | 永久代(PermGen) | JVM 堆中 | java.lang.OutOfMemoryError: PermGen space |
| JDK 8+ | 元空间(Metaspace) | 本地内存 | java.lang.OutOfMemoryError: Metaspace |
为什么要改?永久代大小固定容易 OOM,元空间使用本地内存,自动扩展(但需要 -XX:MaxMetaspaceSize 限制)。
面试必考:哪些区域会 OOM?堆(OOM)、方法区/元空间(OOM)、栈(StackOverflowError / OOM)、PC(不会 OOM)。字符串常量池 JDK7 后从永久代移到了堆中。
| 算法 | 原理 | 优点 | 缺点 | 应用 |
|---|---|---|---|---|
| 标记-清除 | 标记存活→清除未标记 | 简单 | 内存碎片多 | 老年代 CMS |
| 标记-复制 | 存活对象复制到另一半,清空当前 | 无碎片,效率高 | 空间减半 | 新生代 Serial/ParNew |
| 标记-整理 | 标记→存活对象向一端移动→清理边界 | 无碎片 | 移动开销大 | 老年代 Serial Old/Parallel Old |
| 分代收集 | 新生代用复制,老年代用标记-清除/整理 | 综合最优 | 实现复杂 | 现代 JVM 通用 |
标记-清除 = 打扫卫生,看到垃圾就捡,但地上到处是小空位(碎片);标记-复制 = 把有用的东西搬到隔壁房间,然后这间全部重置(简单但不省空间);标记-整理 = 把有用的东西全部推到一边,剩下全是空地(整齐但累)
| 类型 | 触发条件 | 回收区域 | STW |
|---|---|---|---|
| Minor GC / Young GC | Eden 区满 | 新生代 | 短暂 STW(毫秒级) |
| Major GC / Old GC | 老年代满 | 老年代 | STW 较长 |
| Full GC | 整个堆+方法区满 / 调用 System.gc() | 新生代+老年代+元空间 | STW 最长,要避免! |
GC Roots 包括(面试要背全):
| GC Root | 具体是什么 | 为什么是 Root | 泄漏场景 |
|---|---|---|---|
| ① 栈中局部变量 | 方法里正在用的对象:User u = userService.get(id) | 方法正在执行,对象肯定还在用 | 方法执行完自动出栈,一般不泄漏 |
| ② 静态变量 | static List<User> cache、static Map | 类不卸载,static 就一直在 | 最常见!只 put 不 remove → 无限增长 → OOM |
| ③ 活跃线程 | 线程本身就是 Root,线程内部所有引用都可达 | 线程没终止就不会被回收 | ThreadLocal 没 remove + 线程池复用 → 串数据 + 泄漏 |
| ④ 常量引用 | 方法区中的常量:static final String X = new String("x") | 常量永不卸载 | 较少见 |
| ⑤ 同步锁 | synchronized(obj) 持有的对象 | 锁还在持有,对象不能消失 | 锁没释放 → 关联对象不回收 |
| ⑥ JNI 引用 | 本地方法(C/C++)通过 JNI 持有的 Java 对象 | native 代码还在用 | native 代码忘记释放 |
| ⑦ JVM 内部 | 基本类型的 Class 对象、异常对象、类加载器 | JVM 运行需要它们 | 不泄漏(JVM 管) |
GC Roots 就是 "JVM 认为一定还活着的东西":正在用的变量、static 变量、活跃的线程、锁持有的对象。内存泄漏的本质 = 对象不用了,但还被某个 GC Root 引用着,GC 走不到"该回收"的结论。
会,但很容易被 GC 自动回收。关键在于"引用链是否还连着 GC Root":
| 变量类型 | 连着 GC Root 吗 | 泄漏概率 | 原因 |
|---|---|---|---|
方法内局部变量User u = new User() |
方法执行期间连着 方法结束 → 自动断开 |
极低 | 方法结束 → 栈帧弹出 → u 不再是 Root 引用 → User 对象不可达 → GC 回收 |
实例变量(成员变量)private List<User> list |
连着 → 只要外层对象还活着 | 中 | 外层对象被回收 → 成员变量跟着回收。但如果外层对象本身泄漏了,里面的成员也跟着泄漏 |
static 变量static Map cache |
永远连着(类不卸载就在) | 极高! | static 是永久 Root,只 put 不 remove → 永远不可达不了 → 永远不回收 |
| ThreadLocal | 连着活跃线程(线程池不销毁) | 极高! | 线程池复用线程 → ThreadLocalMap 跟着线程不死 → value 强引用不回收 |
| 集合里的元素 List / Map 里的对象 |
取决于集合本身是否连着 Root | 看集合 | 集合是局部变量 → 方法结束自动回收。集合是 static → 里面的元素永远不回收 |
| 引用类型 | 回收时机 | 用途 |
|---|---|---|
| 强引用(Object obj = new Object()) | 永远不回收(只要引用还在) | 普通对象 |
| 软引用(SoftReference) | 内存不足时才回收 | 缓存(如图片缓存) |
| 弱引用(WeakReference) | 下一次 GC 就回收 | ThreadLocal 的 key、WeakHashMap |
| 虚引用(PhantomReference) | 随时可回收,get()永远返回null | 跟踪 GC 回收、管理堆外内存 |
| 收集器 | 算法 | 特点 | 适用场景 |
|---|---|---|---|
| Serial | 复制 | 单线程,STW | 客户端模式、小内存 |
| ParNew | 复制 | 多线程版 Serial | 配合 CMS |
| Parallel Scavenge | 复制 | 吞吐量优先,自适应调节 | 后台计算型应用 |
| CMS | 标记-清除 | 并发标记清除,低延迟,有碎片 | JDK9 已废弃,追求响应 |
| G1 | 复制+整理 | Region 分区,可预测停顿 | JDK9+ 默认,大堆内存 |
| ZGC | 复制+整理 | 亚毫秒级停顿,着色指针 | JDK15+,超低延迟场景 |
CMS 三大问题:
① CPU 敏感(并发阶段占 CPU)
② 浮动垃圾(并发清除阶段新产生的垃圾,下次才能清)
③ 内存碎片(标记-清除 算法固有问题)
• 堆分为多个等大 Region(1~32MB),每个 Region 可以是 Eden/Survivor/Old/Humongous
• 维护优先级列表,优先回收价值最大的 Region
• 可设定最大停顿时间:-XX:MaxGCPauseMillis=200
• 回收步骤:初始标记(STW) → 并发标记 → 最终标记(STW) → 筛选回收(STW)
• 着色指针:在指针中存储 GC 信息,不需要额外的标记位
• 读屏障:对象移动时通过读屏障修正引用,实现并发整理
• 停顿时间 < 1ms,且不随堆大小增长
• JDK 21 引入 分代 ZGC,进一步提升吞吐
| 参数 | 含义 | 推荐值 |
|---|---|---|
| -Xms | 初始堆大小 | 与 Xmx 相同,避免动态扩容 |
| -Xmx | 最大堆大小 | 物理内存的 60~80% |
| -Xmn / -XX:NewRatio | 新生代大小 | 堆的 1/3~1/2 |
| -XX:SurvivorRatio | Eden:S0:S1 比例 | 8(默认 8:1:1) |
| -XX:MetaspaceSize / MaxMetaspaceSize | 元空间大小 | 256M~512M |
| -Xss | 线程栈大小 | 512K~1M(默认 1M) |
| 参数 | 含义 |
|---|---|
| -XX:+UseSerialGC | 使用 Serial + Serial Old |
| -XX:+UseParNewGC | 使用 ParNew + Serial Old |
| -XX:+UseParallelGC | 使用 Parallel Scavenge + Parallel Old(JDK8 默认) |
| -XX:+UseConcMarkSweepGC | 使用 ParNew + CMS |
| -XX:+UseG1GC | 使用 G1(JDK9+ 默认) |
| -XX:+UseZGC | 使用 ZGC(JDK15+) |
| -XX:MaxGCPauseMillis | G1 目标最大停顿时间(默认 200ms) |
| -XX:+PrintGCDetails | 打印 GC 详细日志(JDK8) |
| -Xlog:gc* | 打印 GC 日志(JDK9+ 统一日志格式) |
面试常说:-Xms 和 -Xmx 设成一样,避免堆大小动态调整的开销。HeapDumpOnOutOfMemoryError 一定要加,OOM 时自动生成堆转储,排查利器。
| 错误类型 | 原因 | 排查方向 |
|---|---|---|
| Java heap space | 堆内存不足 | 内存泄漏 / 堆太小 / 大对象 |
| Metaspace | 元空间不足 | 动态代理类过多 / 没限 MaxMetaspaceSize |
| GC overhead limit exceeded | GC 回收的不到 2% | 堆快满了,GC 疯狂跑但没用 |
| unable to create new native thread | 线程数太多 | 线程泄漏 / OS 限制 |
| Direct buffer memory | 堆外内存不足 | NIO ByteBuffer / Netty 未释放 |
• ThreadLocal 未 remove(线程池复用线程)
• 静态集合持续增长(static List/Map 只加不删)
• 资源未关闭(Connection/Stream/Channel)
• 监听器/回调未注销(EventBus 只注册不反注册)
• 缓存无过期(自己写的 Map 缓存没 LRU/TTL)
关键区分:
• 准备阶段:static int a = 10; → a 先赋 0(零值),不是 10!
• 初始化阶段:a 才被赋值为 10
• static final 常量在准备阶段就赋值(编译期常量)
核心原则:先让父加载器加载,父加载器搞不定才自己加载。保证核心类(如 java.lang.Object)不会被自定义类覆盖。
• Tomcat:每个 WebApp 独立 ClassLoader,先自己加载再委派(保证应用隔离)
• SPI 机制:JDBC 的 Driver 接口在 rt.jar,实现在第三方 jar,Bootstrap CL 加不到 → 用 Thread Context ClassLoader
• OSGi:模块化加载,网状结构而非树状
• 热部署:重新创建 ClassLoader 加载新版本类
面试中说出"双亲委派保证了核心类的安全性"还不够,要加上"SPI 用线程上下文类加载器打破双亲委派"和"Tomcat 自定义 ClassLoader 实现应用隔离"这两个实际场景。
5 大区域:
① 堆(对象实例,线程共享)
② 方法区/元空间(类信息、常量,线程共享)
③ 虚拟机栈(栈帧,线程私有)
④ 本地方法栈(Native 方法,线程私有)
⑤ 程序计数器(字节码行号,线程私有,唯一不会 OOM)
• 栈:线程私有,存局部变量/方法调用,自动分配释放,空间小但快
• 堆:线程共享,存对象实例,GC 管理,空间大但相对慢
• 栈溢出 → StackOverflowError;堆溢出 → OutOfMemoryError
① 老年代空间不足
② 方法区/元空间空间不足
③ 显式调用 System.gc()
④ Minor GC 后老年代空间不够放存活对象
⑤ CMS GC 时出现 Concurrent Mode Failure
• CMS:标记-清除,有碎片,不能处理浮动垃圾,JDK9 废弃
• G1:Region 分区 + 复制整理,无碎片,可预测停顿时间,JDK9 默认
• G1 适合大堆(6G+),CMS 适合中小堆
• G1 回收是选择价值最大的 Region 组,不是整代回收
STW = Stop The World,GC 时暂停所有用户线程。
不能完全避免,因为:GC 需要确保一致性快照,如果用户线程同时修改对象引用,GC 结果就不准确。CMS/G1/ZGC 都在尽量缩短 STW 时间,ZGC 已做到亚毫秒级。
• 内存泄漏:对象不再使用但 GC 无法回收(如静态集合持有引用)→ 是原因
• 内存溢出 OOM:没有足够内存分配新对象 → 是结果
• 内存泄漏积累到一定程度 → 内存溢出
两个核心目标(互斥,需权衡):
① 低延迟:减少 GC 停顿时间(选 CMS/G1/ZGC)
② 高吞吐:减少 GC 总耗时占比(选 Parallel Scavenge)
实际项目中一般是降低延迟 + 避免 Full GC。
新生代对象 98% 都是朝生夕灭,存活率极低。
复制算法只需要复制少量存活对象到 Survivor,清空 Eden 即可,效率极高。
而且 8:1:1 的比例让空间浪费只有 10%,非常划算。
分析对象的作用域是否"逃逸"出方法。
没有逃逸的对象可以:
① 栈上分配:不用在堆上分配,方法结束自动回收
② 标量替换:拆成基本类型,直接在栈上
③ 消除同步锁:对象不出方法,没有并发问题,锁可消除
① 年龄达到 MaxTenuringThreshold(默认 15)
② Survivor 区相同年龄对象大小超过 Survivor 空间一半(动态年龄判断)
③ 大对象直接进入(-XX:PretenureSizeThreshold)
④ Minor GC 后 Survivor 放不下
两种算法:
① 引用计数法:有引用+1,无引用-1,为0就回收。问题:循环引用无法回收。
② 可达性分析法(JVM 实际使用):从 GC Roots 出发,沿引用链遍历,不可达的对象可回收。
① 安全性:防止核心类被篡改(如自定义 java.lang.Object 不会被加载)
② 避免重复加载:父加载器已加载的类,子加载器不需要再加载
③ 层次清晰:每一层负责自己的类,职责分明
① 强引用:永远不会被回收(OutOfMemoryError 也不回收)
② 软引用:内存不足时回收 → 缓存
③ 弱引用:下一次 GC 回收 → ThreadLocal、WeakHashMap
④ 虚引用:随时回收,get() 返回 null → 跟踪 GC、管理堆外内存
① 看错误日志,确认 OOM 类型
② 用 jmap -histo 看哪些对象最多
③ 用 jmap -dump 导出堆转储(或提前开启 HeapDumpOnOutOfMemoryError)
④ 用 MAT 分析堆转储,找到 GC Roots 引用链
⑤ 或用 Arthas 在线诊断:dashboard 看 memory,heapdump 导出
⑥ 定位代码行,修复泄漏
• 永久代(JDK7-):在 JVM 堆中,大小固定,容易 OOM
• 元空间(JDK8+):在本地内存中,自动扩展,不容易 OOM
• 常量池从永久代移到堆中(JDK7 开始)
• 字符串常量池在堆,运行时常量池在元空间
内存区域速记:
堆(共享) | 方法区(共享) | 栈(私有) | 本地栈(私有) | PC(私有,不OOM)
堆结构速记:
新生代1/3(Eden:S0:S1=8:1:1) | 老年代2/3 | 年龄≥15晋升
GC算法速记:
标记清除(碎片) | 标记复制(新生代) | 标记整理(老年代) | 分代收集(综合)
收集器速记:
Serial→ParNew→Parallel→CMS→G1→ZGC | JDK8默认Parallel | JDK9+默认G1
调优参数速记:
Xms=Xmx | HeapDumpOnOOM | MaxGCPauseMillis | UseG1GC/ZGC
类加载速记:
加载→验证→准备(零值)→解析→初始化 | 双亲委派 | SPI打破
引用速记:
强(不回收) | 软(内存不足) | 弱(下次GC) | 虚(跟踪回收)
OOM排查速记:
jps→jmap -histo→jmap -dump→MAT分析→定位代码
"JVM 内存有哪些区域?" → "堆怎么划分?" → "对象什么时候进老年代?" → "什么触发 Full GC?"
"有哪些 GC 算法?" → "G1 和 CMS 区别?" → "线上怎么选收集器?" → "JVM 调优参数有哪些?"
"OOM 有哪些类型?" → "线上 OOM 怎么排查?" → "常见内存泄漏场景?" → "怎么避免?"
"类加载过程?" → "双亲委派是什么?" → "有什么好处?" → "哪些场景打破了双亲委派?"
JVM调优:① 先监控后调优(没有数据不调参)→ ② 堆分区比参数值更重要(Eden:Survivor比例影响GC频率)→ ③ 生产环境优先选G1/ZGC(低延迟优于吞吐量)