# JVM
# 1.内存结构
# 1.堆
堆是JVM内存的最大一块,通常存放对象和数组,被所有线程共享
1)特点
通过new关键字创建的对象都会使用堆内存,数组和字符串的常量池也存储在堆中
所有线程共享
对象要考虑线程安全问题,有垃圾回收机制
2)堆内存分配
新生代占1/3,老年代占2/3
新生代:由伊甸园(Eden)和两个幸存者区组成
当你创建一个新对象时,JVM 会优先尝试在新生代的 Eden 区 分配内存。因为大部分对象用完即弃,放在Eden区方便清理。(占新生代区的80%)
存者区包括两个区域,分别为From区(10%) 和 To区(10%)。幸存者区的数据是在 From 区和 To 区之间进行交换的,目的是过滤掉短命对象。
- 举例:当from 区和to区都是null的时候,第一次从新生代eden进行垃圾回收,会把存活下来的对象放入from区,下次垃圾回收会把存活下来的数据放入to区,然后from区清空。再下次垃圾回收会把存活下来的数据放入from区,然后to区清空。直到达到一定的年龄后,这些对象会被晋升到老年代。
老年代:用于存放经过多次GC依旧存活的对象或者新生代放不下的大对象
3)晋升到老年代的方式
(熬老头)在幸存者区的年龄达到15之后
(大对象)大对象直接放入老年代中,防止因为在Eden区和幸存者区过度复制,浪费资源
(幸存者区预防)JVM发现幸存者区相同年龄的对象的内存占据了空间的多一半,就赶紧让>=这批年龄的对象去老年代,避免后续 GC 时放不下
新生代垃圾清理(Minor GC)之前,JVM会先检查老年代的大小,防止因为这些新生代全部活过来而放不下,如果真放不下就进行Full GC清理整个堆和方法区,如果还是不够或者为了保险新生代的GC可能会被直接晋升到老年代(本质还是由于防止幸存者区内存爆炸)
# 2.虚拟机栈
每个线程都有自己的虚拟机栈,用于存储栈帧。每当一个线程调用一个方法时,JVM就会为这个方法创建一个栈帧,存入虚拟机栈中。栈帧中存储
局部变量
运算过程中的操作栈(这是一个“计算器”区域。比如你要算
1 + 1,虚拟机会把两个 1 压入操作数栈,然后执行加法指令,把结果 2 再压回去。)动态链接信息(把符号引用(比如
System.out.println)转换成直接内存地址,确保方法能调用到真正的代码。)方法的返回地址(记录从哪过来执行此方法,方便后续继续向下执行)
两大经典异常
1)StackOverflowError(栈溢出)
原因:线程请求的栈深度 > 虚拟机允许的最大深度。
场景:死循环,自己调用自己
2) OutOfMemoryError: unable to create new native thread
原因: 虚拟机栈是线程私有的。如果你创建了太多线程,每个线程都要分一点栈内存(默认可能 1M),操作系统的内存就被耗尽了。
场景: 高并发场景下,如果没有使用线程池,而是 new Thread() 狂创建线程。
栈是不是越大越好? 不是,如内存为500M,每个栈为1M,那么最多可以有500个线程并发。所以栈越大,线程越少。
# 3.程序计数器
记住下一条jvm指令的执行地址
特点:
线程私有
不存在内存溢出(jvm规定)
# 4.本地方法栈
本地方法栈的结构与虚拟机栈类似,也是由栈帧(Stack Frame)组成的,栈帧中保存了Native方法的局部变量、操作数栈、方法出口等信息。与虚拟机栈不同的是,本地方法栈中的方法不是用Java语言编写的,而是用其它语言编写的,比如C、C++等。因此,本地方法栈的结构与虚拟机栈类似,但是用于调用本地方法。
本地方法栈就是 Java 为了“跨界”调用底层 C/C++ 代码而预留的专属通道。
# 5.方法区
方法区是一块用于存储以下数据的内存区域。
类的相关信息:类名、父类名、接口列表、修饰符
常量池:放了编译期生成的各种字面量(比如代码里写的
int a = 100;中的 100。如果是字符串存储的是符号引用对象)和符号引用(比如System.out.println这个方法的名字和描述符)。静态变量:类的静态变量放在方法区里
字段和方法信息:方法名、参数列表、返回值类型、代码逻辑(字节码)
实现方式:永久代、元空间
JDK 1.7 及以前:永久代
- 那时候方法区的实现叫“永久代”,它本质上是堆内存的一块特殊区域。
- 缺点: 因为和堆连在一起,如果加载的类太多(比如用了大量的反射或动态代理),容易导致堆溢出,而且垃圾回收很难清理它。
JDK 1.8 及以后:元空间
元空间不在使用堆内存,而是使用本地内存(操作系统内存)
只要你的电脑内存够大,理论上就不会因为加载类太多而报
OutOfMemoryError错误
# 6.运行时常量池和字符串常量池的区别
| 特性 | 运行时常量池 | 字符串常量池 |
|---|---|---|
| 存储位置 (JDK 1.8+) | 元空间 (本地内存) | 堆内存 |
| 存储内容 | 类信息、方法信息、字段信息、各种字面量 (int, double等)、符号引用 | 仅存储字符串对象 (及其引用) |
| 主要作用 | 支撑类加载、解析符号引用 | 字符串去重,节省内存 |
| 生命周期 | 随类的卸载而消失 | 随垃圾回收 (GC) 进行清理 |
注:运行时常量池和字符串常量池存的字符串都是对象
# 7.总结
程序计数器:存储jvm指令的执行,不会内存溢出
虚拟机栈:每个线程运行时所需要的内存,每个栈由多个栈帧组成,对应着每次方法调用时所占的内存,每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
本地方法栈:存储非Java代码编写的本地方法,和虚拟机栈类似
堆:通过new关键字创建的对象都会使用堆内存。同时包含字符串常量值和数组
方法区:它存储着每个类的结构,如类相关信息、静态变量、运行时常量池、字段和方法信息等
# 2.垃圾回收
# 1.垃圾判定
垃圾判定是指在编程中确定哪些内存中的对象是“垃圾”,即不再被应用程序使用的对象,因此可以被垃圾回收器回收的过程。
垃圾回收(Garbage Collection,GC)主要采用两种基本方法:引l用计数法和可达性分析。
1)引用计数法
给每个对象分配一个引用计数器,每引用一次计数器加1,引用失效时,计数器减1
java一般不用,会导致引用循环的问题,相互引用,计数器永远不为0
2)可达性分析
JVM一般会从一组被称为GC Roots 的根对象出发,这些对象包括:
虚拟机栈中的引用对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
从GC Roots 开始,向下遍历所有对象的引用关系,形成一条条“引用链”。
如果一个对象到GCRoots 没有任何引用链相连,即从GC Roots 出发无法到达该对象,那么这个对象就被判定为“不可达”,也就是“垃圾”,可以被回收。
示例
public class ReachabilityAnalysis {
public static void main(String[] args) {
ReachabilityAnalysis obj = new ReachabilityAnalysis(); // 对象obj是可达的,因为它被栈上的引用变量所引用
// 现在让我们断开这个引用
obj = null; // 此时对象不再可达
// 垃圾回收可以执行了,它将使用可达性分析来确定obj的内存是否可以被释放
System.gc();
}
}
2
3
4
5
6
7
8
9
10
11
# 2.垃圾回收算法
| 算法名称 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 先标记出所有需要回收的对象,然后统一清理。 | 实现简单。 | 会产生大量不连续的内存碎片。 | 早期 JVM,现代收集器的基础。 |
| 复制算法 | 将内存分为两块,每次只用一块。GC 时将存活对象复制到另一块,然后清空已使用的一块。 | 无内存碎片,实现简单,效率高。 | 内存利用率低,只有一半可用。 | 新生代(对象存活率低)。 |
| 标记-整理 | 先标记存活对象,然后让它们向内存一端移动,最后清理掉边界外的内存。 | 无内存碎片,内存利用率高。 | 移动对象成本高,效率相对较低。 | 老年代(对象存活率高)。 |
# 3、Minor GC 和 Full GC 的区别
Minor GC:对新生代的垃圾回收
Full GC :对堆(新生代、老年代)和方法区(永久代/元空间)的垃圾回收
| 对比维度 | Minor GC (年轻代GC) | Full GC (全局GC) |
|---|---|---|
| 回收范围 | 仅新生代 (Eden区 + Survivor区) | 整个堆 (新生代 + 老年代) + 方法区/元空间 |
| 触发时机 | 新生代 Eden 区空间不足,无法为新对象分配内存时 | 老年代/元空间不足、空间分配担保失败、显式调用 System.gc() 等 |
| 执行频率 | 非常频繁,因为对象“朝生夕死” | 频率很低,通常是 Minor GC 的十分之一或更低 |
| 执行速度 | 速度快,通常在毫秒级 | 速度慢,通常在秒级甚至更长 |
| 算法 | 复制算法 (Copying) | 标记-清除 (Mark-Sweep) 或 标记-整理 (Mark-Compact) |
| 性能影响 | 暂停时间短 (STW),对应用影响较小 | 暂停时间长 (STW),会导致应用明显卡顿 |
# 🧒 Minor GC:新生代的快速清理
Minor GC,也叫 Young GC,是 JVM 中最常见的垃圾回收活动。
工作流程:
- 触发:当程序不断创建新对象,新生代的 Eden 区被填满时,就会触发 Minor GC。
- 标记与复制:JVM 会使用复制算法,将 Eden 区和其中一个 Survivor 区(From Space)中仍然存活的对象,复制到另一个空的 Survivor 区(To Space)。
- 清理与晋升:复制完成后,Eden 区和 From Space 会被一次性清空。同时,存活对象的“年龄”会加 1。如果对象年龄达到阈值(默认15),或者 To Space 空间不足,这些对象就会被晋升到老年代。
特点:由于新生代中绝大多数对象都是“朝生夕死”的,存活率极低,所以复制的成本很小,执行速度非常快。
# 👴 Full GC:整个堆的全局清理
Full GC 是 JVM 中最重量级的垃圾回收,它会暂停所有应用线程(Stop-The-World),对性能影响巨大,是性能调优中需要重点规避的对象。
- 主要触发原因:
- 老年代空间不足:这是最常见的原因。当 Minor GC 后,有大量对象晋升到老年代,或者大对象直接进入老年代,导致老年代空间不足时,会触发 Full GC。
- 方法区/元空间不足:当加载的类信息过多,占满了方法区(JDK 8 及以后为元空间)时,会触发 Full GC。
- 空间分配担保失败:在进行 Minor GC 前,JVM 会检查老年代的剩余空间是否足以容纳新生代所有对象。如果不足,且之前的晋升平均值也超过了老年代剩余空间,就会直接触发 Full GC 来为老年代腾出空间。
- 显式调用:代码中调用了
System.gc(),JVM 会“建议”执行 Full GC(可通过-XX:+DisableExplicitGC参数禁用)。
# 4.垃圾回收器
# 🧬 经典垃圾回收器详解
在 G1 成为主流之前,JVM 的垃圾回收器设计具有明显的“分代”特征,即针对新生代和老年代使用不同的回收器。
# 1. Serial(线性) 收集器
最基础、历史最悠久的收集器。它在进行垃圾回收时,必须暂停所有其他工作线程(Stop-The-World),并且整个过程由一个线程完成。
- 新生代:采用复制算法。
- 老年代:采用标记-整理算法。
- 优点:没有线程交互开销,简单且高效。
- 缺点:STW 时间长,无法利用多核优势。
# 2. Parallel(并发) 收集器
Serial 收集器的多线程版本,也称为吞吐量优先收集器。它关注的是最大化应用程序的运行时间。
- 新生代:采用复制算法,多线程并行回收。
- 老年代:采用标记-整理算法,多线程并行回收(Parallel Old)。
- 优点:充分利用多核 CPU,吞吐量高。
- 缺点:停顿时间可能较长,不适合对延迟敏感的应用。
# 3. CMS (Concurrent Mark Sweep) 收集器
一款以获取最短回收停顿时间为目标的里程碑式收集器,首次实现了让垃圾回收线程与用户线程并发工作。
- 算法:基于标记-清除算法。
- 工作流程:
- 初始标记 (STW):标记 GC Roots 直接关联的对象。
- 并发标记:遍历整个引用链,与用户线程并发执行。
- 重新标记 (STW):修正并发期间标记发生变动的对象。
- 并发清除:清理垃圾对象,与用户线程并发执行。
- 优点:并发收集,低停顿。
- 缺点:会产生内存碎片;对 CPU 资源敏感;实现复杂。
# 🌟 现代垃圾回收器详解
随着硬件发展和应用需求变化,新一代收集器应运而生,旨在打破传统分代的限制,提供更优的性能。
# 1. G1 (Garbage-First) 收集器
G1 是 JDK 7 引入的实验性收集器,在 JDK 9 中成为默认收集器,旨在替代 CMS(标记-清除算法,产生内存碎片,不可预测停顿)。它最大的特点是不再物理隔离新生代和老年代,而是将整个堆划分为多个大小相等的独立区域(Region)。
- 算法:整体看是标记-整理算法,局部(Region之间)是复制算法,因此不会产生内存碎片。
- 核心思想:维护一个优先级列表,每次根据用户设定的停顿时间目标(
-XX:MaxGCPauseMillis),优先回收垃圾最多的 Region,实现“垃圾优先”回收。 - 优点:
- 可预测的停顿:用户可以指定期望的停顿时间。
- 高效利用内存:不产生碎片。
- 适用于大堆:能高效处理 6GB 以上的堆内存。
# 2. ZGC 收集器 (Java21新特性)
ZGC 是一款可扩展的低延迟垃圾回收器,在 JDK 11 中作为实验特性引入,并在后续版本中不断成熟。
- 设计目标:停顿时间不超过 10 毫秒,且停顿时间不会随堆大小增长而增长。
- 核心技术:采用了着色指针和读屏障等先进技术,将大部分耗时的操作(如标记、重定位)都与用户线程并发执行。
- 适用场景:对延迟有极端要求的应用,例如金融交易系统、大型在线游戏等。
# 3.类的加载过程
1)加载 在加载阶段,类加载器负责读取.class文件,将二进制数据转为方法区中运行时的数据结构,并在堆中生成一个java.lang.Class对象作为方法区数据的访问入口
2)验证
确保类被加载的正确性,例如是否符合语法规范、是否存在异常、符号引用是否真的存在
3)准备
将类的静态变量分配内存,并初始化为默认值
4)解析
将符号引用解析为真实引用
5)初始化
对类中静态变量的初始化和执行静态代码块,在初始化阶段,虚拟机会完成以下工作:
- 执行类构造器(方法),该方法是由编译器自动收集类中所有静态变量的赋值动作和静态代码块中的语句合并产生的。
- 如果类中存在多个静态代码块或静态变量初始化语句(将0初始化为实际值),虚拟机会按照其在源代码中的顺序依次执行。
# 静态变量与非静态变量的赋值与初始化过程
1、静态变量
static int number;
准备阶段:分配内存空间在方法区(JDK 8之前称为永久代,JDK 8及以后为元空间),设置初始值为0。
初始化阶段:在静态代码块中被赋值为100。
static String stringConstant = “Hello, World”;
准备阶段:方法区分配内存并设置stringConstant初始值为null。
初始化阶段:在字符串常量池中创建字符串"Hello, World",并让stringConstant指向它。
static final int finalNumber = 42;
准备阶段:因为是 常量(final修饰的静态字段),finalNumber在方法区分配内存空间,并且直接设置为42,不需要等待初始化阶段。
2、实例变量
Long num = 12L;
实例化阶段:当创建一个MyClass实例(new MyClass())时,将为num分配内存空间在堆上,并且创建一个Long对象,值为12。
Object o;
实例化阶段:当创建MyClass实例时,在堆上分配内存空间,但初始值为null。如果后续有赋值操作,则会指向具体的对象。
final Integer in;
实例化阶段:和Object o一样,在实例化时在堆空间分配内存但初始值为null。需要注意的是,因为它是final的,它必须在构造函数中或者在声明时被初始化,否则会导致编译错误。
3、静态初始化块
static {…}
初始化阶段:在类的初始化阶段执行,即在类被首次主动使用时。
# 4.双亲委派机制
当一个类加载器收到类加载请求时,它首先不会自己去尝试加载,而是将这个请求委派给父类加载器去完成。这个委派过程会一直向上,直到顶层的启动类加载器。只有当父类加载器无法完成加载(即在其搜索范围内未找到该类)时,子类加载器才会尝试自己去加载。
优点:
- 安全性:确保核心类库(如
java.lang.Object)不会被用户自定义的类所替换,保证了 Java 程序的稳定运行。 - 唯一性:避免同一个类在 JVM 中被重复加载,保证了类的唯一性。
| 类加载器名称 | 职责与加载范围 | 实现语言 |
|---|---|---|
| 启动类加载器 (Bootstrap ClassLoader) | 负责加载 JVM 核心类库,如 <JAVA_HOME>/lib 目录下的 rt.jar 等。它是所有类加载器的顶层。 | C/C++ (JVM原生) |
| 平台类加载器 (Platform ClassLoader) | JDK 9 引入,取代了扩展类加载器。负责加载平台模块中的类。 | Java |
| 应用程序类加载器 (Application ClassLoader) | 也称系统类加载器。负责加载用户类路径(Classpath)上指定的类库,即我们日常编写的代码和第三方依赖。 | Java |
启动类加载器 加载:
rt.jar→ Java 最核心的类:String、Object、集合、IO 这些必用基础类扩展类加载器 加载:
ext目录下的各种扩展 jar,比如:图形界面、加密、多媒体、字体、国际化等附加功能包
不是日常写代码必须用的,属于锦上添花扩展功能
# 打破双亲委派机制
要打破双亲委派机制,可以自定义类加载器,并重写 ClassLoader 类中的 loadClass(String name, boolean resolve) 方法(或者是 findClass(String name) 方法,根据具体需求)。自定义的类加载器可以先尝试加载类,而不是直接委派给父加载器。
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 首先, 检查请求的类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 尝试自己加载类,而不是委派给父类加载器
c = findClass(name);
} catch (ClassNotFoundException e) {
// 如果自己无法加载类,那么调用父类加载器尝试加载
c = super.loadClass(name);
}
}
return c;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在这里加入具体的类加载逻辑,比如从文件系统中读取.class文件的字节流
// byte[] classBytes = ...;
// return defineClass(name, classBytes, 0, classBytes.length);
// 示例中没有具体实现,因为它通常需要读取文件或其他数据源中的类数据
throw new ClassNotFoundException();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 5.JVM常见的调优参数
# 💾 一、 内存配置参数(基础必配)
这是调优的第一步,决定了 JVM 能使用多少物理内存。
表格
| 参数 | 说明 | 推荐设置/最佳实践 |
|---|---|---|
-Xms | 初始堆内存大小 | 建议与 -Xmx 设为相同值,避免运行时动态扩容带来的性能抖动。 |
-Xmx | 最大堆内存大小 | 通常设置为物理内存的 60%~80%,需预留空间给非堆内存。 |
-Xmn | 新生代大小 | 默认是堆的 1/3。高并发场景可适当调大(如堆的 1/2),减少对象过早进入老年代。 |
-Xss | 线程栈大小 | 默认 1M。线程数多(如 Netty 应用)可调小至 512k,防止 StackOverflowError 或内存耗尽。 |
-XX:MetaspaceSize | 元空间初始大小 | JDK 8+ 使用。建议设置初始值(如 256m),避免频繁扩容。 |
-XX:MaxMetaspaceSize | 元空间最大大小 | 必须设置上限(如 512m),防止元空间无限制增长耗尽物理内存。 |
# 🧹 二、 垃圾回收器参数(核心调优)
根据业务场景(低延迟 vs 高吞吐)选择合适的收集器。目前主流选择是 G1 和 ZGC。
表格
| 参数 | 说明 | 适用场景 |
|---|---|---|
-XX:+UseG1GC | 启用 G1 垃圾回收器 | 生产环境首选,适合大内存、多核,追求低延迟与高吞吐的平衡。 |
-XX:MaxGCPauseMillis | G1 最大停顿时间目标 | 默认 200ms。调小该值(如 100ms)会减少停顿,但可能导致 GC 频率增加。 |
-XX:+UseZGC | 启用 ZGC 回收器 | JDK 15+ 推荐。追求极低延迟(<10ms),适合实时性要求极高的系统。 |
-XX:+UseParallelGC | 启用 Parallel 回收器 | 关注高吞吐量,适合后台批处理、科学计算任务,不介意较长的停顿。 |
-XX:SurvivorRatio | Eden 与 Survivor 比例 | 默认 8 (Eden:S0:S1 = 8:1:1)。若对象存活率高,可调小该值(如 4)以增大 Survivor 区。 |
# 🛠️ 三、 辅助诊断与日志参数(排错必备)
生产环境必须配置这些参数,以便在发生 OOM 或性能问题时留存“案发现场”证据。
表格
| 参数 | 说明 | 作用 |
|---|---|---|
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 Dump | 发生内存溢出时自动生成堆转储文件(.hprof),用于分析内存泄漏。 |
-XX:HeapDumpPath | 指定 Dump 文件路径 | 指定 .hprof 文件的保存位置,防止文件生成在默认目录找不到。 |
-Xloggc (JDK8) / -Xlog (JDK9+) | 开启 GC 日志 | 记录 GC 发生的详细时间、耗时和内存变化,是调优的最重要依据。 |
-XX:+DisableExplicitGC | 禁用手动 GC | 禁止代码中调用 System.gc(),防止其触发 Full GC 导致系统卡顿。 |
# 6.JVM参数引用类型
强引用是最常见的引用类型,如果一个对象具有强引用,那么它永远不会被垃圾回收器回收,即使这个对象以后不再需要了,也可能导致内存 泄漏。
软引用是用来描述一些还有用但并非必需的对象。在 JVM 将要抛出内存溢出异常之前,会把这些对象列入回收范围进行第二次回收。
弱引用不像软引用那么强大,它用来描述非必需对象,但是其强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾回收之前。
虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
虚引用并不直接决定对象的回收时机,而是提供了一种机制,允许程序员在对象被回收时得到通知,以便执行进一步的清理工作。
常用来清理堆外存,比如在使用 NIO 的
DirectByteBuffer时,数据存储在堆外内存中。当内存清理完这个DirectByteBuffer对象后,外存的资源就需要被释放,这时候虚引用会通知你来释放外存资源
← JVM