Java 后端真实面试专题 · JVM 篇
JVM 是真实面经里反复被追问、也是学员最常答不上来的硬骨头。每题三段: ① 标准答(讲透)→ ② 拓展(成体系带出关联点和必追问的)→ ③ 怎么接到你自己的项目。
年限标签:
🟢 3年内🔴 3年+
1. 🟢 JVM 的内存结构(运行时数据区)有哪些?
标准答:
- 线程共享:堆(对象实例,GC 主战场)、方法区/元空间(类信息、常量、静态变量)。
- 线程私有:虚拟机栈(每个方法一个栈帧,存局部变量、操作数栈)、本地方法栈(native 方法)、程序计数器(当前执行的字节码行号)。
拓展:
- "哪些区域会 OOM?"——堆(对象太多)、栈(递归太深 StackOverflowError)、元空间(类太多)。
- "程序计数器为什么不会 OOM?"——它只存一个行号,唯一不会 OOM 的区域。
- JDK8 把永久代换成了元空间(用本地内存)。
往项目引 ⭐:"理解内存结构是排查 OOM 的基础——我项目出过堆 OOM(大对象/内存泄漏)和元空间 OOM(动态生成类太多),知道是哪块区域才能对症下药。"
2. 🟢 堆和栈有什么区别?
标准答:
- 栈:线程私有,存方法的局部变量、对象引用,方法调用入栈、结束出栈,自动回收,速度快。
- 堆:线程共享,存对象实例本身,由 GC 管理回收,空间大但慢。 简单说:栈管"引用和基本类型",堆管"对象"。
拓展:
- "对象一定在堆上吗?"——不一定,JIT 的逃逸分析可能做栈上分配/标量替换(对象没逃出方法就放栈上)。
- "栈溢出和堆溢出?"——栈溢出(StackOverflowError,递归过深)、堆溢出(OutOfMemoryError)。
往项目引 ⭐:"我项目排查过一次 StackOverflowError——是一段递归没正确终止导致栈帧无限叠加。理解栈的机制让我一看异常就知道是递归或调用太深的问题。"
3. 🟢 new 一个对象的过程是什么?
标准答:① 检查类是否已加载,没加载先类加载;② 在堆上分配内存(指针碰撞或空闲列表);③ 内存初始化零值;④ 设置对象头(类型指针、Mark Word);⑤ 执行构造方法(init)赋初值。
拓展:
- "分配内存怎么保证并发安全?"——CAS + 重试,或用 TLAB(每个线程一块私有分配缓冲)。
- "对象头里有什么?"——Mark Word(锁状态、hashcode、GC 年龄)+ 类型指针。
- 这题能引到对象内存布局、锁升级(锁信息在 Mark Word)。
往项目引 ⭐:"理解 new 的过程帮我串起了很多知识——比如 synchronized 的锁状态就存在对象头 Mark Word 里,对象的 GC 年龄也在那,面试时能从'new 对象'自然延伸到锁和 GC。"
4. 🟢 类的加载过程?什么是双亲委派?
标准答:类加载五步——加载(读字节码)→ 验证(校验合法性)→ 准备(静态变量分配内存赋零值)→ 解析(符号引用转直接引用)→ 初始化(执行 static 代码、赋真实值)。 双亲委派:类加载请求先交给父加载器,父加载不了才自己加载(应用类加载器 → 扩展类加载器 → 启动类加载器逐级向上委派)。
拓展:
- "双亲委派的好处?"——保证核心类(如
java.lang.String)不被篡改、避免重复加载。 - "怎么打破双亲委派?"——重写 loadClass(如 Tomcat 每个 webapp 独立类加载器、JDBC 的 SPI)。
- 准备阶段静态变量是零值,初始化阶段才赋真实值。
往项目引 ⭐:"我项目用 SPI 机制加载第三方实现(如不同的支付/存储驱动),它就是打破双亲委派、用线程上下文类加载器加载实现类——理解类加载让我看得懂这种'插件式'扩展。"
5. 🔴 怎么判断一个对象可以被回收?(垃圾判定)
标准答:两种算法——
- 引用计数:对象被引用次数为 0 就回收。缺点:解决不了循环引用,所以 JVM 不用它。
- 可达性分析(JVM 用):从 GC Roots(栈中引用的对象、静态变量、常量、native 引用等)出发往下搜索,搜不到的对象就是垃圾。
拓展:
- "GC Roots 有哪些?"——虚拟机栈引用的对象、方法区静态属性/常量引用的对象、本地方法栈引用的对象。
- "对象被判死一定立刻回收吗?"——不,还有一次 finalize 自救机会(但不推荐用 finalize)。
- 引出四种引用:强、软、弱、虚(见后题)。
往项目引 ⭐:"理解可达性分析帮我理解了内存泄漏的本质——对象其实没用了,但还被某个 GC Roots(如静态集合、ThreadLocal)引用着,所以一直回收不掉。排查泄漏就是找这种'本该死却还被引用'的对象。"
6. 🔴 垃圾回收算法有哪些?分代收集是什么?
标准答:
- 标记-清除:标记垃圾再清除,简单但产生内存碎片。
- 复制:内存分两半,存活对象复制到另一半,无碎片但浪费空间,适合对象存活率低的新生代。
- 标记-整理:标记后把存活对象移到一端,无碎片,适合存活率高的老年代。
- 分代收集:把堆分新生代(复制算法)和老年代(标记整理),不同代用最合适的算法。
拓展:
- "为什么分代?"——大部分对象朝生夕死(新生代用复制最划算),少数长期存活(老年代用标记整理)。
- 新生代分 Eden + 两个 Survivor(8:1:1),对象先进 Eden,Minor GC 后存活的进 Survivor。
往项目引 ⭐:"理解分代让我能看懂 GC 日志——大部分 GC 是新生代的 Minor GC(很快),频繁 Full GC 才是问题信号。我项目调优就是盯着别让对象过早进老年代、减少 Full GC。"
7. 🔴 对象什么时候从新生代进入老年代?
标准答:几种情况——① 年龄达到阈值(每次 Minor GC 存活年龄 +1,默认到 15 进老年代);② 大对象直接进老年代(避免在 Survivor 来回复制);③ 动态年龄判定(Survivor 中同年龄对象超一半,大于该年龄的直接晋升);④ Survivor 放不下,提前进老年代。
拓展:
- "为什么大对象直接进老年代?"——大对象复制成本高,直接放老年代省事。
- "对象过早进老年代有什么问题?"——老年代满得快、Full GC 变频繁,是调优要避免的。
往项目引 ⭐:"我项目调过一次 Full GC 频繁——发现是有批量大对象(大集合)频繁进老年代。优化了对象大小和生命周期、调大新生代,Full GC 明显减少。理解晋升机制才能这么调。"
8. 🔴 有哪些垃圾回收器?G1 的设计思想是什么?
标准答:
- Serial:单线程,STW,适合客户端/小堆。
- Parallel Scavenge:多线程,吞吐优先,JDK8 默认。
- CMS:低停顿、并发标记清除,但有碎片和 concurrent mode failure,已废弃。
- G1(JDK9+ 默认):把堆分成多个 Region,可预测停顿时间、优先回收垃圾最多的 Region(Garbage First),兼顾吞吐和低停顿。
- ZGC/Shenandoah:超低停顿(ms 级),适合大堆。
拓展:
- "G1 和 CMS 区别?"——G1 用 Region 化、能控停顿目标(
-XX:MaxGCPauseMillis)、整体上是标记整理无碎片。 - "怎么选 GC?"——吞吐优先选 Parallel,低延迟选 G1/ZGC。
往项目引 ⭐:"我项目用 G1,设了停顿时间目标,兼顾吞吐和延迟。面试被问'你用哪个 GC、为什么'时,我能答出'G1 + Region 化 + 可控停顿',并说我们怎么根据停顿和吞吐监控调参数。"
9. 🔴 G1 什么时候触发 Full GC?
标准答:G1 设计上尽量用 Mixed GC(混合回收)避免 Full GC,但以下情况会触发 Full GC(早期 G1 是单线程 Full GC、很慢):老年代空间不足、并发标记没跟上分配速度(分配失败)、元空间不足、显式 System.gc()。
拓展:
- G1 的 Full GC 是要极力避免的(会长 STW)。
- 避免方法:调大堆/调整 Region、降低对象分配速率、避免大对象、调
InitiatingHeapOccupancyPercent让并发标记早点开始。 - JDK10+ G1 Full GC 改成并行、快了些。
往项目引 ⭐:"我项目监控里盯着 G1 的 Full GC——一旦出现就要查是不是分配太快、并发标记跟不上。调过 IHOP 让标记提前启动,避免来不及回收触发 Full GC。"
10. 🟢 Minor GC、Full GC 的区别?什么是 STW?
标准答:
- Minor GC(Young GC):回收新生代,频繁但快。
- Full GC:回收整个堆(含老年代)+ 方法区,慢、停顿长,要尽量减少。
- STW(Stop The World):GC 时暂停所有用户线程,停顿期间应用无响应。GC 优化核心就是减少 STW 时间和频率。
拓展:
- "Full GC 频繁的原因?"——内存泄漏、大对象、老年代太小、元空间不足、显式 System.gc()。
- 几乎所有 GC 都有 STW(CMS/G1 把并发标记部分并发化以减少 STW)。
往项目引 ⭐:"我项目把'Full GC 次数和耗时'作为核心监控指标——Full GC 频繁 = 有内存问题。一次告警就是内存泄漏导致老年代填满频繁 Full GC,定位修复后恢复。"
11. 🔴 线上 CPU 飙高怎么排查?
标准答:
top找到 CPU 高的进程 pid。top -Hp pid找到该进程里 CPU 高的线程 tid。- 线程 tid 转 16 进制(
printf %x)。 jstack pid导出线程栈,搜那个 16 进制 nid,定位到具体代码。 常见原因:死循环、频繁 GC、正则回溯、锁竞争。
拓展:
- 如果是 GC 导致 CPU 高,jstack 会看到 GC 线程,要去看 GC 日志。
- 用 Arthas 的
thread命令能更方便地定位忙线程。
往项目引 ⭐:"我项目处理过一次线上 CPU 100%——top -Hp + jstack 定位到一段异常导致的死循环,10 分钟修复。有这套完整命令链,面试官会觉得你真扛过线上事故。"
12. 🔴 内存溢出(OOM)和内存泄漏的区别?怎么排查 OOM?
标准答:
- 内存泄漏:对象用完了但还被引用、回收不掉,越积越多,最终导致内存溢出。
- 内存溢出(OOM):申请内存时没有足够空间,抛 OutOfMemoryError。
泄漏是因、溢出是果。排查 OOM:加
-XX:+HeapDumpOnOutOfMemoryError导出堆 dump,用 MAT/JProfiler 分析大对象和引用链(GC Roots 到大对象的路径),找泄漏点。
拓展:
- 常见泄漏点:静态集合一直 add 不删、ThreadLocal 没 remove、连接/流没关、缓存无上限。
- 不是所有 OOM 都是泄漏——也可能是真的需要那么多内存(要扩容)或大对象。
往项目引 ⭐:"我项目排查过内存泄漏——dump 分析发现是一个静态 Map 当缓存用、只加不删,越积越大。改成有上限的 LRU 缓存(或加过期)后解决。能讲'dump → MAT → 找引用链'这套流程很加分。"
13. 🔴 JVM 调优一般调什么?
标准答:核心目标是减少 Full GC 频率和停顿。常调:
- 堆大小
-Xms/-Xmx(设相等避免动态扩容抖动)。 - 新生代大小
-Xmn(让短命对象在新生代回收,别过早进老年代)。 - 选合适的 GC(吞吐选 Parallel、低延迟选 G1)。
- G1 的停顿目标
-XX:MaxGCPauseMillis、InitiatingHeapOccupancyPercent。
拓展:
- 调优要先监控定位再调(GC 日志、监控平台),别凭感觉。
- 很多"GC 问题"其实是代码问题(内存泄漏、大对象),先查代码。
往项目引 ⭐:"我项目调优是数据驱动的——先看 GC 日志和监控(Full GC 频率、停顿、各代占用),定位是对象过早晋升,再调新生代大小和晋升阈值。不是上来就堆参数。"
14. 🔴 JDK8 的内存模型相比之前有什么变化?
标准答:最大变化是永久代(PermGen)被元空间(Metaspace)取代。永久代在堆里、大小固定容易 OOM;元空间用本地内存、默认随需扩展,存类元信息。同时字符串常量池从永久代移到了堆。
拓展:
- "为什么改?"——永久代大小难调、容易
PermGen OOM;元空间用本地内存更灵活。 - 元空间也会 OOM(动态生成类太多,如大量 CGLIB 代理),可用
-XX:MaxMetaspaceSize限制。
往项目引 ⭐:"我项目是 JDK8/11,知道元空间用本地内存——排查过一次元空间 OOM,是某框架动态生成代理类太多。理解这个变化才知道去看元空间而不是堆。"
15. 🔴 强引用、软引用、弱引用、虚引用的区别?
标准答:
- 强引用:普通的
new,只要引用在就不回收(OOM 也不回收)。 - 软引用(SoftReference):内存不足时才回收,适合做缓存。
- 弱引用(WeakReference):下次 GC 就回收,如 ThreadLocalMap 的 key、WeakHashMap。
- 虚引用(PhantomReference):随时可能被回收,用于在对象回收时收到通知(管理堆外内存)。
拓展:
- "ThreadLocal 为什么 key 用弱引用?"——让 ThreadLocal 对象本身能被回收,但 value 是强引用还在,所以要 remove(内存泄漏点)。
- 软引用缓存在内存紧张时自动释放,是一种优雅降级。
往项目引 ⭐:"我项目本地缓存用过软引用——内存够时缓存命中、内存紧张时自动被回收,避免缓存把内存撑爆 OOM。也理解了 ThreadLocal 弱引用 key + 强引用 value 为什么必须 remove。"
16. 🟢 什么是 JIT?解释执行和编译执行?
标准答:Java 字节码先解释执行(一行行翻译),JVM 的 JIT(即时编译器) 会把热点代码(频繁执行的方法/循环)编译成本地机器码缓存起来,后续直接执行,大幅提速。
拓展:
- "怎么判断热点?"——方法调用计数 + 循环回边计数。
- JIT 还会做逃逸分析、内联、栈上分配等优化。
- 所以 Java"越跑越快"(热点被编译后)。
往项目引 ⭐:"理解 JIT 让我知道为什么压测要'预热'——刚启动是解释执行慢,跑一会热点被 JIT 编译后才到稳定性能,所以压测数据要看预热后的。"
17. 🔴 类加载器有哪几种?
标准答:
- 启动类加载器(Bootstrap):加载
JAVA_HOME/lib核心类(C++ 实现)。 - 扩展类加载器(Extension):加载
lib/ext。 - 应用类加载器(Application):加载 classpath 下我们写的类。
- 自定义类加载器:继承 ClassLoader 实现特殊加载(热部署、加密类、隔离)。 它们按双亲委派协作。
拓展:
- "Tomcat 为什么自定义类加载器?"——每个 webapp 一个类加载器,实现应用间类隔离(同名类不冲突),打破了双亲委派。
- 热部署、模块化(OSGi)也靠自定义类加载器。
往项目引 ⭐:"我项目部署在 Tomcat,多个应用各自的类加载器隔离,所以不同应用用不同版本的同名 jar 不会冲突——理解类加载器才明白这种隔离是怎么来的。"
18. 🟢 String 创建了几个对象?字符串常量池了解吗?
标准答:String s = "abc" 在常量池里有就复用、没有就创建一个;String s = new String("abc") 创建两个(常量池一个 + 堆里一个),返回堆里的引用。intern() 能把字符串放入/返回常量池的引用。
拓展:
- "为什么 String 不可变?"——安全(做 key、参数)、可缓存 hashcode、支持常量池复用。
- JDK7+ 字符串常量池在堆里(之前在永久代)。
- 大量字符串拼接用 StringBuilder,别用
+(每次生成新对象)。
往项目引 ⭐:"我项目里高频拼接(如拼 SQL、拼日志)一律用 StringBuilder,循环里用 + 会产生大量临时 String 对象、加重 GC——理解 String 不可变和常量池才知道为什么。"
19. 🟢 happens-before 和 Java 内存模型(JMM)了解吗?
标准答:JMM 规定了多线程下共享变量的可见性、有序性规则。每个线程有自己的工作内存,改了共享变量要刷回主内存别人才看得到。happens-before 是判断"前一个操作的结果对后一个操作是否可见"的规则,如:锁的解锁 happens-before 后续加锁、volatile 写 happens-before 后续读。
拓展:
- JMM 解决的是并发三大问题里的可见性和有序性。
- volatile、synchronized、final 都是靠 JMM 规则保证可见性/有序性。
- 和 JVM 内存结构(堆栈)是两码事,别混。
往项目引 ⭐:"理解 JMM 让我知道为什么并发下共享变量要加 volatile 或锁——不是值没改,是改了没及时刷到主内存、别的线程看到旧值。我项目状态标志位加 volatile 就是为了可见性。"
20. 🟢 你项目里有没有做过 JVM 相关的排查或优化?
标准答:结合真实经历讲——如排查 OOM(dump + MAT 找泄漏)、CPU 飙高(top -Hp + jstack)、Full GC 频繁(GC 日志定位对象晋升问题调参数)。讲清"现象 → 工具 → 定位 → 解决"。
拓展:
- 没真实经历也别瞎编,可以说"了解排查思路"并把方法论讲清。
- 工具:jstack、jmap、jstat、MAT、Arthas、GC 日志。
往项目引 ⭐:"我项目处理过 CPU 飙高(jstack 定位死循环)和内存泄漏(MAT 定位静态缓存只增不减),都是'现象→工具→定位→修复'的完整闭环。哪怕年限不长,能讲出一次真实排查经历就比纯背 JVM 概念强。"
你能答到第几层?
- 三段都能答、还能往项目引:JVM 这块你超过大多数候选人了。
- 标准答 + 拓展能成体系答:知识扎实,差把它接到一次真实排查/调优经历上。
- 标准答都磕巴:JVM 有主线(内存结构 → 类加载 → GC → 调优排查),跟着学一遍 + 动手做一次 OOM/GC 排查就懂。
这是面试专题的「JVM 篇」,网站上还有并发、MySQL、Redis、Spring、微服务、消息队列、项目场景等系统整理。 🌐 更多真实面试专题与资料:smallredtech.com 💬 想系统学 / 简历与辅导咨询,加微信:Ahongbb666(备注「面试题」)