目录
[模块一:JVM 运行时数据区(面试打底必问)](#模块一:JVM 运行时数据区(面试打底必问))
[1. 类加载的 5 个完整阶段](#1. 类加载的 5 个完整阶段)
[2. 双亲委派模型](#2. 双亲委派模型)
[模块三:垃圾回收 GC(面试重灾区,重中之重)](#模块三:垃圾回收 GC(面试重灾区,重中之重))
[1. 垃圾判定算法](#1. 垃圾判定算法)
[2. 四大垃圾回收算法](#2. 四大垃圾回收算法)
[3. 分代回收核心逻辑](#3. 分代回收核心逻辑)
[4. 核心垃圾回收器(面试必问 CMS、G1)](#4. 核心垃圾回收器(面试必问 CMS、G1))
[模块四:Java 内存模型 JMM(并发底层核心)](#模块四:Java 内存模型 JMM(并发底层核心))
[1. JMM 核心定义](#1. JMM 核心定义)
[2. JMM 三大特性(并发编程核心)](#2. JMM 三大特性(并发编程核心))
[3. Happens-Before 原则(先行发生原则)](#3. Happens-Before 原则(先行发生原则))
[JVM 与 Java 内存模型 面试模拟题(实习面试专属,附标准答案)](#JVM 与 Java 内存模型 面试模拟题(实习面试专属,附标准答案))
[一、基础必考题(8 道,中小厂实习 100% 覆盖)](#一、基础必考题(8 道,中小厂实习 100% 覆盖))
[1. JVM 运行时数据区是怎么划分的?哪些是线程私有 / 共享?每个区域的核心作用是什么?](#1. JVM 运行时数据区是怎么划分的?哪些是线程私有 / 共享?每个区域的核心作用是什么?)毁)
[2. 双亲委派模型的原理是什么?有什么核心好处?哪些场景破坏了双亲委派?](#2. 双亲委派模型的原理是什么?有什么核心好处?哪些场景破坏了双亲委派?)
[3. 可达性分析算法的 GC Roots 根对象有哪些?引用计数法有什么缺点?](#3. 可达性分析算法的 GC Roots 根对象有哪些?引用计数法有什么缺点?)
[4. 四大垃圾回收算法的优缺点和适用场景是什么?](#4. 四大垃圾回收算法的优缺点和适用场景是什么?)
[5. CMS 和 G1 收集器的核心区别、执行步骤、优缺点是什么?](#5. CMS 和 G1 收集器的核心区别、执行步骤、优缺点是什么?)
[6. Minor GC、Major GC、Full GC 的核心区别是什么?触发条件是什么?](#6. Minor GC、Major GC、Full GC 的核心区别是什么?触发条件是什么?)
[7. Java 内存模型(JMM)的三大特性是什么?分别怎么实现?](#7. Java 内存模型(JMM)的三大特性是什么?分别怎么实现?)
[8. 类加载的 5 个核心阶段是什么?准备阶段和初始化阶段的核心区别是什么?](#8. 类加载的 5 个核心阶段是什么?准备阶段和初始化阶段的核心区别是什么?)
[二、场景坑点题(6 道,大厂实习高频业务题 / 排查题)](#二、场景坑点题(6 道,大厂实习高频业务题 / 排查题))
[1. 你的校园二手平台商品批量导入功能,一次性读取 10 万条数据到内存,出现堆 OOM,怎么排查?怎么解决?](#1. 你的校园二手平台商品批量导入功能,一次性读取 10 万条数据到内存,出现堆 OOM,怎么排查?怎么解决?)
[2. 你的校园二手平台商品分类树用递归查询,分类层级过深时出现 StackOverflowError,是什么原因?怎么解决?](#2. 你的校园二手平台商品分类树用递归查询,分类层级过深时出现 StackOverflowError,是什么原因?怎么解决?)
[3. 你的项目上线后 Full GC 频繁,接口卡顿,可能有哪些原因?怎么排查优化?](#3. 你的项目上线后 Full GC 频繁,接口卡顿,可能有哪些原因?怎么排查优化?)
[4. 开发环境用 Spring Boot DevTools 热部署,出现ClassCastException: com.goods.Goods cannot be cast to com.goods.Goods,同一个类转换异常,是什么原因?怎么解决?](#4. 开发环境用 Spring Boot DevTools 热部署,出现ClassCastException: com.goods.Goods cannot be cast to com.goods.Goods,同一个类转换异常,是什么原因?怎么解决?)
[5. 项目启动时配置 JVM 参数-Xms1024m -Xmx2048m,有什么问题?最佳实践是什么?](#5. 项目启动时配置 JVM 参数-Xms1024m -Xmx2048m,有什么问题?最佳实践是什么?)
[6. 你的项目用 volatile 修饰接口限流开关,后台修改开关值后,所有线程都能立即生效,底层原理是什么?](#6. 你的项目用 volatile 修饰接口限流开关,后台修改开关值后,所有线程都能立即生效,底层原理是什么?)
[三、进阶原理题(4 道,中大厂实习拔高题)](#三、进阶原理题(4 道,中大厂实习拔高题))
[1. JVM 为什么要设计分代回收?为什么年轻代用标记复制算法,老年代用标记整理?](#1. JVM 为什么要设计分代回收?为什么年轻代用标记复制算法,老年代用标记整理?)
[2. CMS 收集器为什么会有浮动垃圾?为什么用标记清除不用标记整理?](#2. CMS 收集器为什么会有浮动垃圾?为什么用标记清除不用标记整理?)
[3. G1 收集器的 Region 设计有什么核心优势?和传统分代的 CMS 核心区别是什么?](#3. G1 收集器的 Region 设计有什么核心优势?和传统分代的 CMS 核心区别是什么?)
[4. 什么是 Happens-Before 原则?为什么需要这个原则?核心规则有哪些?](#4. 什么是 Happens-Before 原则?为什么需要这个原则?核心规则有哪些?)
模块一:JVM 运行时数据区(面试打底必问)
核心定义
JVM 运行时将内存划分为 5 个核心区域,分为线程私有 和线程共享两大类:
| 区域 | 线程私有 / 共享 | 核心作用 | 异常场景 |
|---|---|---|---|
| 程序计数器 | 线程私有 | 记录当前线程执行的字节码指令行号,线程切换后恢复执行位置 | 唯一无 OOM 的区域 |
| 虚拟机栈 | 线程私有 | 每个方法执行时创建栈帧,存储局部变量表、操作数栈、方法出口等,方法调用和执行的内存模型 | StackOverflowError(栈深度溢出)、OOM(栈内存不足) |
| 本地方法栈 | 线程私有 | 为 Native(本地)方法服务,作用和虚拟机栈一致 | 同虚拟机栈 |
| 堆(Heap) | 线程共享 | JVM 最大的内存区域,所有对象实例、数组都存在这里,是 GC 垃圾回收的核心区域 | OOM(对象过多、内存泄漏、堆内存不足) |
| 方法区(元空间) | 线程共享 | 存储类的元信息、静态变量、常量、即时编译后的代码;JDK8 之前叫永久代(堆内),JDK8 之后移到本地内存,改名元空间 | OOM(加载的类过多、动态生成类过多) |
补充:运行时常量池、StringTable(字符串常量池)JDK7 之后移到堆内存,JDK8 之后永久代完全被元空间替代。
个人理解
JVM 内存划分是 Java 跨平台的核心基础,屏蔽了不同操作系统的内存管理差异;线程私有区域随线程创建销毁,线程共享区域随 JVM 启动销毁;90% 的线上 JVM 问题都出在堆内存,是调优和排查的核心。
项目实际使用场景
结合校园二手平台开发实践:
- 栈溢出问题 :商品分类树的递归查询,递归层级过深导致
StackOverflowError,后来改成迭代实现解决; - 堆 OOM 问题:商品批量导入时,一次性把 10 万条数据全部读入内存,导致堆 OOM,后来改成分批读取 + 手动 GC 解决;商品大图片上传,直接把整个文件读入字节数组,导致堆内存溢出,改成分片上传 + NIO 零拷贝优化;
- 元空间 OOM :项目集成大量动态代理、AOP 切面,动态生成的类过多,元空间不足,启动时增加元空间最大内存参数
-XX:MaxMetaspaceSize=256m解决; - JVM 启动参数配置 :项目启动时配置堆内存
-Xms1024m -Xmx1024m(初始堆和最大堆一致,避免扩容),新生代和老年代比例 1:2,优化 GC 性能。
面试考点标注
✅ 必问:JVM 运行时数据区的完整划分,哪些是线程私有 / 共享,每个区域的作用;
✅ 必问:JDK8 和之前版本的方法区(永久代 / 元空间)的区别;
✅ 场景题:常见的 OOM 场景有哪些?怎么排查 OOM 问题?
【
常见 OOM:
堆溢出、元空间溢出、栈溢出、直接内存溢出、GC 超限、线程过多、内存泄漏。
排查方法:
加 JVM 参数自动生成 dump,用 MAT/JProfiler 分析大对象和引用链,定位代码并优化内存使用、防止泄漏、合理分页与淘汰。
】
✅ 细节:程序计数器为什么是线程私有?为什么没有 OOM?
模块二:类加载机制(面试高频核心)
1. 类加载的 5 个完整阶段
核心定义
一个类从加载到 JVM 到卸载,完整生命周期分为 7 个阶段,核心加载过程 5 个阶段:
- 加载:通过类的全限定名读取字节码,在堆中生成 Class 对象,作为方法区类信息的入口;
- 验证:校验字节码的合法性,确保不会危害 JVM 安全(文件格式、语义、字节码、符号引用验证);
- 准备 :给类的静态变量分配内存,赋默认初始值(比如 int=0、引用类型 = null),常量直接赋值;
- 解析:把常量池的符号引用转为直接引用(内存地址);
- 初始化 :执行静态代码块、给静态变量赋实际定义的值,是类加载的最后一步。
触发类初始化的场景:new 对象、调用静态方法 / 变量、反射、子类初始化、主类启动。
2. 双亲委派模型
核心定义
(1)类加载器层级
Java 有四种类加载器,层级从高到低:
- Bootstrap ClassLoader(启动类加载器):C++ 实现,加载 JDK 核心类(java.lang.*、java.util.* 等),开发者无法直接获取;
- Extension ClassLoader(扩展类加载器):加载 JDK 扩展包 lib/ext 下的类;
- Application ClassLoader(应用程序类加载器):加载用户 classpath 下的类,是默认的类加载器;
- 自定义类加载器:用户自定义,加载指定路径的类,实现热部署、加密类加载等。
(2)双亲委派核心逻辑
加载类时,先委托父类加载器加载,父加载器找不到,再自己加载,全程向上委托,向下加载。
(3)核心好处
- 核心类安全 :避免核心类被篡改,比如用户自己写的
java.lang.String不会被加载,保证 JDK 核心类的唯一性; - 类的唯一性:同一个类被不同类加载器加载,会被认为是不同的类,双亲委派保证同一个类只会被加载一次。
(4)破坏双亲委派的场景
- SPI 机制:JDBC、JNDI 等服务发现接口,核心接口由 Bootstrap 加载,实现类由 Application 加载,父加载器委托子加载器加载实现类,破坏了双亲委派;
- Tomcat 类加载器:Web 应用隔离,每个 Web 应用有自己的类加载器,优先加载自己的类,不委托父加载器,实现应用隔离;
- 热部署 / 热加载:OSGi、Spring Boot DevTools,自定义类加载器,重新加载修改的类,实现热更新。
个人理解
双亲委派是 Java 类加载的核心设计,本质是优先级机制,保证了 JDK 核心类的安全和稳定;破坏双亲委派不是 bug,是为了解决「父加载器加载的接口,需要子加载器加载实现类」的场景,是灵活的扩展设计。
项目实际使用场景
- SPI 机制应用:项目中集成的 JDBC 驱动、Spring 的 SPI 扩展,都是破坏双亲委派的实现,核心接口由父加载器加载,实现类由应用类加载器加载;
- 热部署:开发环境用 Spring Boot DevTools,自定义类加载器,修改代码后自动重新加载类,不用重启项目;
- 类加载问题排查 :项目中出现
ClassCastException,同一个类被两个类加载器加载,导致类型转换失败,排查类加载器来源解决。
面试考点标注
✅ 必问:类加载的 5 个阶段,准备阶段和初始化阶段的区别;
✅ 必问:双亲委派模型的原理、好处、破坏场景;
✅ 必问:什么情况下会触发类的初始化;
✅ 细节:两个不同的类加载器加载同一个类,是不是同一个类?(不是,Class 对象不同)。
模块三:垃圾回收 GC(面试重灾区,重中之重)
1. 垃圾判定算法
核心定义
(1)引用计数法
给对象加一个引用计数器,被引用 + 1,引用失效 - 1,计数器为 0 就是垃圾;缺点:无法解决循环引用的问题(A 引用 B,B 引用 A,没有其他引用,计数器永远不为 0,无法回收),Java 不采用。
(2)可达性分析算法(Java 采用)
以GC Roots 为起点,向下搜索,搜索不到的对象就是垃圾,可以被回收;GC Roots 根对象(必背):
- 虚拟机栈中局部变量表引用的对象;
- 方法区中静态变量、常量引用的对象;
- 本地方法栈 JNI 引用的对象;
- JVM 内部的核心类对象、锁对象等。
2. 四大垃圾回收算法
| 算法 | 核心逻辑 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记 - 清除 | 先标记存活对象,再清除垃圾对象 | 实现简单,不需要移动对象 | 产生大量内存碎片,分配大对象时找不到连续内存 | 老年代 |
| 标记 - 复制 | 内存分为两块,一块用完,把存活对象复制到另一块,清空整个区域 | 无内存碎片,实现简单 | 内存利用率只有 50%,存活对象多的时候复制开销大 | 年轻代(对象朝生夕死,存活率低) |
| 标记 - 整理 | 先标记存活对象,把存活对象整理到内存一端,清除剩下的区域 | 无内存碎片,内存利用率 100% | 需要移动对象,性能开销大 | 老年代(对象存活率高) |
| 分代收集算法 | 把堆分为年轻代、老年代,不同代用不同算法 | 兼顾性能和内存利用率,是 JVM 的默认算法 | 无 | JVM 通用回收算法 |
3. 分代回收核心逻辑
JVM 堆内存默认分为年轻代和老年代,比例 1:2:
(1)年轻代
- 分为 Eden 区、Survivor0(S0)、Survivor1(S1),默认比例 8:1:1;
- 对象优先在 Eden 区分配,Eden 区满了触发 Minor GC(年轻代 GC),把存活对象复制到 S0/S1,对象年龄 + 1;
- 核心特点:对象朝生夕死,存活率极低,用标记 - 复制算法,Minor GC 频率高,速度快,STW(Stop The World)时间短。
(2)老年代
- 存放长期存活的对象、大对象(超过阈值的大对象直接进入老年代);
- 对象年龄达到阈值(默认 15),从年轻代晋升到老年代;
- 核心特点:对象存活率高,用标记 - 清除 / 标记 - 整理算法,Major GC/Full GC 频率低,速度慢,STW 时间长。
4. 核心垃圾回收器(面试必问 CMS、G1)
(1)CMS 收集器(Concurrent Mark Sweep)
- 定位:老年代收集器,以低延迟为目标,适合互联网 Web 项目、对响应时间要求高的服务;
- 核心步骤 :
- 初始标记:STW,只标记 GC Roots 直接关联的对象,速度极快;
- 并发标记:和用户线程并发执行,遍历所有存活对象,无 STW;
- 重新标记:STW,修正并发标记期间变化的对象,速度快;
- 并发清除:和用户线程并发执行,清除垃圾对象,无 STW;
- 缺点:内存碎片、CPU 占用高、并发清除期间产生的浮动垃圾无法处理。
(2)G1 收集器(Garbage First)
- 定位:兼顾年轻代和老年代的区域化分代收集器,JDK9 之后默认收集器,适合大内存(4G 以上)服务,可控制最大停顿时间;
- 核心设计:把堆分成多个大小相等的 Region,每个 Region 可以是 Eden、S、老年代,优先回收垃圾最多的 Region;
- 核心步骤:初始标记→并发标记→最终标记→筛选回收(STW,按停顿时间优先回收垃圾最多的 Region);
- 优势:可控的停顿时间、无内存碎片、兼顾吞吐量和延迟,是现在的主流收集器。
了解即可:ZGC、Shenandoah:超低延迟收集器,TB 级内存,STW 时间不超过 1ms,适合超大内存服务。
个人理解
GC 的核心是「找到垃圾,回收垃圾」,分代回收是 JVM 经过验证的最优方案,针对不同生命周期的对象用不同算法,平衡性能和内存;垃圾回收器的选择核心是「延迟优先还是吞吐量优先」,互联网项目优先低延迟的 CMS/G1。
项目实际使用场景
- GC 参数配置:校园二手平台服务用 G1 收集器,配置最大停顿时间 200ms,堆内存 1G,满足接口低延迟要求;
- OOM 排查 :项目出现堆 OOM 时,配置
-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动 dump 堆文件,用 MAT 工具分析,发现是商品本地缓存没有设置过期时间,导致内存泄漏,解决后 GC 恢复正常; - GC 优化:大对象直接进入老年代,导致 Full GC 频繁,调整大对象阈值,分批处理大文件,减少 Full GC 次数。
面试考点标注
✅ 必问:可达性分析算法的 GC Roots 有哪些?
✅ 必问:四大垃圾回收算法的优缺点、适用场景;
✅ 必问:年轻代和老年代的回收逻辑,Minor GC 和 Full GC 的区别;
✅ 必问:CMS 和 G1 的核心区别、执行步骤、优缺点;
✅ 场景题:OOM 怎么排查?内存泄漏怎么定位?
模块四:Java 内存模型 JMM(并发底层核心)
1. JMM 核心定义
Java 内存模型(Java Memory Model,JMM)是为了屏蔽不同硬件、操作系统的内存访问差异,定义了线程和主内存的交互规则,保证多线程场景下的原子性、可见性、有序性。
- 主内存:所有共享变量都存在主内存,所有线程共享;
- 工作内存:每个线程有自己的工作内存,保存该线程使用的变量的副本,线程对变量的所有操作都在工作内存中完成,再同步回主内存。
2. JMM 三大特性(并发编程核心)
| 特性 | 定义 | 实现方式 |
|---|---|---|
| 原子性 | 一个操作要么全部执行,要么全部不执行,中间不会被中断 | synchronized、Lock、原子类 Atomic |
| 可见性 | 一个线程修改了共享变量的值,其他线程能立即看到最新值 | volatile、synchronized、final |
| 有序性 | 禁止编译器和 CPU 对指令进行重排序,保证指令执行顺序和代码顺序一致 | volatile、synchronized |
3. Happens-Before 原则(先行发生原则)
JMM 定义的一套可见性规则,不需要加锁,只要满足规则,就保证可见性,是判断多线程场景下是否有可见性问题的核心依据,核心规则(必背):
- 程序次序规则:同一个线程内,前面的操作 Happens-Before 后面的操作;
- 锁定规则:同一个锁的 unlock 操作 Happens-Before 后续的 lock 操作;
- volatile 变量规则:对 volatile 变量的写操作 Happens-Before 后续对这个变量的读操作;
- 传递规则:A Happen-Before B,B Happen-Before C,那么 A Happen-Before C;
- 线程启动规则:线程的 start () 方法 Happens-Before 线程的所有操作;
- 线程中断规则:对线程 interrupt () 的调用 Happens-Before 线程检测到中断事件。
个人理解
JMM 是多线程安全的底层保障,解决的是多线程场景下「变量可见性、指令重排序」的问题;Happens-Before 原则是 JMM 给开发者的承诺,不需要理解底层实现,只要遵守规则就能保证线程安全。
项目实际使用场景
- volatile 的应用:项目中的服务开关、灰度配置用 volatile 修饰,保证修改后所有线程立即看到最新值,底层就是 volatile 的 Happens-Before 规则;
- DCL 单例:项目中的单例线程池用 volatile 修饰,禁止指令重排,避免半初始化对象,就是利用 volatile 的有序性和可见性;
- 锁的可见性:synchronized 加锁解锁,保证锁内的变量修改对其他线程可见,底层是锁定规则。
面试考点标注
✅ 必问:JMM 的三大特性是什么?分别怎么实现?
✅ 必问:volatile 怎么保证可见性和有序性?
✅ 必问:Happens-Before 核心规则有哪些?
✅ 原理:多线程场景下为什么会有可见性、指令重排序问题?
当日验收清单
- 不看资料口述:JVM 运行时数据区完整结构、双亲委派模型完整流程、分代回收的核心逻辑;
- 结合项目口述:你的项目 JVM 参数怎么配置的,有没有遇到过 OOM,怎么排查的;
- 避坑确认:元空间 OOM 的场景、内存泄漏和内存溢出的区别、Full GC 频繁的常见原因。
JVM 与 Java 内存模型 面试模拟题(实习面试专属,附标准答案)
一、基础必考题(8 道,中小厂实习 100% 覆盖)
1. JVM 运行时数据区是怎么划分的?哪些是线程私有 / 共享?每个区域的核心作用是什么?
【标准答案】JVM 运行时内存分为 5 大核心区域,分为线程私有和线程共享两大类:
(1)线程私有(随线程创建 / 销毁)
- 程序计数器 :记录当前线程执行的字节码指令行号,线程切换后恢复执行位置,是唯一不会出现 OOM 的区域;
- 虚拟机栈:每个方法执行时创建栈帧,存储局部变量表、操作数栈、方法出口等,是 Java 方法执行的内存模型;
- 本地方法栈:为 Native 本地方法服务,作用和虚拟机栈完全一致。
(2)线程共享(随 JVM 启动 / 销毁)
- 堆(Heap):JVM 最大的内存区域,所有对象实例、数组都存在这里,是 GC 垃圾回收的核心区域;
- 方法区(元空间):存储类的元信息、静态变量、常量、即时编译后的代码;JDK8 之前叫永久代(堆内),JDK8 之后移到本地内存,改名为元空间。
补充:StringTable(字符串常量池)、运行时常量池 JDK7 之后移到堆内存。
【考点对应】模块一:JVM 运行时数据区
2. 双亲委派模型的原理是什么?有什么核心好处?哪些场景破坏了双亲委派?
【标准答案】
(1)核心原理
类加载时,先向上委托父类加载器加载,父加载器找不到,再自己加载,全程向上委托、向下加载。类加载器层级从高到低:
- 启动类加载器:加载 JDK 核心类;
- 扩展类加载器:加载 JDK 扩展包类;
- 应用类加载器:加载用户 classpath 下的类;
- 自定义类加载器:加载自定义路径的类。
(2)核心好处
- 核心类安全 :避免 JDK 核心类被篡改,比如用户自定义的
java.lang.String不会被加载,保证核心类的唯一性; - 类的唯一性:同一个类只会被加载一次,避免重复加载。
(3)破坏双亲委派的场景
- SPI 机制:JDBC、JNDI 等,核心接口由父加载器加载,实现类由子加载器加载;
- Tomcat 类加载器:Web 应用隔离,每个应用优先加载自己的类,不委托父加载器;
- 热部署 / 热加载:Spring Boot DevTools、OSGi,自定义类加载器重新加载修改的类。
【考点对应】模块二:双亲委派模型
3. 可达性分析算法的 GC Roots 根对象有哪些?引用计数法有什么缺点?
【标准答案】
(1)GC Roots 根对象(必背)
- 虚拟机栈中局部变量表引用的对象;
- 方法区中静态变量、常量引用的对象;
- 本地方法栈 JNI 引用的对象;
- JVM 内部的核心类对象、锁对象等。
(2)引用计数法的缺点
给对象加引用计数器,被引用 + 1,引用失效 - 1,计数器为 0 就是垃圾;核心缺点是无法解决循环引用问题(A 引用 B、B 引用 A,无其他引用,计数器永远不为 0,无法回收),因此 Java 不采用该算法。
【考点对应】模块三:垃圾判定算法
4. 四大垃圾回收算法的优缺点和适用场景是什么?
【标准答案】
| 算法 | 核心逻辑 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记 - 清除 | 标记存活对象,清除垃圾对象 | 实现简单,无需移动对象 | 产生大量内存碎片 | 老年代 |
| 标记 - 复制 | 内存分两块,存活对象复制到另一块,清空原区域 | 无内存碎片,实现简单 | 内存利用率仅 50%,存活对象多复制开销大 | 年轻代(对象朝生夕死,存活率低) |
| 标记 - 整理 | 标记存活对象,整理到内存一端,清除剩余区域 | 无内存碎片,内存利用率 100% | 需要移动对象,性能开销大 | 老年代(对象存活率高) |
| 分代收集 | 堆分年轻代、老年代,不同代用不同算法 | 兼顾性能和内存利用率,JVM 默认算法 | 无 | 所有 JVM 通用 |
【考点对应】模块三:垃圾回收算法
5. CMS 和 G1 收集器的核心区别、执行步骤、优缺点是什么?
【标准答案】
(1)核心区别
| 对比维度 | CMS | G1 |
|---|---|---|
| 定位 | 老年代低延迟收集器 | 全堆分代收集器,兼顾年轻代 + 老年代 |
| 内存设计 | 传统分代,连续内存 | 区域化分代,堆拆分为多个独立 Region |
| 目标 | 最小化停顿时间 | 可控制最大停顿时间,兼顾吞吐量和延迟 |
| 内存碎片 | 标记清除,有内存碎片 | 标记整理,无内存碎片 |
| 适用场景 | 4G 以下小内存,低延迟 Web 服务 | 4G 以上大内存,对停顿时间有要求的服务 |
(2)执行步骤
- CMS:初始标记(STW 极短)→ 并发标记(无 STW)→ 重新标记(STW 短)→ 并发清除(无 STW)
- G1:初始标记(STW 极短)→ 并发标记(无 STW)→ 最终标记(STW 短)→ 筛选回收(STW,按停顿时间优先回收垃圾最多的 Region)
(3)优缺点
- CMS 优点:低延迟,并发执行;缺点:内存碎片、CPU 占用高、浮动垃圾无法处理;
- G1 优点:可控停顿时间、无内存碎片、大内存性能优;缺点:小内存场景性能不如 CMS。
【考点对应】模块三:核心垃圾回收器
6. Minor GC、Major GC、Full GC 的核心区别是什么?触发条件是什么?
【标准答案】
| 类型 | 作用区域 | 特点 | 触发条件 |
|---|---|---|---|
| Minor GC | 年轻代 | 频率高、速度快、STW 时间极短 | Eden 区满了 |
| Major GC | 老年代 | 频率低、速度慢、STW 时间长 | 老年代空间不足、晋升担保失败 |
| Full GC | 全堆(年轻代 + 老年代 + 元空间) | STW 时间最长,对业务影响最大 | 老年代满、元空间满、System.gc () 主动触发 |
【考点对应】模块三:分代回收核心逻辑
7. Java 内存模型(JMM)的三大特性是什么?分别怎么实现?
【标准答案】JMM 是为了屏蔽不同硬件的内存差异,定义的多线程交互规则,三大核心特性:
- 原子性:操作要么全部执行要么不执行,实现方式:synchronized、Lock、原子类 Atomic;
- 可见性:一个线程修改共享变量,其他线程能立即看到最新值,实现方式:volatile、synchronized、final;
- 有序性:禁止指令重排序,实现方式:volatile、synchronized。
【考点对应】模块四:JMM 三大特性
8. 类加载的 5 个核心阶段是什么?准备阶段和初始化阶段的核心区别是什么?
【标准答案】
5 个核心阶段
- 加载:读取字节码,生成 Class 对象;
- 验证:校验字节码合法性,保证 JVM 安全;
- 准备 :给静态变量分配内存,赋默认初始值(int=0、引用 = null),常量直接赋值;
- 解析:符号引用转为直接内存地址;
- 初始化 :执行静态代码块,给静态变量赋业务定义的实际值。
核心区别
- 准备阶段:JVM 自动赋默认值,不执行用户代码;
- 初始化阶段:执行用户定义的静态代码和赋值逻辑。
【考点对应】模块二:类加载 5 个阶段
二、场景坑点题(6 道,大厂实习高频业务题 / 排查题)
1. 你的校园二手平台商品批量导入功能,一次性读取 10 万条数据到内存,出现堆 OOM,怎么排查?怎么解决?
【标准答案】
排查步骤
- 启动时加 JVM 参数
-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动生成堆 dump 文件; - 用 MAT、JProfiler 工具分析 dump 文件,找到占用内存最大的对象,确认是商品对象集合;
- 查看 GC 日志,确认 Full GC 频繁,老年代占满无法回收。
解决方案
- 分批读取:每次读取 1000 条,处理完释放引用,再读取下一批,避免全量加载到内存;
- 避免强引用缓存:处理完的商品对象及时置空,方便 GC 回收;
- 优化 JVM 参数:适当调大堆内存,调整年轻代比例,减少对象直接晋升老年代;
- 大文件优化:用 NIO 流式读取,不一次性加载整个文件到内存。
【考点对应】模块一:OOM 排查与优化
2. 你的校园二手平台商品分类树用递归查询,分类层级过深时出现 StackOverflowError,是什么原因?怎么解决?
【标准答案】
原因
递归调用时,每个方法都会在虚拟机栈中创建一个栈帧,递归层级过深,栈深度超过 JVM 默认的栈深度(默认 1M 左右),触发栈溢出错误。
解决方案
- 最优方案:改成迭代实现:用栈数据结构模拟递归,避免方法调用的栈帧创建,无层级限制;
- 临时方案:调大栈内存 :JVM 参数
-Xss2m调大栈内存,只能缓解,不能根治,不推荐; - 业务优化:限制分类最大层级,避免过深的递归。
【考点对应】模块一:虚拟机栈溢出问题
3. 你的项目上线后 Full GC 频繁,接口卡顿,可能有哪些原因?怎么排查优化?
【标准答案】
常见原因
- 内存泄漏:本地缓存、静态集合没有过期清理,对象无法回收,老年代占满;
- 大对象过多:大文件、大集合直接进入老年代,老年代快速占满;
- 元空间不足:动态生成类过多(AOP、动态代理),元空间满触发 Full GC;
- JVM 参数不合理:堆内存太小、年轻代比例太小,对象频繁晋升老年代。
排查优化
- 查看 GC 日志,确认 Full GC 的触发原因;
- dump 堆文件,分析是否有内存泄漏,修复泄漏点(比如给缓存加过期时间);
- 调整大对象阈值,避免大对象直接进入老年代,分批处理大对象;
- 调大元空间最大内存
-XX:MaxMetaspaceSize=256m; - 合理配置堆内存,年轻代和老年代比例 1:2,用 G1 收集器控制停顿时间。
【考点对应】模块三:GC 问题排查优化
4. 开发环境用 Spring Boot DevTools 热部署,出现ClassCastException: com.goods.Goods cannot be cast to com.goods.Goods,同一个类转换异常,是什么原因?怎么解决?
【标准答案】
原因
类的唯一性由「全类名 + 类加载器」共同决定,同一个类被两个不同的类加载器加载,会被 JVM 认为是完全不同的类,因此转换失败。Spring Boot DevTools 热部署时,会用自定义的 RestartClassLoader 重新加载修改的类,原来的类是应用类加载器加载的,因此出现同一个类转换异常。
解决方案
- 热部署环境避免跨类加载器传递对象;
- 生产环境关闭热部署,不用 DevTools;
- 重启项目,清空旧的类加载器缓存。
【考点对应】模块二:类加载器唯一性
5. 项目启动时配置 JVM 参数-Xms1024m -Xmx2048m,有什么问题?最佳实践是什么?
【标准答案】
问题
-Xms是初始堆内存,-Xmx是最大堆内存,两者不一致时:
- 堆内存不足时会触发扩容,扩容过程会发生 Full GC,影响服务性能;
- 内存碎片会更多,GC 效率降低。
最佳实践
生产环境将初始堆和最大堆设置为相同值,比如-Xms1024m -Xmx1024m,避免堆扩容的 GC 开销,提升稳定性;同时设置-XX:+AlwaysPreTouch,启动时预分配所有堆内存,提升运行时性能。
【考点对应】模块一:JVM 参数最佳实践
6. 你的项目用 volatile 修饰接口限流开关,后台修改开关值后,所有线程都能立即生效,底层原理是什么?
【标准答案】volatile 通过两个机制保证可见性:
- 内存屏障:volatile 写操作后加写屏障,强制把工作内存的修改立即刷回主内存;volatile 读操作前加读屏障,强制从主内存读取最新值;
- MESI 缓存一致性协议:CPU 通过该协议保证多个核心的缓存一致性,一个核心修改了 volatile 变量,其他核心的缓存会立即失效,必须从主内存读取最新值。同时符合 JMM 的 Happens-Before 原则:对 volatile 变量的写操作,先行发生于后续对这个变量的读操作。
【考点对应】模块四:volatile 可见性原理
三、进阶原理题(4 道,中大厂实习拔高题)
1. JVM 为什么要设计分代回收?为什么年轻代用标记复制算法,老年代用标记整理?
【标准答案】
分代回收的原因
基于统计规律:98% 的对象都是朝生夕死,存活时间极短,只有少量对象长期存活。不分代的话,所有对象放在一起,每次 GC 都要扫描全堆,性能极差;分代后针对不同生命周期的对象用不同算法,大幅提升 GC 效率。
算法选择的原因
- 年轻代用标记复制:年轻代对象存活率极低,每次 GC 只有少量对象存活,复制开销极小,无内存碎片,性能最高;虽然内存利用率 50%,但年轻代内存占比小,浪费可以接受;
- 老年代用标记整理:老年代对象存活率高,用标记复制的话复制开销极大;用标记清除会产生大量内存碎片,大对象无法分配;标记整理无内存碎片,内存利用率 100%,适合老年代。
【考点对应】模块三:分代回收设计原理
2. CMS 收集器为什么会有浮动垃圾?为什么用标记清除不用标记整理?
【标准答案】
浮动垃圾的原因
CMS 的并发清除阶段,用户线程和 GC 线程并发执行,清除过程中用户线程会产生新的垃圾对象,这些对象无法在本次 GC 中被标记,只能等到下一次 GC 回收,这部分就是浮动垃圾。
用标记清除的原因
- 并发清除阶段如果用标记整理,需要移动存活对象的内存地址,用户线程正在访问对象,会导致引用错乱,无法并发执行;
- 标记清除不需要移动对象,支持和用户线程并发执行,符合 CMS 低延迟的设计目标;
- 内存碎片问题可以通过参数设置,在 Full GC 时做一次标记整理来缓解。
【考点对应】模块三:CMS 收集器原理
3. G1 收集器的 Region 设计有什么核心优势?和传统分代的 CMS 核心区别是什么?
【标准答案】
Region 设计的核心优势
- 可控的停顿时间:G1 可以预测每个 Region 的回收时间,根据配置的最大停顿时间,优先回收垃圾最多的 Region,精准控制 STW 时间;
- 无内存碎片:每个 Region 回收用标记复制,整体是标记整理,全程无内存碎片;
- 大内存性能优:传统分代 GC 扫描全堆,大内存下 STW 时间不可控;G1 只扫描需要回收的 Region,大内存下性能远优于 CMS;
- 灵活的分代:每个 Region 可以动态切换为 Eden、Survivor、老年代,不需要固定的内存划分,内存利用率更高。
和 CMS 的核心区别
- CMS 是老年代收集器,只能回收老年代,G1 是全堆收集器,兼顾年轻代和老年代;
- CMS 是连续内存分代,G1 是离散 Region 分代;
- CMS 无法控制停顿时间,G1 可以设置最大停顿时间;
- CMS 有内存碎片,G1 无内存碎片。
【考点对应】模块三:G1 收集器设计原理
4. 什么是 Happens-Before 原则?为什么需要这个原则?核心规则有哪些?
【标准答案】
定义
Happens-Before 是 JMM 给开发者的可见性承诺:如果 A 操作 Happens-Before B 操作,那么 A 操作的结果对 B 操作可见,不需要开发者额外加锁控制。
为什么需要
JMM 为了性能,允许指令重排序和工作内存缓存,多线程场景下可见性无法保证;Happens-Before 原则给了开发者一套简单的规则,不需要理解底层复杂的内存屏障,只要遵守规则就能保证线程安全。
核心规则(必背)
- 程序次序规则:同一个线程内,前面的操作先行发生于后面的操作;
- 锁定规则:同一个锁的 unlock 操作,先行发生于后续的 lock 操作;
- volatile 规则:对 volatile 变量的写操作,先行发生于后续对这个变量的读操作;
- 传递规则:A Happen-Before B,B Happen-Before C,则 A Happen-Before C;
- 线程启动规则:线程的 start () 方法先行发生于线程的所有操作;
- 线程中断规则:interrupt () 调用先行发生于线程检测到中断事件。
【考点对应】模块四:Happens-Before 原则
实习面试答题加分技巧(绑定你的校园二手平台项目)
- 所有 JVM 题都绑定项目踩坑经验 :
- 问 OOM 排查:主动说「我做校园二手平台商品批量导入的时候,一次性加载 10 万条数据踩过 OOM 的坑,后来加了 dump 参数,用 MAT 分析是商品集合占满了堆,改成分批读取就解决了」;
- 问 GC 优化:主动说「我项目用的 G1 收集器,配置最大停顿时间 200ms,堆内存 1G,之前大对象过多导致 Full GC 频繁,调整了大对象阈值,分批处理图片就好了」;
- 主动说参数配置实践:问 JVM 参数就说自己项目的启动参数配置,为什么这么设置,比单纯背参数加分很多;
- 答题逻辑固定为「结论→原理→我的项目实践 / 踩坑」,面试官会认为你有实际生产经验,不是纯背题。