第一部分:JVM 宏观认知与面试必答
P1 1、JVM 的学习方式
1.1 学习核心原则
- 先体系后细节:先掌握 JVM 整体架构,再深入每个模块的原理,避免碎片化学习
- 理论 + 实践结合:理解原理的同时,用工具验证、写代码复现问题(如 OOM、GC 日志)
- 面向面试与生产:重点掌握高频面试点(类加载、GC、内存调优),同时结合生产环境调优场景
- 循序渐进:从基础架构 → 内存区域 → 类加载 → 垃圾回收 → 调优工具,逐步深入
1.2 学习路径规划
- 基础阶段:掌握 JVM 体系结构、内存区域划分、类加载机制
- 核心阶段:深入垃圾回收算法、垃圾收集器、GC 日志分析
- 实战阶段:学习 JProfiler 等调优工具,解决 OOM、内存泄漏、性能优化问题
- 进阶阶段:理解 JVM 底层实现、即时编译、虚拟机优化等高级特性
1.3 学习资源推荐
- 书籍:《深入理解 Java 虚拟机》(周志明,JVM 经典圣经)、《Java 虚拟机规范》
- 工具:JProfiler、VisualVM、jstat、jmap、jhat 等 JDK 自带工具
- 实践:写代码复现 OOM、分析 GC 日志、模拟内存泄漏场景
1. 请你谈谈你对 JVM 的理解?Java8 虚拟机和之前的变化更新?
1.1 JVM 核心定义(一句话回答)
JVM(Java Virtual Machine)是运行在操作系统之上的软件模拟计算机 ,负责加载、执行 Java 字节码,实现跨平台 (一次编写,到处运行)。它屏蔽了底层系统差异,提供自动内存管理(GC)和安全沙箱机制。
1.2 JVM 的核心作用
| 核心作用 | 说明 |
|---|---|
| 字节码执行引擎 | 解释执行字节码,或通过 JIT 编译为机器码执行 |
| 内存管理系统 | 分配、使用、回收内存(堆、栈、方法区) |
| 垃圾回收器 (GC) | 自动回收无引用对象,释放内存 |
| 安全沙箱 | 限制程序对系统资源的访问,保障安全 |
1.3 Java 8 虚拟机核心变化(面试必点)
Java 8 是 JVM 历史的分水岭,核心变化如下:
- 元空间(Metaspace)替代永久代(PermGen)
- JDK 7 及之前 :方法区 / 运行时常量池位于永久代 (堆内存),受
-XX:PermSize限制,易 OOM。 - JDK 8 :永久代被移除,方法区迁移到元空间(直接使用本地内存),大小动态扩展,解决永久代 OOM 问题。
- JDK 7 及之前 :方法区 / 运行时常量池位于永久代 (堆内存),受
- 内置 Nashorn JavaScript 引擎(已被替代)
- 默认垃圾收集器变更
- JDK 7:默认 Serial GC。
- JDK 8 :默认 Parallel GC(高吞吐量),同时引入 G1 GC 作为主流选择。
- 引入 Lambda 表达式(依赖 JVM invokedynamic 指令)
- 时区数据独立:JRE 不再包含时区数据,需独立依赖。
1.4 从 Java 8 到现代版本(Java 17)的进阶
| 版本 | 核心 JVM 变化 |
|---|---|
| Java 9 | 模块化系统(Project Jigsaw),JRE 被废弃 |
| Java 11 | 引入 ZGC(低延迟垃圾收集器),正式版 |
| Java 17 | 引入 Shenandoah GC,正式版;密封类;彻底移除 Applet |
2. 什么是 OOM,什么是栈溢出 StackOverflowError?怎么分析?
2.1 OOM(OutOfMemoryError)定义
OOM 是内存溢出错误 ,指 JVM 在申请内存时,没有足够的空闲内存可用,且垃圾回收无法释放更多内存,导致程序崩溃。
- 核心特征:JVM 堆内存耗尽、方法区耗尽、直接内存耗尽等。
2.2 StackOverflowError(栈溢出)定义
栈溢出是线程请求的栈深度超过了虚拟机允许的最大深度 ,通常由无限递归 或方法调用链过长导致。
- 核心区别 :OOM 是堆 / 方法区 内存不够;StackOverflowError 是线程栈深度不够。
2.3 分析方法(实战步骤)
2.3.1 OOM 分析流程
-
获取 Dump 文件 :配置 JVM 参数开启快照。
# 自动生成OOM时的dump文件 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./oom.hprof -
使用分析工具 :导入
hprof文件。- 工具:JProfiler、VisualVM、MAT(Eclipse Memory Analyzer)。
-
定位问题 :
- 查看大对象:哪些实例占用内存最多。
- 查看引用链:找出哪些引用未释放(如静态 Map、未关闭的连接)。
2.3.2 StackOverflowError 分析流程
- 查看堆栈信息:查看异常栈追踪,找到递归入口或无限循环方法。
- 优化代码 :
- 修复无限递归。
- 优化递归为迭代。
- 调整栈大小(
-Xss,不推荐)。
3. JVM 的常用调优参数有哪些?
3.1 堆内存核心参数(最常用)
| 参数 | 作用 | 说明 |
|---|---|---|
-Xms |
初始堆大小 | 等价于-XX:InitialHeapSize,建议与-Xmx一致 |
-Xmx |
最大堆大小 | 等价于-XX:MaxHeapSize,物理内存的 1/4~1/2 |
-Xmn |
新生代大小 | 如-Xmn2g,默认占堆的 1/3 |
-Xss |
栈大小 | 每个线程的栈大小,默认 1M,减小可提升线程数 |
3.2 垃圾收集器参数
| 参数 | 作用 |
|---|---|
-XX:+UseG1GC |
使用 G1 收集器(主流推荐) |
-XX:+UseParallelGC |
使用 Parallel GC(默认) |
-XX:+UseConcMarkSweepGC |
使用 CMS 收集器(老年代) |
3.3 元空间参数(Java 8+)
| 参数 | 作用 |
|---|---|
-XX:MetaspaceSize |
元空间初始大小 |
-XX:MaxMetaspaceSize |
元空间最大大小 |
3.4 内存快照参数
| 参数 | 作用 |
|---|---|
-XX:+HeapDumpOnOutOfMemoryError |
OOM 时自动生成 Dump 文件 |
-XX:HeapDumpPath |
指定 Dump 文件生成路径 |
4. 内存快照如何抓取,怎么分析 Dump 文件?
4.1 内存快照(Dump)抓取方式
4.1.1 自动抓取(生产环境推荐)
配置 JVM 启动参数,OOM 发生时自动抓取:
java -jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./app.hprof app.jar
4.1.2 手动抓取(在线 / 离线)
使用 JDK 自带工具:
# 生成整个堆快照
jmap -dump:format=b,file=heap.hprof <pid>
# 生成存活对象快照(推荐,体积小)
jmap -dump:live,format=b,file=live.hprof <pid>
4.2 Dump 文件分析步骤(核心流程)
- 工具选择:MAT(功能最强)、JProfiler、VisualVM。
- 核心分析动作 :
- Dominator Tree(支配树):找出占用内存最大的对象。
- Histogram(直方图):统计各类实例数量。
- GC Roots:查找对象的引用链,确定为何未被回收。
- 常见结论 :
- 大集合未清理:缓存 Map 无限增长。
- 数据库连接未关闭:导致连接池溢出。
- ThreadLocal 未清理:线程复用导致内存泄漏。
5. 谈谈 JVM 中,类加载器你的认识?
5.1 类加载器定义
类加载器(ClassLoader)是 JVM 的类加载子系统 核心,负责将.class文件的二进制字节流加载到 JVM 中,生成java.lang.Class对象。
5.2 分类与层级(双亲委派)
JVM 采用双亲委派机制,层级从下到上如下:
- 启动类加载器(Bootstrap ClassLoader) :C++ 实现,加载
rt.jar核心类。 - 扩展类加载器(Extension ClassLoader) :加载
jre/lib/ext扩展类。 - 应用程序类加载器(Application ClassLoader):加载用户 classpath 下的类。
- 自定义类加载器 :继承
ClassLoader,实现自定义加载逻辑(如热部署、加密类加载)。
5.3 双亲委派机制(核心面试点)
- 核心规则 :当一个类加载器收到加载请求时,优先委托父类加载器加载,父类无法加载时,才由自己加载。
- 优势 :
- 避免类重复加载 :保证核心类(如
Object)只加载一次。 - 安全性保障 :防止恶意代码替换核心类(如自定义
java.lang.String)。
- 避免类重复加载 :保证核心类(如
- 破坏场景:SPI 机制(如 JDBC 驱动)、热部署、OSGi 架构。
P9 9、使用 JProfiler 工具分析 OOM 原因
9.1 JProfiler 简介
JProfiler 是一款Java 性能分析工具,用于分析内存泄漏、OOM、CPU 性能瓶颈、线程问题等,是 Java 调优的必备工具。
9.2 JProfiler 分析 OOM 的步骤
9.2.1 准备工作
- 启动 JProfiler,连接目标 Java 进程(本地 / 远程)
- 配置 OOM 触发条件,开启内存快照
9.2.2 分析内存快照
- 查看堆内存使用情况:分析堆内存的占用率、对象数量
- 找出大对象:按对象大小排序,找出占用内存最多的对象(如大集合、缓存)
- 分析对象引用链:查看对象的引用关系,找出内存泄漏的根源(如未关闭的连接、静态集合)
- 分析 GC 情况:查看 GC 频率、GC 时间,找出 GC 瓶颈
9.3 常见 OOM 原因与解决方案
| OOM 类型 | 原因 | 解决方案 |
|---|---|---|
| 堆 OOM | 对象无限创建、内存泄漏、堆大小过小 | 优化代码、增大堆大小、修复内存泄漏 |
| 元空间 OOM | 类加载过多、动态代理过多、元空间过小 | 增大元空间大小、优化类加载、避免动态代理滥用 |
| 栈 OOM | 方法递归过深、方法调用链过长 | 优化递归逻辑、增大栈大小 |
| 直接内存 OOM | NIO 使用过多、直接内存大小过小 | 增大直接内存大小、优化 NIO 使用 |
P14 14、如何快速学习方法讲解
14.1 快速学习 JVM 的核心方法
1. 建立体系化认知
- 先画 JVM 体系结构图,掌握整体架构,再逐个模块深入
- 按照体系结构 → 内存区域 → 类加载 → 垃圾回收 → 调优工具的顺序学习
2. 抓重点,避细节
- 重点掌握高频面试点:类加载、双亲委派、内存区域、GC 算法、垃圾收集器、OOM 分析
- 非核心细节(如虚拟机底层实现)可后期深入,避免一开始陷入细节
3. 理论 + 实践结合
- 写代码复现 OOM、栈溢出、内存泄漏等问题
- 用 JProfiler、VisualVM 等工具分析 GC 日志、内存快照
- 分析生产环境的 GC 日志,优化 JVM 参数
4. 结合面试题学习
- 刷 JVM 高频面试题,通过面试题反推知识点,加深理解
- 总结面试题的答题思路,形成自己的知识体系
5. 读经典书籍
- 精读《深入理解 Java 虚拟机》,反复阅读,每次都有新收获
- 结合《Java 虚拟机规范》,理解 JVM 的底层原理
14.2 学习避坑指南
- 不要只背理论,不实践:JVM 是实践性极强的知识,不实践等于没学
- 不要一开始就啃底层细节:先掌握核心原理,再深入底层
- 不要忽视 GC 日志分析:GC 日志是调优的核心,必须掌握
- 不要盲目调优:调优前必须先监控、分析,再调整参数,避免盲目优化
14.3 学习路线图
- 基础阶段(1-2 周):掌握 JVM 体系结构、内存区域、类加载机制
- 核心阶段(2-3 周):深入垃圾回收算法、垃圾收集器、GC 日志分析
- 实战阶段(1-2 周):学习调优工具,解决 OOM、内存泄漏问题
- 进阶阶段(持续):理解 JVM 底层实现、即时编译、ZGC 等新特性
第二部分:JVM 体系结构与内存区域
P2 2、JVM 的体系结构
2.1 JVM 整体架构
JVM(Java Virtual Machine)是 Java 程序运行的基础,核心分为三大子系统 + 两大组件:
┌─────────────────────────────────────────────────┐
│ JVM 体系结构 │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ 类加载子系统 │ │ 运行时数据区 │ │ 执行引擎 │ │
│ └─────────────┘ └─────────────┘ └──────────┘ │
│ ┌─────────────┐ ┌──────────────────────────┐ │
│ │ 本地方法接口 │ │ 本地方法栈(Native方法) │ │
│ └─────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────┘
2.2 核心组件详解
| 组件 | 核心作用 |
|---|---|
| 类加载子系统 | 负责加载.class文件,验证、准备、解析、初始化类,双亲委派机制核心 |
| 运行时数据区 | JVM 内存管理核心,分为线程私有 (虚拟机栈、本地方法栈、程序计数器)和线程共享(堆、方法区) |
| 执行引擎 | 执行字节码,包含解释器、即时编译器(JIT),HotSpot 虚拟机核心 |
| 本地方法接口 | 调用 Native 方法(如 C/C++ 实现的方法),连接 Java 与底层系统 |
| 本地方法栈 | 为 Native 方法服务,记录 Native 方法调用状态 |
2.3 JVM 核心特性
- 跨平台:一次编写,到处运行(JVM 屏蔽了底层操作系统差异)
- 自动内存管理:垃圾回收机制自动回收堆内存,避免手动内存管理的问题
- 即时编译:JIT 编译器将热点代码编译为机器码,提升运行效率
P3 3、类加载器及双亲委派机制
3.1 类加载的生命周期
类加载分为5 个阶段:
- 加载 :获取
.class文件的二进制字节流,生成 Class 对象 - 验证:验证字节码的安全性、正确性,避免恶意代码
- 准备 :为类的静态变量分配内存,设置默认初始值(如
int=0、Object=null) - 解析:将符号引用替换为直接引用(如方法名替换为内存地址)
- 初始化:执行静态代码块、静态变量赋值,是类加载的最后一步
3.2 类加载器分类
JVM 提供 3 种核心类加载器,层级结构如下:
启动类加载器(Bootstrap ClassLoader)
↳ 扩展类加载器(Extension ClassLoader)
↳ 应用程序类加载器(Application ClassLoader)
↳ 自定义类加载器(Custom ClassLoader)
- 启动类加载器 :加载
JAVA_HOME/jre/lib下的核心类库(如rt.jar),由 C++ 实现,无 Java 对象 - 扩展类加载器 :加载
JAVA_HOME/jre/lib/ext下的扩展类库 - 应用程序类加载器 :加载用户项目
classpath下的类,是默认的类加载器 - 自定义类加载器 :继承
ClassLoader,实现自定义加载逻辑(如加密类加载、热部署)
3.3 双亲委派机制
3.3.1 核心原理
当类加载器收到加载请求时,优先委托父类加载器加载,父类加载器无法加载时,才由自己加载。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查类是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2. 委托父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 父类为null,委托启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载失败,不做处理
}
if (c == null) {
// 4. 父类加载失败,自己加载
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
3.3.2 核心优势
- 避免类重复加载 :保证核心类库(如
java.lang.Object)只加载一次 - 保证核心类安全 :防止恶意代码替换核心类(如自定义
java.lang.String) - 实现类加载的层级管理:规范类加载流程,提升系统稳定性
3.4 打破双亲委派机制
- 场景:热部署、SPI 机制(如 JDBC 驱动加载)、自定义类加载
- 实现 :重写
loadClass方法,不委托父类加载器,直接自己加载 - 注意:打破双亲委派需谨慎,避免类安全问题
P4 4、Java 历史 - 沙箱安全机制
4.1 沙箱安全机制的诞生背景
Java 早期(Applet 时代)需要在浏览器中运行远程代码,为了防止恶意代码破坏本地系统,诞生了沙箱安全机制。
4.2 沙箱安全机制核心原理
沙箱将 Java 程序限制在一个安全的运行环境中,限制程序对本地系统的访问权限,核心规则:
- 限制文件读写:禁止程序读写本地文件
- 限制网络访问:仅允许与代码来源服务器通信
- 限制系统调用:禁止调用本地系统资源(如注册表、硬件)
- 字节码验证:验证字节码的安全性,避免恶意代码
4.3 沙箱机制的演进
- Java 1.0:严格沙箱,所有 Applet 代码都受限制
- Java 1.1:引入签名类,信任签名的代码,放宽限制
- Java 2+ :引入权限模型(
SecurityManager),细粒度控制权限,沙箱机制演变为安全管理器 - Java 17+ :
SecurityManager被废弃,沙箱机制逐步退出历史舞台
4.4 沙箱机制的意义
- 保障了 Java Applet 的安全运行,推动了 Java 的普及
- 为 Java 安全体系奠定了基础,后续的安全机制均基于沙箱思想
- 是 JVM 类加载验证、字节码验证等安全机制的源头
P5 5、Native、方法区
5.1 Native 方法详解
- 定义 :Native 方法是由 ** 非 Java 语言(如 C/C++)** 实现的方法,用
native关键字修饰,没有方法体 - 作用:调用底层系统资源、提升性能、复用现有 C/C++ 代码
- 示例 :
Object.hashCode()、Thread.start()、Unsafe类的方法均为 Native 方法 - 注意:Native 方法不受 JVM 内存管理限制,可能导致内存泄漏、系统崩溃
5.2 方法区(Method Area)
5.2.1 方法区核心定义
方法区是线程共享 的内存区域,用于存储类的结构信息,包括:
- 类的全限定名、父类全限定名、接口
- 类的构造方法、普通方法、静态方法
- 类的静态变量、常量、运行时常量池
- 类的字节码、即时编译后的机器码
5.2.2 方法区的演进
-
JDK 7 及之前:方法区在永久代(PermGen)中,属于堆内存的一部分,大小固定,易导致 OOM
-
JDK 8 及之后 :永久代被移除,方法区迁移到元空间(Metaspace),直接使用本地内存,大小动态调整
-
核心区别 :
特性 永久代(PermGen) 元空间(Metaspace) 内存位置 堆内存 本地内存(直接内存) 大小限制 固定大小,易 OOM 动态调整,默认无上限 垃圾回收 回收效率低,易内存泄漏 自动回收,无 OOM 风险 配置参数 -XX:PermSize、-XX:MaxPermSize-XX:MetaspaceSize、-XX:MaxMetaspaceSize
5.2.3 运行时常量池
运行时常量池是方法区的一部分,用于存储类的常量、符号引用、字面量,是 Class 文件常量池的运行时表示。
- 特点 :动态性,运行时可将新的常量放入池中(如
String.intern()) - 区别:Class 文件常量池是静态的,运行时常量池是动态的
P6 6、深入理解一下栈
6.1 虚拟机栈(VM Stack)核心定义
虚拟机栈是线程私有的内存区域,每个线程创建时都会创建一个虚拟机栈,生命周期与线程一致。
-
作用 :记录 Java 方法的调用状态,每个方法调用对应一个栈帧(Stack Frame)
-
栈帧结构 :
┌─────────────────┐ │ 局部变量表 │ 存储方法的局部变量、参数、this ├─────────────────┤ │ 操作数栈 │ 存储运算的中间结果,是字节码执行的工作区 ├─────────────────┤ │ 动态链接 │ 指向运行时常量池的方法引用 ├─────────────────┤ │ 方法返回地址 │ 方法执行完后,返回调用者的地址 ├─────────────────┤ │ 附加信息 │ 异常表、调试信息等 └─────────────────┘
6.2 栈的核心特性
- 线程私有:每个线程的栈相互独立,无线程安全问题
- 后进先出(LIFO):最后调用的方法最先执行完,栈帧入栈出栈遵循 LIFO
- 栈大小限制 :栈大小由
-Xss参数配置,栈过深会导致栈溢出(StackOverflowError) - 栈内存分配:栈内存是连续的,分配速度快,性能高
6.3 栈溢出与栈深度
-
栈溢出场景:方法递归调用过深、方法调用链过长
-
示例 :
public class StackOverflowDemo { public static void recursive() { recursive(); // 无限递归,导致栈溢出 } public static void main(String[] args) { recursive(); } } -
解决方案 :优化递归逻辑、调整
-Xss参数增大栈大小(不推荐,易导致内存浪费)
6.4 本地方法栈(Native Method Stack)
- 作用:为 Native 方法服务,结构与虚拟机栈类似,记录 Native 方法的调用状态
- 区别:虚拟机栈服务 Java 方法,本地方法栈服务 Native 方法
- HotSpot 虚拟机:将本地方法栈与虚拟机栈合并,统一管理
P7 7、走近 HotSpot 和堆
7.1 HotSpot 虚拟机简介
HotSpot 是 Oracle/Sun 公司开发的主流 JVM 实现,是 Java 生态的核心虚拟机,具有以下特性:
- 即时编译(JIT):将热点代码编译为机器码,提升运行效率
- 自适应优化:根据程序运行情况,动态优化代码
- 分代垃圾回收:将堆分为新生代、老年代,采用不同的垃圾回收算法
- 开源:OpenJDK 开源,是 Java 虚拟机的标准实现
7.2 堆(Heap)核心定义
堆是线程共享 的内存区域,是 JVM 中最大的内存块,用于存储对象实例和数组,是垃圾回收的核心区域。
- 核心特点 :
- 线程共享,需考虑线程安全
- 垃圾回收的主要区域(GC 堆)
- 大小可通过
-Xms(初始堆大小)、-Xmx(最大堆大小)配置 - 物理上不连续,逻辑上连续
7.3 HotSpot 堆的内存布局
HotSpot 将堆分为新生代(Young Generation)和老年代(Old Generation),比例默认 1:2:
┌─────────────────────────────────────────────────┐
│ 堆内存 │
│ ┌─────────────┐ ┌──────────────────────────┐ │
│ │ 新生代 │ │ 老年代 │ │
│ │ (1/3堆大小) │ │ (2/3堆大小) │ │
│ └─────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────┘
- 新生代:存储新创建的对象,大部分对象朝生夕死
- 老年代:存储存活时间长的对象,经过多次 GC 后晋升
7.4 堆内存溢出(OOM)
-
场景:对象无限创建、内存泄漏、堆大小设置过小
-
示例 :
public class OOMDemo { public static void main(String[] args) { List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); // 无限创建对象,导致OOM } } } -
错误信息 :
java.lang.OutOfMemoryError: Java heap space
P8 8、新生区、永久区、堆内存调优
8.1 新生区(新生代)详解
新生代分为3 个区域,比例默认 8:1:1:
┌─────────────────────────────────────────────────┐
│ 新生代 │
│ ┌──────────┐ ┌──────┐ ┌──────┐ │
│ │ Eden区 │ │ S0区 │ │ S1区 │ │
│ │ (80%空间) │ │(10%) │ │(10%) │ │
│ └──────────┘ └──────┘ └──────┘ │
└─────────────────────────────────────────────────┘
- Eden 区:存储新创建的对象,大部分对象在此区域
- Survivor 区(S0/S1):存活对象的缓冲区,每次 GC 后存活对象复制到另一个 Survivor 区
- 对象晋升规则:对象在 Survivor 区存活 15 次(默认)后,晋升到老年代;大对象直接进入老年代
8.2 永久区 / 元空间(方法区)调优
- JDK 7 及之前(永久代) :
- 配置参数:
-XX:PermSize=128m、-XX:MaxPermSize=256m - 调优重点:避免永久代 OOM(
java.lang.OutOfMemoryError: PermGen space)
- 配置参数:
- JDK 8 及之后(元空间) :
- 配置参数:
-XX:MetaspaceSize=128m、-XX:MaxMetaspaceSize=512m - 调优重点:限制元空间大小,避免占用过多本地内存
- 配置参数:
8.3 堆内存调优核心参数
| 参数 | 作用 | 推荐值 |
|---|---|---|
-Xms |
初始堆大小 | 与-Xmx设置为相同值,避免堆动态扩容 |
-Xmx |
最大堆大小 | 物理内存的 1/4~1/2,服务器可设置为 8g~16g |
-Xmn |
新生代大小 | 堆大小的 1/3,如-Xms8g -Xmx8g -Xmn2g |
-XX:SurvivorRatio |
Eden 区与 Survivor 区比例 | 默认 8,可调整为 6~8 |
-XX:MaxTenuringThreshold |
对象晋升老年代的年龄 | 默认 15,可根据业务调整 |
-XX:+UseConcMarkSweepGC |
使用 CMS 垃圾收集器(老年代) | 适合低延迟应用 |
-XX:+UseG1GC |
使用 G1 垃圾收集器 | 适合大堆内存(>4g)应用 |
8.4 堆内存调优最佳实践
- 设置堆大小固定 :
-Xms与-Xmx相同,避免堆动态扩容导致的性能波动 - 合理分配新生代大小:根据对象存活周期调整,避免对象频繁晋升老年代
- 选择合适的垃圾收集器:低延迟应用用 G1/CMS,高吞吐量应用用 Parallel GC
- 监控 GC 日志 :通过
-XX:+PrintGCDetails、-Xloggc:gc.log分析 GC 情况,优化参数
第三部分:GC 算法、JMM 与总结(对应 P10-P13 + JMM)
P10 10、GC 介绍之引用计数法
10.1 垃圾回收(GC)核心定义
GC(Garbage Collection)是 JVM 的自动内存管理机制,用于回收不再被引用的对象,释放内存,避免内存泄漏。
- 核心目标:识别垃圾对象、回收垃圾对象、整理内存
- 垃圾对象定义:不再被任何引用指向的对象
10.2 引用计数法(Reference Counting)
10.2.1 核心原理
为每个对象添加一个引用计数器,当有引用指向对象时,计数器 + 1;引用失效时,计数器 - 1;计数器为 0 时,对象为垃圾,可回收。
对象A ← 引用1 → 计数器=1
对象A ← 引用2 → 计数器=2
引用1失效 → 计数器=1
引用2失效 → 计数器=0 → 回收对象A
10.2.2 优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,判定垃圾快 | 无法解决循环引用问题(如 A 引用 B,B 引用 A,计数器均为 1,无法回收) |
| 实时性高,对象死亡立即回收 | 计数器维护开销大,影响性能 |
10.2.3 应用场景
- Python、PHP 等语言使用引用计数法
- JVM不使用引用计数法,因为无法解决循环引用问题
P11 11、GC 之复制算法
11.1 复制算法(Copying)核心原理
将内存分为两个相等的区域,每次只使用一个区域;当该区域满时,将存活对象复制到另一个区域,然后清空当前区域。
┌─────────────┐ ┌─────────────┐
│ 区域A │ │ 区域B │
│ (使用中) │ │ (空闲) │
└─────────────┘ └─────────────┘
↓ 复制存活对象
┌─────────────┐ ┌─────────────┐
│ 区域A(清空) │ │ 区域B(使用)│
└─────────────┘ └─────────────┘
11.2 复制算法的应用场景
- 新生代:新生代对象朝生夕死,存活对象少,复制算法效率高
- HotSpot 新生代实现:将新生代分为 Eden 区和两个 Survivor 区(S0/S1),比例 8:1:1,每次 GC 将 Eden 和一个 Survivor 的存活对象复制到另一个 Survivor 区
11.3 优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,回收效率高 | 内存利用率低,只能使用一半内存 |
| 无内存碎片 | 存活对象多时,复制开销大 |
11.4 优化:分代复制
HotSpot 将新生代分为 Eden 区和两个 Survivor 区,避免了内存浪费,同时保证了复制效率:
- 大部分对象在 Eden 区死亡,每次 GC 仅复制少量存活对象到 Survivor 区
- 两个 Surviv 区交替使用,保证内存无碎片
P12 12、GC 之标记压缩清除算法
12.1 标记 - 清除算法(Mark-Sweep)
12.1.1 核心原理
分为标记 和清除两个阶段:
-
标记:遍历所有对象,标记出存活对象(可达性分析)
-
清除:清除未标记的垃圾对象,释放内存
┌─────────────────────────────────┐
│ 内存区域:存活 垃圾 存活 垃圾 存活 │
│ ↓ 标记: √ × √ × √ │
│ ↓ 清除: 存活 存活 存活 │
└─────────────────────────────────┘
12.1.2 优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,无需移动对象 | 产生大量内存碎片,影响内存分配效率 |
| 适合存活对象多的场景 | 清除效率低,需遍历所有对象 |
12.2 标记 - 压缩算法(Mark-Compact)
12.2.1 核心原理
在标记 - 清除的基础上,增加压缩阶段:
-
标记:标记存活对象
-
压缩:将存活对象向内存一端移动,整理内存
-
清除:清除压缩后的垃圾对象
┌─────────────────────────────────┐
│ 内存区域:存活 垃圾 存活 垃圾 存活 │
│ ↓ 标记: √ × √ × √ │
│ ↓ 压缩: 存活 存活 存活 │
│ ↓ 清除: 清除后面的垃圾区域 │
└─────────────────────────────────┘
12.2.2 优缺点
| 优点 | 缺点 |
|---|---|
| 无内存碎片,内存分配效率高 | 移动对象开销大,影响性能 |
| 适合存活对象多的场景(老年代) | 标记、压缩、清除三个阶段,效率低 |
12.3 两种算法的应用场景
- 标记 - 清除:适合老年代,存活对象多,垃圾对象少,内存碎片可接受
- 标记 - 压缩:适合老年代,需要整理内存,避免内存碎片,如 Serial Old、Parallel Old 收集器
- 复制算法:适合新生代,存活对象少,效率高
P13 13、GC 算法总结和鸡汤
13.1 GC 算法核心总结
| 算法 | 核心原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 引用计数法 | 计数器记录引用数,为 0 则回收 | 小对象、实时性要求高 | 实现简单、实时性高 | 无法解决循环引用、开销大 |
| 复制算法 | 分区域复制存活对象 | 新生代(存活对象少) | 效率高、无碎片 | 内存利用率低、复制开销大 |
| 标记 - 清除 | 标记存活对象,清除垃圾 | 老年代(存活对象多) | 实现简单、无需移动 | 内存碎片多、效率低 |
| 标记 - 压缩 | 标记 + 压缩 + 清除 | 老年代(需整理内存) | 无碎片、分配效率高 | 移动开销大、效率低 |
13.2 分代回收思想
JVM 采用分代回收思想,根据对象的存活周期,将堆分为新生代和老年代,采用不同的 GC 算法:
- 新生代 :对象朝生夕死,存活对象少,采用复制算法
- 老年代 :对象存活时间长,存活对象多,采用标记 - 清除 / 标记 - 压缩算法
13.3 垃圾收集器总结
HotSpot 提供多种垃圾收集器,适配不同场景:
| 收集器 | 适用区域 | 算法 | 特点 | 适用场景 |
|---|---|---|---|---|
| Serial | 新生代 | 复制算法 | 单线程、简单 | 客户端应用、小内存 |
| Parallel | 新生代 | 复制算法 | 多线程、高吞吐量 | 服务器应用、高吞吐量 |
| CMS | 老年代 | 标记 - 清除 | 低延迟、并发 | 低延迟应用、互联网服务 |
| G1 | 整堆 | 标记 - 压缩 | 分区域、低延迟 | 大堆内存(>4g)、低延迟 |
| ZGC | 整堆 | 染色指针 | 低延迟(<10ms) | 超大堆、极致低延迟 |
13.4 学习 JVM 的 "鸡汤"
- JVM 是 Java 的根基:掌握 JVM,才能真正理解 Java 的运行原理,写出高性能、稳定的代码
- 理论 + 实践是唯一捷径:不要只背理论,要动手写代码、用工具分析,才能真正掌握
- JVM 是面试的敲门砖:Java 后端面试必问 JVM,掌握 JVM,才能拿到心仪的 offer
- 持续学习,不断深入:JVM 一直在演进(如 ZGC、Shenandoah),要持续学习,跟上技术发展
16. JMM(Java 内存模型)
16.1 定义
JMM(Java Memory Model,Java 内存模型)是一套抽象的逻辑模型,不是真实的硬件内存布局,它用来规范多线程环境下,线程如何与主内存交互、如何保证可见性、原子性、有序性。
16.2 核心作用
- 屏蔽不同操作系统、硬件的内存访问差异
- 解决多线程并发下的原子性、可见性、有序性问题
- 规定线程工作内存与主内存的数据同步规则
16.3 三大特性(面试必考)
-
原子性 一个操作不可分割、不可中断,要么全部执行成功,要么全部不执行。保证手段:
synchronized、Lock、Atomic原子类。 -
可见性 一个线程修改了共享变量,其他线程能立即看到最新值。保证手段:
volatile、synchronized、Lock。 -
有序性 程序执行顺序按照代码顺序执行,禁止指令重排。保证手段:
volatile(禁止重排)、synchronized、happens-before原则。
16.4 volatile 关键字作用
- 保证变量可见性
- 禁止指令重排序
- 不保证原子性
16.5 happens-before 原则
JMM 用来判断线程是否安全、数据是否竞争的核心规则,常见几条:
- 程序次序规则
- 锁定规则(
synchronized解锁一定发生于后续加锁之前) - volatile 变量规则
- 线程启动规则
- 传递性规则
17. JVM 整体总结
17.1 JVM 核心结构一句话总结
JVM 是运行在操作系统之上的虚拟计算机 ,通过类加载子系统 加载字节码,在运行时数据区 分配内存,由执行引擎 执行代码,并通过GC 自动回收垃圾,最终实现 Java 程序跨平台、安全、自动管理内存。
17.2 内存区域一句话总结
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:堆、方法区(元空间)
- 堆存对象,栈存方法调用,方法区存类信息
17.3 GC 一句话总结
根据对象生命周期分代回收:
- 新生代对象存活率低 → 复制算法
- 老年代对象存活率高 → 标记 - 清除 / 标记 - 压缩
- 目的是减少 STW、避免 OOM、提升程序稳定性
17.4 学习 JVM 的意义
- 理解 Java 程序底层运行原理
- 排查 OOM、内存泄漏、死锁、CPU 飙高问题
- 服务端性能调优必备
- 中高级 Java 面试核心考点
补充:三种 JVM(主流实现)
| 实现 | 特点 | 适用场景 |
|---|---|---|
| HotSpot | Oracle/Sun 官方实现,性能优异,生态最广 | 绝大多数 Java 应用 |
| OpenJ9 | IBM 开源,内存占用低,启动快 | 嵌入式、资源受限环境 |
| GraalVM | 新一代虚拟机,支持多语言,原生镜像 | 微服务、Serverless、多语言混合开发 |
补充:PC 寄存器(程序计数器)
- 定义 :一块线程私有的小内存区域。
- 作用 :记录当前线程正在执行的 Java 字节码的行号。
- 特性 :
- 线程私有,无线程安全问题。
- 唯一不会抛出 OOM 的区域。
- 执行 Native 方法时,计数器值为
undefined。