JVM面试基础篇
🧠 建议的学习顺序(针对小白)
你可以按以下顺序逐步掌握:
- 理解 JVM 内存结构(这几乎是最常问)
- 了解 GC 与对象生命周期
- 掌握基本参数与调试工具
- 深入学习垃圾回收器与调优技巧(进阶)
🔹一、JVM 是什么?
**JVM(Java Virtual Machine)**是 Java 程序运行的虚拟平台。
它的主要职责包括:加载字节码、执行代码、内存管理、垃圾回收(GC)。
🔹二、JVM 的内存结构(重点)
面试常问:“你了解 JVM 的内存结构吗?”
💡 JVM 运行时内存区域(JDK 8 之前):
区域名称 | 作用 | 生命周期 |
---|---|---|
程序计数器 | 当前线程执行的字节码行号 | 每个线程独立 |
虚拟机栈 | 方法调用,局部变量,操作数栈 | 每个线程独立 |
本地方法栈 | 本地方法执行(如 C) | 每个线程独立 |
堆(Heap) | 存放对象实例(几乎所有 new 的对象) | 所有线程共享 |
方法区(JDK8 改为元空间) | 存放类的元信息、常量池、静态变量等 | 所有线程共享 |
JDK 8 以后,方法区 被 元空间(Metaspace) 替代,存储在本地内存中。
🔹三、类加载机制(了解即可)
面试可能问:“Java 类是怎么被加载的?”
🌱 类加载生命周期:
加载 → 验证 → 准备 → 解析 → 初始化
👨🏫 类加载器(ClassLoader)
- 启动类加载器(BootstrapClassLoader):加载 JDK 核心类
- 扩展类加载器(ExtClassLoader)
- 应用类加载器(AppClassLoader)
- 自定义类加载器
💡 双亲委派机制:先让父加载器尝试加载,避免重复加载或安全问题。
🔹四、垃圾回收机制(GC) ⭐️⭐️⭐️(重头戏)
4.1 什么是 GC?
JVM 自动帮你清理不用的内存对象,防止内存泄漏。
4.2 内存分代模型(重点)
区域 | 含义 | 特点 |
---|---|---|
新生代(Young) | 新生对象 | 空间小,GC频繁 |
老年代(Old) | 长期存活对象 | 空间大,GC少 |
永久代 / 元空间 | 类的元信息 | JDK8 改为元空间,存在本地内存 |
新生代内部分为:
- Eden 区(伊甸园)
- S0 区(Survivor 0)
- S1 区(Survivor 1)
4.3 常见垃圾收集器(了解即可)
收集器 | 适用区域 | 特点 |
---|---|---|
Serial | 单线程,简单稳定 | 适合小内存 |
Parallel | 多线程,吞吐优先 | 性能较好 |
CMS | 并发低延迟,响应快 | 老年代收集,容易碎片 |
G1(JDK9默认) | 分区回收,低延迟 | 高级面试会问 |
ZGC / Shenandoah | 大内存+低延迟 | 面向大数据应用 |
🔹五、JVM 常用参数(了解)
-Xms512m # 初始堆内存
-Xmx1024m # 最大堆内存
-Xss256k # 每个线程栈大小
-XX:+PrintGCDetails # 打印 GC 日志
🔹六、JVM 调优与排查(了解即可)
初学者面试可能只会问你是否了解工具:
工具 | 作用 |
---|---|
jconsole | 图形化监控 JVM 运行 |
jstack | 查看线程堆栈,排查死锁 |
jmap | 查看内存快照(heap dump) |
jstat | 查看 GC 状态 |
VisualVM | 图形工具,支持内存分析 |
🔹高频面试题整理
- JVM 内存结构都有哪些?各自存什么?
- 对象是如何在内存中分配的?什么情况下进入老年代?
- 你知道哪些垃圾回收器?G1 和 CMS 区别?
- GC Roots 有哪些?什么是可达性分析?
- 如何查看线程死锁?如何排查内存泄漏?
- 类加载器的双亲委派机制是怎么回事?
🎯 面试高频题:JVM 内存结构都有哪些?各自存什么?
✅ 一、JVM 内存结构(运行时内存区域)
在 Java 程序运行过程中,JVM 会将内存划分为以下几个主要区域(以 JDK 8 为例):
区域名称 | 是否线程共享 | 存储内容 | 说明 |
---|---|---|---|
程序计数器 | 否(每线程独立) | 当前线程执行的字节码行号 | 类比于 CPU 的指令指针 |
虚拟机栈(Java Stack) | 否(每线程独立) | 方法的局部变量、操作数栈、返回地址等 | 每个方法执行时都会创建一个栈帧 |
本地方法栈(Native Method Stack) | 否 | 本地方法(C/C++)使用的栈 | 与虚拟机栈类似,服务于 native 方法 |
堆(Heap) | ✅ 是 | 所有对象实例、数组 | 垃圾回收的主要区域,分为新生代、老年代 |
方法区(Method Area)/元空间(Metaspace) | ✅ 是 | 类的元信息(结构)、常量池、静态变量等 | JDK 8 起从堆中移出,变为 Metaspace 存在于本地内存 |
运行时常量池(Runtime Constant Pool) | ✅ 是 | 字面量、符号引用等常量 | 方法区的一部分,用于类加载时填充常量表 |
🧠 二、各区域详解(简明好记)
1. 📌 程序计数器(每线程独立)
- 用来记录每个线程执行到哪一行字节码。
- 线程切换后能恢复到正确位置。
- 它是唯一一个不会出现 OutOfMemoryError 的区域。
2. 📌 虚拟机栈
- 每个线程创建时分配一块栈空间,用来执行方法。
- 每次调用方法就会压入一个“栈帧”,包含局部变量、参数、操作数栈等。
- 可能出现异常:
StackOverflowError
(方法递归太深)OutOfMemoryError
(栈太大或线程太多)
3. 📌 本地方法栈
- 用于执行
native
方法(C/C++ 实现)。 - 与虚拟机栈类似,但专门为本地方法服务。
4. 📌 堆(Heap)(面试重点)
- 所有 Java 对象(new 出来的)都会分配在堆中。
- 是垃圾收集器的核心管理区域。
- 分区模型(默认):
- 新生代(Young):Eden + S0 + S1,GC频繁
- 老年代(Old):存活时间长的对象
- (JDK 8 之前:永久代;JDK 8 起:元空间)
5. 📌 方法区 / 元空间(JDK8 起)
- 存储类的结构、常量池、静态变量、类加载信息等。
- 以前叫“永久代”(PermGen),现在是“元空间”(Metaspace),存储在本地内存。
6. 📌 运行时常量池
- 存储类中的符号引用、字符串字面量等常量。
- 是方法区的一部分。
- 常见的如
"abc"
字符串常量、final
基本类型值等。
✅ 三、图示记忆(结构层次)
┌────────────────────────┐│ JVM 进程内存 │└────────────────────────┘↓┌────────────────────────────────────────────────────┐│ 线程私有区域 │├─────────────┬────────────┬─────────────────────────┤│程序计数器 │Java 虚拟机栈 │本地方法栈 │└─────────────┴────────────┴─────────────────────────┘↓┌────────────────────────────────────────────────────┐│ 线程共享区域 │├────────────────────────┬───────────────────────────┤│ Java 堆 │ 方法区 / 元空间 │└────────────────────────┴───────────────────────────┘
🧪 面试回答模板(记住这个版本可以直接说出口):
JVM 在运行时将内存分为线程私有和线程共享区域。线程私有的包括程序计数器、虚拟机栈和本地方法栈,主要用于方法执行和本地代码支持;线程共享的区域包括堆和方法区。堆是对象实例的主要存储区域,方法区(或 JDK8 起的元空间)则用于存储类结构信息、静态变量和常量池数据。
🎯 面试高频题:对象在内存中的分配过程
当我们用 new
创建一个对象时,对象通常被分配在 JVM 堆内存,具体是在 新生代的 Eden 区域。
✅ 默认流程如下:
- 大部分新创建的对象 → 新生代的 Eden 区
Eden 容量较小,创建对象快,GC(垃圾回收)也频繁。 - 当 Eden 区满了,会触发 Minor GC(小型垃圾回收)
存活下来的对象会进入 Survivor 区(S0 或 S1)
多次 Minor GC 后,仍然存活的对象可能晋升到 老年代。
🔄 二、对象晋升到老年代的情况(重点)
以下是对象进入老年代的常见 三种情况:
✳️ 1. 对象存活时间足够长
- JVM 为每个对象设置一个 年龄计数器(Age)。
- 每经过一次 Minor GC,Age +1。
- 达到阈值(如
15
,可通过-XX:MaxTenuringThreshold
设置)→ 晋升老年代。
✅ 示例:某对象在 Eden 中经历了多次 GC 仍然存活,Age 达到阈值 10 → 进入老年代。
✳️ 2. 大对象直接进入老年代
-
大对象(如大量连续内存的数组)可能直接在老年代分配,避免在新生代复制耗时。
-
JVM 参数控制阈值:
-XX:PretenureSizeThreshold=大小(如1M)
-
常见于图片处理、大数据系统。
✳️ 3. Survivor 区放不下
- 当 Survivor 区无法容纳所有存活对象时,部分对象会被直接晋升到老年代。
📦 补充:TLAB(线程本地分配缓冲区)
在高并发环境中,为了避免锁竞争,对象优先在线程的 TLAB(Thread Local Allocation Buffer)中分配。TLAB 是堆中为每个线程预留的一小块区域。
🧠 总结记忆口诀:
对象先到 Eden,小GC就清理;
死不了的进 Survivor,多次GC再升级;
对象大就老年代,Survivor装不下也去那儿待。
✅ 建议答题模板(面试时这样回答)
Java 中对象一般是在堆内存中分配,优先分配在新生代的 Eden 区。当 Eden 区满时会触发 Minor GC,存活对象移动到 Survivor 区。经历多次 GC 仍然存活的对象会根据年龄阈值晋升到老年代。此外,如果是大对象或 Survivor 区空间不足,也可能直接进入老年代。JVM 通过这种机制来优化垃圾回收效率,减少复制成本。
🎯 面试高频题:你知道哪些垃圾回收器?
JVM 中的垃圾回收器分为 新生代收集器 和 老年代收集器,以下是主流的 GC 组合:
🔹 新生代垃圾回收器(Minor GC)
名称 | 特点 |
---|---|
Serial | 单线程,简单稳定,停顿时间长 |
ParNew | Serial 的多线程版本 |
Parallel Scavenge | 高吞吐量,适合批处理系统 |
🔹 老年代垃圾回收器(Major/Full GC)
名称 | 特点 |
---|---|
Serial Old | Serial 的老年代版本 |
Parallel Old | 高吞吐,Parallel 的搭档 |
CMS | 并发低延迟,响应快 |
G1 | 区域化回收,适合大堆,低延迟 |
🔹 新一代垃圾回收器(JDK 11+)
名称 | 特点 |
---|---|
ZGC | 超低延迟(<10ms),大堆友好 |
Shenandoah | 红帽出品,停顿与堆大小无关 |
✅ 二、CMS 和 G1 的区别(重点)
维度 | CMS(Concurrent Mark Sweep) | G1(Garbage First) |
---|---|---|
分代模型 | 有:新生代 + 老年代 | 逻辑分代,但统一管理多个 Region |
GC 方式 | 老年代并发标记 + 并发清除 | 并发标记 + 区域化回收(Region) |
停顿时间 | 低(并发回收)但可能出现碎片 | 更低,支持预测停顿时间(可配置) |
空间整理 | 不做整理,可能有碎片 | 自动压缩整理,避免碎片 |
GC 并发阶段 | 可能与应用线程同时运行 | 支持并发标记、复制,吞吐更稳定 |
失败场景 | 会触发 Full GC(Serial Old) | 不容易 Full GC,容错能力强 |
适用场景 | 响应时间敏感,如 Web 应用 | 大堆 + 低延迟场景,如大数据系统 |
是否过时 | 是(JDK 14 起被标记为废弃) | 是 JDK 默认垃圾收集器(JDK 9+) |
✅ 三、答题示范(面试模板)
Java 中常见的垃圾回收器包括 Serial、ParNew、CMS、G1、Parallel、ZGC 等。其中 CMS 和 G1 是低延迟收集器。CMS 采用“标记-清除”算法,虽然回收时停顿短,但存在内存碎片问题;G1 将堆划分为多个 Region,采用“标记-复制-整理”方式,能更好地控制停顿时间,同时避免内存碎片。相比 CMS,G1 更适合大内存、对响应时间有要求的应用场景。
🧠 小技巧:记忆口诀
CMS 扫地式回收,快但碎;G1 智能分区,稳又全。
🎯 面试高频题:什么是 GC Roots?
GC Roots
是一组固定的、不会被垃圾回收器回收的引用对象,它们作为起点,通过引用链向下查找,从而决定哪些对象是“可达的(活着的)”。
简单理解:GC Roots 就是“活人名单”,从他们出发能找到的对象都不会被回收。
✅ 二、常见的 GC Roots 有哪些?
GC Root 来源 | 举例或说明 |
---|---|
✅ 虚拟机栈(栈帧中的局部变量表) | 方法中的局部变量、参数等(线程私有) |
✅ 方法区中类的静态字段引用的对象 | 如:static User user = new User(); |
✅ 方法区中常量引用的对象 | 如:final String s = "abc" ,字符串常量池中的对象 |
✅ 本地方法栈中 JNI 引用的对象 | native 方法中引用的对象 |
✅ 活跃线程对象 | 每个运行中的线程本身不会被 GC 回收 |
✅ JVM 内部的一些引用 | 如系统类加载器、线程上下文类加载器等 |
补充:在某些 GC 场景中(如 G1),还可能包括:
- 断点调试时的临时对象
- JNI 的全局引用
✅ 三、什么是 可达性分析算法(Reachability Analysis)
🧠 定义:
可达性分析就是从 GC Roots 出发,沿着对象的引用链(reference)向下搜索,凡是能从 GC Roots 找到的对象就是“可达”的,反之就是“不可达”对象,可被 GC 回收。
GC Roots│▼
对象A → 对象B → 对象C↑被引用
如果一个对象没有被 GC Roots 直接或间接引用,那它就是“不可达对象”,GC 会标记为“垃圾”准备回收。
✅ 与“引用计数法”的对比:
算法 | 特点 | 缺点 |
---|---|---|
引用计数法 | 计数 +1/-1 | 无法处理循环引用 |
可达性分析 | 从 GC Roots 开始遍历引用图 | 成本高一些,但准确可靠(现代 JVM 使用) |
✅ 四、答题模板(可直接在面试中说):
JVM 在判断一个对象是否可以回收时,并不是通过引用计数法,而是通过“可达性分析算法”实现的。它以一组称为 GC Roots 的对象作为起点,遍历对象之间的引用链。凡是能从 GC Roots 追踪到的对象就是“可达”的,存活下来;否则就是“不可达”的对象,会被回收。常见的 GC Roots 包括虚拟机栈中的引用、本地方法栈中的 JNI 引用、方法区中静态字段和常量、以及活跃线程本身。
📌 图解助记(简化版)
GC Roots│┌─────┴─────┐↓ ↓对象A 对象B↓对象C(被引用)
对象A/B/C 是可达对象;无法从 GC Roots 追踪到的对象就是垃圾对象。
🎯 面试高频题:如何查看线程死锁?
🔹 什么是线程死锁?
死锁是指多个线程互相等待对方释放资源,导致程序无法继续执行。比如:
Thread 1 获取锁 A,等待锁 B;
Thread 2 获取锁 B,等待锁 A;
→ 相互等待,进入死锁状态。
🔹 如何查看死锁(面试 + 实战通用):
✅ 方法一:使用 jps + jstack
- 使用
jps
查看 Java 进程 ID:
jps
- 使用
jstack
查看线程堆栈信息:
jstack <pid> > threadDump.txt
- 打开
threadDump.txt
,搜索关键词:
Found one Java-level deadlock:
若存在死锁,jstack 会标记并打印出互相等待的线程、锁信息等,非常直观。
✅ 方法二:IDEA 内置工具(图形化)
- 打开 Run → Debug,点击 Threads 视图。
- 看到红色标记线程就是陷入死锁的线程。
✅ 方法三:使用 VisualVM
或 JConsole
- 在“线程”面板可以查看线程状态。
- 死锁线程通常会显示为“blocked”或“waiting”状态,VisualVM 能直接点出 Deadlock 的线程对。
✅ 二、如何排查内存泄漏?
🔹 什么是内存泄漏?
在 Java 中,对象不再被使用,但仍然被引用(没有被 GC 回收),造成堆内存不断增加,最终导致 OutOfMemoryError。
🔹 内存泄漏常见场景:
场景 | 示例 |
---|---|
静态集合类 | static List list = new ArrayList(); |
监听器/回调未移除 | 注册了监听器但未注销 |
ThreadLocal | 使用不当未清理 ThreadLocal.remove() |
缓存不当 | 自定义 Map 缓存没过期策略 |
数据库连接、文件流未关闭 | 长期占用堆资源 |
🔹 如何排查内存泄漏?
✅ 方法一:使用 VisualVM
- 打开 VisualVM,连接目标进程。
- 查看
Monitor
面板的 Heap Usage 是否持续增长不回落。 - 在
Heap Dump
面板中生成快照,对比“对象实例”是否持续增加。 - 使用“类实例树”查看谁在引用大对象。
- 使用“引用链”(Reference Tree)定位是哪个类保留了引用。
✅ 方法二:使用 MAT(Memory Analyzer Tool)
深度分析
- 导出堆快照(
.hprof
文件):
jmap -dump:format=b,file=heap.hprof <pid>
- 使用 Eclipse MAT 打开:
- 分析
Dominator Tree
,查找内存占用最多的对象。 - 使用
Leak Suspects Report
自动生成泄漏分析报告。
✅ 方法三:使用 Arthas
阿里开源的 Java 诊断神器,适合生产排查。
# 连接目标进程
./as.sh
# 查找类实例数量
sc -d com.example.YourClass
✅ 面试答题模板(精简版):
查看线程死锁可以使用
jstack
工具,它会直接输出 Java 层的死锁信息;也可以借助VisualVM
或IDEA
等图形工具查看线程状态。排查内存泄漏则需要结合堆分析工具,如 VisualVM 或 MAT,通过 heap dump 快照分析哪些对象无法被 GC 回收,以及是哪些类/字段保留了引用,从而找出泄漏根源。
🎁 Bonus:一图速记
🔍 死锁排查
└── jstack → Found one Java-level deadlock
└── VisualVM → Threads 显示 BLOCKED
└── IDEA Threads → 红色标记🧠 内存泄漏排查
└── VisualVM → Heap Dump + Reference Tree
└── MAT → Dominator Tree + Leak Suspects
└── jmap + jhat → 分析堆内存快照
└── Arthas → 查询实例、引用链
🎯 面试高频题:类加载器的双亲委派机制是怎么回事?
✅ 一、什么是类加载器(ClassLoader)?
Java 中的类加载器负责将 .class
字节码文件加载进内存并生成 Class 对象。JVM 启动时并不会一次性加载所有类,而是按需加载。
✅ 二、Java 中的类加载器分类
类加载器 | 作用 | 加载路径 |
---|---|---|
Bootstrap ClassLoader | 启动类加载器 | 加载核心类(rt.jar、java.lang.*) |
Extension ClassLoader | 扩展类加载器 | 加载 $JAVA_HOME/lib/ext 目录下的类 |
App ClassLoader | 应用类加载器 | 加载 classpath 下的类(如我们写的代码) |
自定义 ClassLoader | 用户自定义 | 覆盖或扩展默认加载机制 |
✅ 三、什么是双亲委派机制?
🌟 定义:
双亲委派机制是一种类加载层级结构,每个类加载器在加载某个类时,首先不会自己加载,而是把请求交给“父类加载器”去加载,只有父加载器无法加载时,才由当前加载器尝试加载。
🔄 加载流程:
AppClassLoader.loadClass("java.lang.String")│├──> 父:ExtClassLoader│└──> 父:BootstrapClassLoader│└──> 找到并加载 String.class
如果 Bootstrap 能加载,则其他加载器都不会重复加载。防止类的重复加载与安全问题。
✅ 优点:
优势 | 说明 |
---|---|
避免重复加载 | 确保同一个类只由一个类加载器加载(如 String 类) |
防止篡改核心类 | 用户不能自定义一个假的 java.lang.String 被误加载 |
层级清晰、便于管理 | 从上到下,清晰有序 |
✅ 四、能否打破双亲委派机制?
可以,但需要自定义 ClassLoader 并覆盖 loadClass()
方法的委派逻辑。很多框架(如 Tomcat、OSGi、SPI 插件机制)就会打破双亲委派,实现类隔离或热部署等需求。
✅ 五、面试答题模板(推荐背诵):
Java 类加载器在加载类时采用双亲委派机制。即一个类加载器接到类加载请求时,会先把请求交给它的父类加载器去处理,只有父加载器无法完成加载时,才由当前加载器去尝试加载。这种机制可以避免重复加载,防止核心类被篡改,保障 JVM 的安全性。常见的类加载器有 Bootstrap、Ext、App 以及用户自定义加载器。
✅ 六、配套口诀助记
类加载有三层:启动、扩展、应用层;
加载类先问爹,不行才自己干;
安全防重复,双亲保平安。
CMS 扫地式回收,快但碎;G1 智能分区,稳又全。