JVM 与 Java 内存模型 专属复习笔记

目录

[模块一: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 问题都出在堆内存,是调优和排查的核心。


项目实际使用场景

结合校园二手平台开发实践:

  1. 栈溢出问题 :商品分类树的递归查询,递归层级过深导致StackOverflowError,后来改成迭代实现解决;
  2. 堆 OOM 问题:商品批量导入时,一次性把 10 万条数据全部读入内存,导致堆 OOM,后来改成分批读取 + 手动 GC 解决;商品大图片上传,直接把整个文件读入字节数组,导致堆内存溢出,改成分片上传 + NIO 零拷贝优化;
  3. 元空间 OOM :项目集成大量动态代理、AOP 切面,动态生成的类过多,元空间不足,启动时增加元空间最大内存参数-XX:MaxMetaspaceSize=256m解决;
  4. JVM 启动参数配置 :项目启动时配置堆内存-Xms1024m -Xmx1024m(初始堆和最大堆一致,避免扩容),新生代和老年代比例 1:2,优化 GC 性能。

面试考点标注

✅ 必问:JVM 运行时数据区的完整划分,哪些是线程私有 / 共享,每个区域的作用;

✅ 必问:JDK8 和之前版本的方法区(永久代 / 元空间)的区别;

✅ 场景题:常见的 OOM 场景有哪些?怎么排查 OOM 问题?

常见 OOM:

堆溢出、元空间溢出、栈溢出、直接内存溢出、GC 超限、线程过多、内存泄漏。

排查方法:

加 JVM 参数自动生成 dump,用 MAT/JProfiler 分析大对象和引用链,定位代码并优化内存使用、防止泄漏、合理分页与淘汰。

✅ 细节:程序计数器为什么是线程私有?为什么没有 OOM?


模块二:类加载机制(面试高频核心)

1. 类加载的 5 个完整阶段

核心定义

一个类从加载到 JVM 到卸载,完整生命周期分为 7 个阶段,核心加载过程 5 个阶段:

  1. 加载:通过类的全限定名读取字节码,在堆中生成 Class 对象,作为方法区类信息的入口;
  2. 验证:校验字节码的合法性,确保不会危害 JVM 安全(文件格式、语义、字节码、符号引用验证);
  3. 准备 :给类的静态变量分配内存,赋默认初始值(比如 int=0、引用类型 = null),常量直接赋值;
  4. 解析:把常量池的符号引用转为直接引用(内存地址);
  5. 初始化 :执行静态代码块、给静态变量赋实际定义的值,是类加载的最后一步。

触发类初始化的场景:new 对象、调用静态方法 / 变量、反射、子类初始化、主类启动。


2. 双亲委派模型

核心定义
(1)类加载器层级

Java 有四种类加载器,层级从高到低:

  1. Bootstrap ClassLoader(启动类加载器):C++ 实现,加载 JDK 核心类(java.lang.*、java.util.* 等),开发者无法直接获取;
  2. Extension ClassLoader(扩展类加载器):加载 JDK 扩展包 lib/ext 下的类;
  3. Application ClassLoader(应用程序类加载器):加载用户 classpath 下的类,是默认的类加载器;
  4. 自定义类加载器:用户自定义,加载指定路径的类,实现热部署、加密类加载等。
(2)双亲委派核心逻辑

加载类时,先委托父类加载器加载,父加载器找不到,再自己加载,全程向上委托,向下加载。

(3)核心好处
  1. 核心类安全 :避免核心类被篡改,比如用户自己写的java.lang.String不会被加载,保证 JDK 核心类的唯一性;
  2. 类的唯一性:同一个类被不同类加载器加载,会被认为是不同的类,双亲委派保证同一个类只会被加载一次。
(4)破坏双亲委派的场景
  1. SPI 机制:JDBC、JNDI 等服务发现接口,核心接口由 Bootstrap 加载,实现类由 Application 加载,父加载器委托子加载器加载实现类,破坏了双亲委派;
  2. Tomcat 类加载器:Web 应用隔离,每个 Web 应用有自己的类加载器,优先加载自己的类,不委托父加载器,实现应用隔离;
  3. 热部署 / 热加载:OSGi、Spring Boot DevTools,自定义类加载器,重新加载修改的类,实现热更新。

个人理解

双亲委派是 Java 类加载的核心设计,本质是优先级机制,保证了 JDK 核心类的安全和稳定;破坏双亲委派不是 bug,是为了解决「父加载器加载的接口,需要子加载器加载实现类」的场景,是灵活的扩展设计。


项目实际使用场景

  1. SPI 机制应用:项目中集成的 JDBC 驱动、Spring 的 SPI 扩展,都是破坏双亲委派的实现,核心接口由父加载器加载,实现类由应用类加载器加载;
  2. 热部署:开发环境用 Spring Boot DevTools,自定义类加载器,修改代码后自动重新加载类,不用重启项目;
  3. 类加载问题排查 :项目中出现ClassCastException,同一个类被两个类加载器加载,导致类型转换失败,排查类加载器来源解决。

面试考点标注

✅ 必问:类加载的 5 个阶段,准备阶段和初始化阶段的区别;

✅ 必问:双亲委派模型的原理、好处、破坏场景;

✅ 必问:什么情况下会触发类的初始化;

✅ 细节:两个不同的类加载器加载同一个类,是不是同一个类?(不是,Class 对象不同)。


模块三:垃圾回收 GC(面试重灾区,重中之重)

1. 垃圾判定算法

核心定义
(1)引用计数法

给对象加一个引用计数器,被引用 + 1,引用失效 - 1,计数器为 0 就是垃圾;缺点:无法解决循环引用的问题(A 引用 B,B 引用 A,没有其他引用,计数器永远不为 0,无法回收),Java 不采用。

(2)可达性分析算法(Java 采用)

GC Roots 为起点,向下搜索,搜索不到的对象就是垃圾,可以被回收;GC Roots 根对象(必背)

  1. 虚拟机栈中局部变量表引用的对象;
  2. 方法区中静态变量、常量引用的对象;
  3. 本地方法栈 JNI 引用的对象;
  4. 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 项目、对响应时间要求高的服务;
  • 核心步骤
    1. 初始标记:STW,只标记 GC Roots 直接关联的对象,速度极快;
    2. 并发标记:和用户线程并发执行,遍历所有存活对象,无 STW;
    3. 重新标记:STW,修正并发标记期间变化的对象,速度快;
    4. 并发清除:和用户线程并发执行,清除垃圾对象,无 STW;
  • 缺点:内存碎片、CPU 占用高、并发清除期间产生的浮动垃圾无法处理。
(2)G1 收集器(Garbage First)
  • 定位:兼顾年轻代和老年代的区域化分代收集器,JDK9 之后默认收集器,适合大内存(4G 以上)服务,可控制最大停顿时间;
  • 核心设计:把堆分成多个大小相等的 Region,每个 Region 可以是 Eden、S、老年代,优先回收垃圾最多的 Region;
  • 核心步骤:初始标记→并发标记→最终标记→筛选回收(STW,按停顿时间优先回收垃圾最多的 Region);
  • 优势:可控的停顿时间、无内存碎片、兼顾吞吐量和延迟,是现在的主流收集器。

了解即可:ZGC、Shenandoah:超低延迟收集器,TB 级内存,STW 时间不超过 1ms,适合超大内存服务。


个人理解

GC 的核心是「找到垃圾,回收垃圾」,分代回收是 JVM 经过验证的最优方案,针对不同生命周期的对象用不同算法,平衡性能和内存;垃圾回收器的选择核心是「延迟优先还是吞吐量优先」,互联网项目优先低延迟的 CMS/G1。


项目实际使用场景

  1. GC 参数配置:校园二手平台服务用 G1 收集器,配置最大停顿时间 200ms,堆内存 1G,满足接口低延迟要求;
  2. OOM 排查 :项目出现堆 OOM 时,配置-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动 dump 堆文件,用 MAT 工具分析,发现是商品本地缓存没有设置过期时间,导致内存泄漏,解决后 GC 恢复正常;
  3. 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 定义的一套可见性规则,不需要加锁,只要满足规则,就保证可见性,是判断多线程场景下是否有可见性问题的核心依据,核心规则(必背):

  1. 程序次序规则:同一个线程内,前面的操作 Happens-Before 后面的操作;
  2. 锁定规则:同一个锁的 unlock 操作 Happens-Before 后续的 lock 操作;
  3. volatile 变量规则:对 volatile 变量的写操作 Happens-Before 后续对这个变量的读操作;
  4. 传递规则:A Happen-Before B,B Happen-Before C,那么 A Happen-Before C;
  5. 线程启动规则:线程的 start () 方法 Happens-Before 线程的所有操作;
  6. 线程中断规则:对线程 interrupt () 的调用 Happens-Before 线程检测到中断事件。

个人理解

JMM 是多线程安全的底层保障,解决的是多线程场景下「变量可见性、指令重排序」的问题;Happens-Before 原则是 JMM 给开发者的承诺,不需要理解底层实现,只要遵守规则就能保证线程安全。


项目实际使用场景

  1. volatile 的应用:项目中的服务开关、灰度配置用 volatile 修饰,保证修改后所有线程立即看到最新值,底层就是 volatile 的 Happens-Before 规则;
  2. DCL 单例:项目中的单例线程池用 volatile 修饰,禁止指令重排,避免半初始化对象,就是利用 volatile 的有序性和可见性;
  3. 锁的可见性:synchronized 加锁解锁,保证锁内的变量修改对其他线程可见,底层是锁定规则。

面试考点标注

✅ 必问:JMM 的三大特性是什么?分别怎么实现?

✅ 必问:volatile 怎么保证可见性和有序性?

✅ 必问:Happens-Before 核心规则有哪些?

✅ 原理:多线程场景下为什么会有可见性、指令重排序问题?


当日验收清单

  1. 不看资料口述:JVM 运行时数据区完整结构、双亲委派模型完整流程、分代回收的核心逻辑;
  2. 结合项目口述:你的项目 JVM 参数怎么配置的,有没有遇到过 OOM,怎么排查的;
  3. 避坑确认:元空间 OOM 的场景、内存泄漏和内存溢出的区别、Full GC 频繁的常见原因。

JVM 与 Java 内存模型 面试模拟题(实习面试专属,附标准答案)


一、基础必考题(8 道,中小厂实习 100% 覆盖)

1. JVM 运行时数据区是怎么划分的?哪些是线程私有 / 共享?每个区域的核心作用是什么?

【标准答案】JVM 运行时内存分为 5 大核心区域,分为线程私有和线程共享两大类:

(1)线程私有(随线程创建 / 销毁)
  1. 程序计数器 :记录当前线程执行的字节码指令行号,线程切换后恢复执行位置,是唯一不会出现 OOM 的区域
  2. 虚拟机栈:每个方法执行时创建栈帧,存储局部变量表、操作数栈、方法出口等,是 Java 方法执行的内存模型;
  3. 本地方法栈:为 Native 本地方法服务,作用和虚拟机栈完全一致。
(2)线程共享(随 JVM 启动 / 销毁)
  1. 堆(Heap):JVM 最大的内存区域,所有对象实例、数组都存在这里,是 GC 垃圾回收的核心区域;
  2. 方法区(元空间):存储类的元信息、静态变量、常量、即时编译后的代码;JDK8 之前叫永久代(堆内),JDK8 之后移到本地内存,改名为元空间。

补充:StringTable(字符串常量池)、运行时常量池 JDK7 之后移到堆内存。

【考点对应】模块一:JVM 运行时数据区


2. 双亲委派模型的原理是什么?有什么核心好处?哪些场景破坏了双亲委派?

【标准答案】

(1)核心原理

类加载时,先向上委托父类加载器加载,父加载器找不到,再自己加载,全程向上委托、向下加载。类加载器层级从高到低:

  1. 启动类加载器:加载 JDK 核心类;
  2. 扩展类加载器:加载 JDK 扩展包类;
  3. 应用类加载器:加载用户 classpath 下的类;
  4. 自定义类加载器:加载自定义路径的类。
(2)核心好处
  1. 核心类安全 :避免 JDK 核心类被篡改,比如用户自定义的java.lang.String不会被加载,保证核心类的唯一性;
  2. 类的唯一性:同一个类只会被加载一次,避免重复加载。
(3)破坏双亲委派的场景
  1. SPI 机制:JDBC、JNDI 等,核心接口由父加载器加载,实现类由子加载器加载;
  2. Tomcat 类加载器:Web 应用隔离,每个应用优先加载自己的类,不委托父加载器;
  3. 热部署 / 热加载:Spring Boot DevTools、OSGi,自定义类加载器重新加载修改的类。

【考点对应】模块二:双亲委派模型


3. 可达性分析算法的 GC Roots 根对象有哪些?引用计数法有什么缺点?

【标准答案】

(1)GC Roots 根对象(必背)
  1. 虚拟机栈中局部变量表引用的对象;
  2. 方法区中静态变量、常量引用的对象;
  3. 本地方法栈 JNI 引用的对象;
  4. 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 是为了屏蔽不同硬件的内存差异,定义的多线程交互规则,三大核心特性:

  1. 原子性:操作要么全部执行要么不执行,实现方式:synchronized、Lock、原子类 Atomic;
  2. 可见性:一个线程修改共享变量,其他线程能立即看到最新值,实现方式:volatile、synchronized、final;
  3. 有序性:禁止指令重排序,实现方式:volatile、synchronized。

【考点对应】模块四:JMM 三大特性


8. 类加载的 5 个核心阶段是什么?准备阶段和初始化阶段的核心区别是什么?

【标准答案】

5 个核心阶段
  1. 加载:读取字节码,生成 Class 对象;
  2. 验证:校验字节码合法性,保证 JVM 安全;
  3. 准备 :给静态变量分配内存,赋默认初始值(int=0、引用 = null),常量直接赋值;
  4. 解析:符号引用转为直接内存地址;
  5. 初始化 :执行静态代码块,给静态变量赋业务定义的实际值
核心区别
  • 准备阶段:JVM 自动赋默认值,不执行用户代码;
  • 初始化阶段:执行用户定义的静态代码和赋值逻辑。

【考点对应】模块二:类加载 5 个阶段


二、场景坑点题(6 道,大厂实习高频业务题 / 排查题)

1. 你的校园二手平台商品批量导入功能,一次性读取 10 万条数据到内存,出现堆 OOM,怎么排查?怎么解决?

【标准答案】

排查步骤
  1. 启动时加 JVM 参数-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动生成堆 dump 文件;
  2. 用 MAT、JProfiler 工具分析 dump 文件,找到占用内存最大的对象,确认是商品对象集合;
  3. 查看 GC 日志,确认 Full GC 频繁,老年代占满无法回收。
解决方案
  1. 分批读取:每次读取 1000 条,处理完释放引用,再读取下一批,避免全量加载到内存;
  2. 避免强引用缓存:处理完的商品对象及时置空,方便 GC 回收;
  3. 优化 JVM 参数:适当调大堆内存,调整年轻代比例,减少对象直接晋升老年代;
  4. 大文件优化:用 NIO 流式读取,不一次性加载整个文件到内存。

【考点对应】模块一:OOM 排查与优化


2. 你的校园二手平台商品分类树用递归查询,分类层级过深时出现 StackOverflowError,是什么原因?怎么解决?

【标准答案】

原因

递归调用时,每个方法都会在虚拟机栈中创建一个栈帧,递归层级过深,栈深度超过 JVM 默认的栈深度(默认 1M 左右),触发栈溢出错误。

解决方案
  1. 最优方案:改成迭代实现:用栈数据结构模拟递归,避免方法调用的栈帧创建,无层级限制;
  2. 临时方案:调大栈内存 :JVM 参数-Xss2m调大栈内存,只能缓解,不能根治,不推荐;
  3. 业务优化:限制分类最大层级,避免过深的递归。

【考点对应】模块一:虚拟机栈溢出问题


3. 你的项目上线后 Full GC 频繁,接口卡顿,可能有哪些原因?怎么排查优化?

【标准答案】

常见原因
  1. 内存泄漏:本地缓存、静态集合没有过期清理,对象无法回收,老年代占满;
  2. 大对象过多:大文件、大集合直接进入老年代,老年代快速占满;
  3. 元空间不足:动态生成类过多(AOP、动态代理),元空间满触发 Full GC;
  4. JVM 参数不合理:堆内存太小、年轻代比例太小,对象频繁晋升老年代。
排查优化
  1. 查看 GC 日志,确认 Full GC 的触发原因;
  2. dump 堆文件,分析是否有内存泄漏,修复泄漏点(比如给缓存加过期时间);
  3. 调整大对象阈值,避免大对象直接进入老年代,分批处理大对象;
  4. 调大元空间最大内存-XX:MaxMetaspaceSize=256m
  5. 合理配置堆内存,年轻代和老年代比例 1:2,用 G1 收集器控制停顿时间。

【考点对应】模块三:GC 问题排查优化


4. 开发环境用 Spring Boot DevTools 热部署,出现ClassCastException: com.goods.Goods cannot be cast to com.goods.Goods,同一个类转换异常,是什么原因?怎么解决?

【标准答案】

原因

类的唯一性由「全类名 + 类加载器」共同决定,同一个类被两个不同的类加载器加载,会被 JVM 认为是完全不同的类,因此转换失败。Spring Boot DevTools 热部署时,会用自定义的 RestartClassLoader 重新加载修改的类,原来的类是应用类加载器加载的,因此出现同一个类转换异常。

解决方案
  1. 热部署环境避免跨类加载器传递对象;
  2. 生产环境关闭热部署,不用 DevTools;
  3. 重启项目,清空旧的类加载器缓存。

【考点对应】模块二:类加载器唯一性


5. 项目启动时配置 JVM 参数-Xms1024m -Xmx2048m,有什么问题?最佳实践是什么?

【标准答案】

问题

-Xms是初始堆内存,-Xmx是最大堆内存,两者不一致时:

  1. 堆内存不足时会触发扩容,扩容过程会发生 Full GC,影响服务性能;
  2. 内存碎片会更多,GC 效率降低。
最佳实践

生产环境将初始堆和最大堆设置为相同值,比如-Xms1024m -Xmx1024m,避免堆扩容的 GC 开销,提升稳定性;同时设置-XX:+AlwaysPreTouch,启动时预分配所有堆内存,提升运行时性能。

【考点对应】模块一:JVM 参数最佳实践


6. 你的项目用 volatile 修饰接口限流开关,后台修改开关值后,所有线程都能立即生效,底层原理是什么?

【标准答案】volatile 通过两个机制保证可见性:

  1. 内存屏障:volatile 写操作后加写屏障,强制把工作内存的修改立即刷回主内存;volatile 读操作前加读屏障,强制从主内存读取最新值;
  2. MESI 缓存一致性协议:CPU 通过该协议保证多个核心的缓存一致性,一个核心修改了 volatile 变量,其他核心的缓存会立即失效,必须从主内存读取最新值。同时符合 JMM 的 Happens-Before 原则:对 volatile 变量的写操作,先行发生于后续对这个变量的读操作。

【考点对应】模块四:volatile 可见性原理


三、进阶原理题(4 道,中大厂实习拔高题)

1. JVM 为什么要设计分代回收?为什么年轻代用标记复制算法,老年代用标记整理?

【标准答案】

分代回收的原因

基于统计规律:98% 的对象都是朝生夕死,存活时间极短,只有少量对象长期存活。不分代的话,所有对象放在一起,每次 GC 都要扫描全堆,性能极差;分代后针对不同生命周期的对象用不同算法,大幅提升 GC 效率。

算法选择的原因
  1. 年轻代用标记复制:年轻代对象存活率极低,每次 GC 只有少量对象存活,复制开销极小,无内存碎片,性能最高;虽然内存利用率 50%,但年轻代内存占比小,浪费可以接受;
  2. 老年代用标记整理:老年代对象存活率高,用标记复制的话复制开销极大;用标记清除会产生大量内存碎片,大对象无法分配;标记整理无内存碎片,内存利用率 100%,适合老年代。

【考点对应】模块三:分代回收设计原理


2. CMS 收集器为什么会有浮动垃圾?为什么用标记清除不用标记整理?

【标准答案】

浮动垃圾的原因

CMS 的并发清除阶段,用户线程和 GC 线程并发执行,清除过程中用户线程会产生新的垃圾对象,这些对象无法在本次 GC 中被标记,只能等到下一次 GC 回收,这部分就是浮动垃圾。

用标记清除的原因
  1. 并发清除阶段如果用标记整理,需要移动存活对象的内存地址,用户线程正在访问对象,会导致引用错乱,无法并发执行;
  2. 标记清除不需要移动对象,支持和用户线程并发执行,符合 CMS 低延迟的设计目标;
  3. 内存碎片问题可以通过参数设置,在 Full GC 时做一次标记整理来缓解。

【考点对应】模块三:CMS 收集器原理


3. G1 收集器的 Region 设计有什么核心优势?和传统分代的 CMS 核心区别是什么?

【标准答案】

Region 设计的核心优势
  1. 可控的停顿时间:G1 可以预测每个 Region 的回收时间,根据配置的最大停顿时间,优先回收垃圾最多的 Region,精准控制 STW 时间;
  2. 无内存碎片:每个 Region 回收用标记复制,整体是标记整理,全程无内存碎片;
  3. 大内存性能优:传统分代 GC 扫描全堆,大内存下 STW 时间不可控;G1 只扫描需要回收的 Region,大内存下性能远优于 CMS;
  4. 灵活的分代:每个 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 原则给了开发者一套简单的规则,不需要理解底层复杂的内存屏障,只要遵守规则就能保证线程安全。

核心规则(必背)
  1. 程序次序规则:同一个线程内,前面的操作先行发生于后面的操作;
  2. 锁定规则:同一个锁的 unlock 操作,先行发生于后续的 lock 操作;
  3. volatile 规则:对 volatile 变量的写操作,先行发生于后续对这个变量的读操作;
  4. 传递规则:A Happen-Before B,B Happen-Before C,则 A Happen-Before C;
  5. 线程启动规则:线程的 start () 方法先行发生于线程的所有操作;
  6. 线程中断规则:interrupt () 调用先行发生于线程检测到中断事件。

【考点对应】模块四:Happens-Before 原则


实习面试答题加分技巧(绑定你的校园二手平台项目)

  1. 所有 JVM 题都绑定项目踩坑经验
    • 问 OOM 排查:主动说「我做校园二手平台商品批量导入的时候,一次性加载 10 万条数据踩过 OOM 的坑,后来加了 dump 参数,用 MAT 分析是商品集合占满了堆,改成分批读取就解决了」;
    • 问 GC 优化:主动说「我项目用的 G1 收集器,配置最大停顿时间 200ms,堆内存 1G,之前大对象过多导致 Full GC 频繁,调整了大对象阈值,分批处理图片就好了」;
  2. 主动说参数配置实践:问 JVM 参数就说自己项目的启动参数配置,为什么这么设置,比单纯背参数加分很多;
  3. 答题逻辑固定为「结论→原理→我的项目实践 / 踩坑」,面试官会认为你有实际生产经验,不是纯背题。
相关推荐
苦逼的猿宝8 小时前
医院管理系统.(源码+论文)
java·毕业设计·springboot·计算机毕业设计
爱睡觉1118 小时前
从 6500ms 到 49ms:一次 Java 内存布局优化的实录
java
摇滚侠9 小时前
IDEA 新建 Java 项目 学习 Java SE
java·学习·intellij-idea
kinl20189 小时前
Softmax Linear Units (SoLU)
笔记
未秃头的程序猿9 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·后端·ai编程
程序员老乔9 小时前
03-Spring-Security-JWT认证
java·后端·spring
程序员buddha9 小时前
传统 Spring 框架,XML 配置 Bean 的方式
xml·java·spring
希望永不加班9 小时前
SpringBoot 消费者并发控制:线程池配置
java·spring boot·后端·spring
叶~小兮9 小时前
K8s常用组件学习笔记
笔记·学习·kubernetes