Skip to content

JVM 内存模型与垃圾回收(GC) ​

本模块面试重点 ​

  • JVM 内存结构:能画出运行时数据区(堆、方法区、栈、程序计数器、本地方法栈),并区分哪些是线程私有、哪些是线程共享。
  • GC 基本概念:分代收集的思想、常见 GC 算法的关键词(标记-清除、复制、标记-整理)、Minor GC / Full GC 的差别和触发场景(入门即可)。
  • 线上故障排查思路:遇到 OOM / 频繁 GC 时,知道大致用哪些工具(如 jmap、jstat、内存快照)排查问题点。

❓ 面试官:能画一下 JVM 的运行时数据区(内存模型)吗?哪些是线程私有的? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): JVM 内存主要分为 5 大区域。其中堆(Heap)和方法区(Method Area)是所有线程共享的;而虚拟机栈(Stack)、本地方法栈(Native Stack)和程序计数器(PC Register)是每个线程私有的,不会发生线程安全问题。

📝 详细区域解析:

  1. 堆(Heap):
    • 占用内存最大的一块。几乎所有的对象实例和数组都在这里分配内存("几乎"是因为有逃逸分析优化,对象也可能在栈上分配)。
    • 是垃圾回收(GC)发生的主要战场。
  2. 方法区(Method Area / 元空间 Metaspace):
    • 存储已被 JVM 加载的类信息(Class)、常量、静态变量(Static)、即时编译器(JIT)编译后的代码等。
    • 在 JDK 8 之前叫“永久代(PermGen)”,JDK 8 之后移除了永久代,改存在本地内存中,称为“元空间”。
  3. 虚拟机栈(VM Stack):
    • 描述的是 Java 方法执行的内存模型。每次调用一个方法,就会压入一个“栈帧(Stack Frame)”。
    • 栈帧里存放着:局部变量表(比如你在方法里写的 int a = 1)、操作数栈、动态链接、方法出口信息。方法执行完自动弹栈销毁,没有 GC。
  4. 程序计数器(PC Register):
    • 占用内存极小。它记录着当前线程执行到了哪一行字节码指令。因为多线程要来回切换,必须有个地方记住刚才执行到哪了。
  5. 本地方法栈(Native Method Stack):
    • 和虚拟机栈类似,不过它服务的是用 native 关键字修饰的底层 C/C++ 方法。

❓ 面试官:如果线上系统抛出了 OOM(OutOfMemoryError),你怎么排查是哪个区域溢出了?【中级】 ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): 首先看 OOM 后面的报错提示信息!不同的区域溢出,报错的后缀完全不一样。根据后缀再去用对应的工具(如 jmap, MAT, Arthas)分析。

📝 详细报错分析与场景:

  1. java.lang.OutOfMemoryError: Java heap space(堆溢出):
    • 原因:最常见的 OOM。堆里塞满了活生生的对象,GC 怎么清理都清理不掉(通常是内存泄漏,或者瞬间读取了千万级的数据到内存)。
    • 排查:在启动参数里加上 -XX:+HeapDumpOnOutOfMemoryError,拿到 .hprof 堆转储快照文件,用 Eclipse MAT 等工具打开,一眼就能看到是哪个类的对象占了 90% 的内存。
  2. java.lang.StackOverflowError(栈溢出):
    • 原因:这不是 OOM,但也很常见。通常是写了无限递归(死循环调用方法),导致栈帧一层一层往上叠,最终把栈深度撑爆了。
  3. java.lang.OutOfMemoryError: Metaspace(元空间溢出):
    • 原因:方法区被塞满了。通常是因为在运行期间,通过 CGLib 等动态字节码技术生成了海量的动态代理类(Class),把方法区撑爆了。

❓ 面试官:什么是垃圾回收(GC)?JVM 怎么判断一个对象是不是垃圾? ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): GC 就是自动清理掉内存中那些“没用的对象”。JVM 判断对象是不是垃圾,不使用引用计数法,而是使用 可达性分析算法(GC Roots Tracing)。

📝 详细原理解析:

  1. 为什么不用引用计数法?:
    • 理念:给对象加个计数器,被引用 1 次就 +1,断开就 -1,为 0 就当垃圾回收。
    • 致命缺陷:循环引用(内存泄漏)。A 引用 B,B 引用 A,但它们俩谁都没在用了,计数器都是 1,导致永远无法被回收。
  2. 可达性分析算法(主流做法):
    • 理念:找几个绝对不能被当成垃圾的“祖宗节点”(GC Roots)。从这几个祖宗节点开始,顺着引用链往下找(就像葡萄藤一样)。
    • 凡是能被这根藤牵出来的对象,都是“存活的”(可达);凡是断在一旁,跟这根藤没有任何联系的对象,统统判定为垃圾(不可达)。

🌟 面试加分项(实战底层): 面试官常追问:“那到底哪些对象能作为 GC Roots(祖宗节点)?” 绝杀回答: 主要有四种:

  1. 虚拟机栈中(局部变量表)正在引用的对象。
  2. 方法区中静态属性(static)引用的对象。
  3. 方法区中常量(final)引用的对象。
  4. 本地方法栈中(native 方法)引用的对象。

❓ 面试官:讲讲堆内存的分代模型,以及 Minor GC 和 Full GC 的区别?【中高级】 ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 为了提高垃圾回收效率,JVM(如 JDK 8 默认的 CMS/Parallel 组合)把堆内存分为了新生代(Young Gen)和老年代(Old Gen)。新生代发生的清理叫 Minor GC(速度极快),老年代发生的清理叫 Full GC(速度极慢,会引发长期的系统停顿 STW)。

📝 详细分代与流转机制(必背):

  1. 新生代(占比 1/3):
    • 又细分为一个 Eden 区(伊甸园)和两个 Survivor 区(S0 和 S1,通常比例是 8:1:1)。
    • 新对象出生:绝大多数刚 new 出来的新对象,都放在 Eden 区。
    • Minor GC(清理新生代):当 Eden 区满了,触发 Minor GC。存活下来的对象会被复制到一个空闲的 Survivor 区,同时年龄 +1。
    • 特点:绝大多数对象都是“朝生夕死”的(比如一个接口里的局部变量对象,接口结束就没用了),所以 Minor GC 发生极其频繁,且非常高效。
  2. 老年代(占比 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 缓存彻底解决了问题。"