JVM 内存模型与垃圾回收(GC)
本模块面试重点
- JVM 内存结构:能画出运行时数据区(堆、方法区、栈、程序计数器、本地方法栈),并区分哪些是线程私有、哪些是线程共享。
- GC 基本概念:分代收集的思想、常见 GC 算法的关键词(标记-清除、复制、标记-整理)、Minor GC / Full GC 的差别和触发场景(入门即可)。
- 线上故障排查思路:遇到 OOM / 频繁 GC 时,知道大致用哪些工具(如
jmap、jstat、内存快照)排查问题点。
❓ 面试官:能画一下 JVM 的运行时数据区(内存模型)吗?哪些是线程私有的?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): JVM 内存主要分为 5 大区域。其中堆(Heap)和方法区(Method Area)是所有线程共享的;而虚拟机栈(Stack)、本地方法栈(Native Stack)和程序计数器(PC Register)是每个线程私有的,不会发生线程安全问题。
📝 详细区域解析:
- 堆(Heap):
- 占用内存最大的一块。几乎所有的对象实例和数组都在这里分配内存("几乎"是因为有逃逸分析优化,对象也可能在栈上分配)。
- 是垃圾回收(GC)发生的主要战场。
- 方法区(Method Area / 元空间 Metaspace):
- 存储已被 JVM 加载的类信息(Class)、常量、静态变量(Static)、即时编译器(JIT)编译后的代码等。
- 在 JDK 8 之前叫“永久代(PermGen)”,JDK 8 之后移除了永久代,改存在本地内存中,称为“元空间”。
- 虚拟机栈(VM Stack):
- 描述的是 Java 方法执行的内存模型。每次调用一个方法,就会压入一个“栈帧(Stack Frame)”。
- 栈帧里存放着:局部变量表(比如你在方法里写的
int a = 1)、操作数栈、动态链接、方法出口信息。方法执行完自动弹栈销毁,没有 GC。
- 程序计数器(PC Register):
- 占用内存极小。它记录着当前线程执行到了哪一行字节码指令。因为多线程要来回切换,必须有个地方记住刚才执行到哪了。
- 本地方法栈(Native Method Stack):
- 和虚拟机栈类似,不过它服务的是用
native关键字修饰的底层 C/C++ 方法。
- 和虚拟机栈类似,不过它服务的是用
❓ 面试官:如果线上系统抛出了 OOM(OutOfMemoryError),你怎么排查是哪个区域溢出了?【中级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): 首先看 OOM 后面的报错提示信息!不同的区域溢出,报错的后缀完全不一样。根据后缀再去用对应的工具(如 jmap, MAT, Arthas)分析。
📝 详细报错分析与场景:
java.lang.OutOfMemoryError: Java heap space(堆溢出):- 原因:最常见的 OOM。堆里塞满了活生生的对象,GC 怎么清理都清理不掉(通常是内存泄漏,或者瞬间读取了千万级的数据到内存)。
- 排查:在启动参数里加上
-XX:+HeapDumpOnOutOfMemoryError,拿到.hprof堆转储快照文件,用 Eclipse MAT 等工具打开,一眼就能看到是哪个类的对象占了 90% 的内存。
java.lang.StackOverflowError(栈溢出):- 原因:这不是 OOM,但也很常见。通常是写了无限递归(死循环调用方法),导致栈帧一层一层往上叠,最终把栈深度撑爆了。
java.lang.OutOfMemoryError: Metaspace(元空间溢出):- 原因:方法区被塞满了。通常是因为在运行期间,通过 CGLib 等动态字节码技术生成了海量的动态代理类(Class),把方法区撑爆了。
❓ 面试官:什么是垃圾回收(GC)?JVM 怎么判断一个对象是不是垃圾?
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): GC 就是自动清理掉内存中那些“没用的对象”。JVM 判断对象是不是垃圾,不使用引用计数法,而是使用 可达性分析算法(GC Roots Tracing)。
📝 详细原理解析:
- 为什么不用引用计数法?:
- 理念:给对象加个计数器,被引用 1 次就 +1,断开就 -1,为 0 就当垃圾回收。
- 致命缺陷:循环引用(内存泄漏)。A 引用 B,B 引用 A,但它们俩谁都没在用了,计数器都是 1,导致永远无法被回收。
- 可达性分析算法(主流做法):
- 理念:找几个绝对不能被当成垃圾的“祖宗节点”(GC Roots)。从这几个祖宗节点开始,顺着引用链往下找(就像葡萄藤一样)。
- 凡是能被这根藤牵出来的对象,都是“存活的”(可达);凡是断在一旁,跟这根藤没有任何联系的对象,统统判定为垃圾(不可达)。
🌟 面试加分项(实战底层): 面试官常追问:“那到底哪些对象能作为 GC Roots(祖宗节点)?” 绝杀回答: 主要有四种:
- 虚拟机栈中(局部变量表)正在引用的对象。
- 方法区中静态属性(
static)引用的对象。 - 方法区中常量(
final)引用的对象。 - 本地方法栈中(
native方法)引用的对象。
❓ 面试官:讲讲堆内存的分代模型,以及 Minor GC 和 Full GC 的区别?【中高级】
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): 为了提高垃圾回收效率,JVM(如 JDK 8 默认的 CMS/Parallel 组合)把堆内存分为了新生代(Young Gen)和老年代(Old Gen)。新生代发生的清理叫 Minor GC(速度极快),老年代发生的清理叫 Full GC(速度极慢,会引发长期的系统停顿 STW)。
📝 详细分代与流转机制(必背):
- 新生代(占比 1/3):
- 又细分为一个 Eden 区(伊甸园)和两个 Survivor 区(S0 和 S1,通常比例是 8:1:1)。
- 新对象出生:绝大多数刚
new出来的新对象,都放在 Eden 区。 - Minor GC(清理新生代):当 Eden 区满了,触发 Minor GC。存活下来的对象会被复制到一个空闲的 Survivor 区,同时年龄 +1。
- 特点:绝大多数对象都是“朝生夕死”的(比如一个接口里的局部变量对象,接口结束就没用了),所以 Minor GC 发生极其频繁,且非常高效。
- 老年代(占比 2/3):
- 里面存放的都是“生命力极其顽强”的长期存活对象(比如 Spring 里的单例 Bean、本地缓存对象)。
- 晋升条件:
- 年龄达标:默认对象在 Survivor 区熬过 15 次 Minor GC 后,晋升到老年代。
- 大对象直接进入:极其庞大的数组或长字符串,直接扔进老年代(防止在两个 Survivor 区之间来回复制极其耗费性能)。
- Full GC(全堆清理):当老年代也满了,就会触发 Full GC。它不仅要清理老年代,通常还会顺带清理新生代和方法区。
- 致命痛点(STW):Full GC 发生时,整个 JVM 会产生 Stop-The-World(STW) 现象,所有业务线程全部被强行暂停!如果发生频繁,会导致接口严重卡顿、超时。
🌟 面试拔高(排查思路): "在日常线上调优中,我们的终极目标就是尽量减少 Full GC 的频率。比如遇到由于设置了过大的本地 HashMap 缓存且从不清理,导致老年代频繁撑满触发几十秒的 Full GC 卡顿,我们最终通过更换为带有自动过期淘汰机制的 Caffeine 缓存彻底解决了问题。"