Java 代码(.java 文件)要先使用
javac
编译器编译为.class
文件(字节码),紧接着再通过JVM 的执行引擎(Execution Engine) 负责处理 Java 字节码并执行,它的主要组成部分包括:
- 解释器(Interpreter):逐行解释字节码执行,启动快但执行速度较慢。
- JIT 编译器(Just-In-Time Compiler):将热点字节码编译为本地机器码,提高执行效率。
- 垃圾回收器(Garbage Collector, GC):管理 Java 堆中的对象回收,提升内存管理效率。
(1)Java 代码先编译成字节码
javac
编译器编译为 .class
文件(字节码)。(2)JVM 先解释执行,再逐步编译为机器码
(3)JIT 编译器优化热点代码
总结:
解释器负责启动时快速执行,JIT 编译器负责优化热点代码。 这就是 Java 既有解释语言的灵活性,又有编译语言的高效性的原因。
概述
程序计数器(PC Register)是 JVM 中一个小型的内存区域,它的作用是记录当前线程正在执行的字节码指令地址。
特点
undefined
(未定义)。作用
概述
Java 虚拟机栈(JVM Stack)用于存储 Java 方法执行时的栈帧(Stack Frame),是 Java 方法执行的基础。
特点
-Xss
参数设置(例如 -Xss1M
设置栈大小为 1MB)。栈帧(Stack Frame)的组成
每个栈帧对应一个正在执行的方法,包含:
局部变量表(Local Variable Table)
long
和 double
类型占 两个 存储单元,其它数据类型占 一个 存储单元。操作数栈(Operand Stack)
iadd
指令会从操作数栈中弹出两个整数相加后再压入栈中)。动态链接(Dynamic Linking)
方法返回地址(Return Address)
栈的空间大小
-Xss
参数控制,通常 默认 1MB,可根据需求调整: java -Xss512k MyApplication
StackOverflowError
(递归调用过深)。为什么栈是从高地址向低地址增长?
LIFO 机制(后进先出)
CPU 设计 & 指针运算优化
ESP
(栈指针寄存器)直接递减来分配新的栈帧,提高性能。可能出现的异常
概述
本地方法栈(Native Method Stack)与 JVM 栈类似,但它用于存储 Native 方法的执行信息。
特点
StackOverflowError
OutOfMemoryError
作用
System.arraycopy()
方法时,JVM 需要通过本地方法栈进入 C 代码执行内存拷贝。概述
堆(Heap)是 JVM 内存中最大的区域,用于存储所有对象实例。
特点
-Xms
(初始大小)和 -Xmx
(最大大小)参数控制。OutOfMemoryError: Java heap space
。堆的分代
垃圾回收
堆的空间大小
堆的大小可以通过 JVM 参数设置:
-Xms
:堆的初始大小(默认通常是 1/64 物理内存)-Xmx
:堆的最大大小(默认通常是 1/4 物理内存)java -Xms256m -Xmx1024m MyApplication
默认情况下,堆的大小会随着 GC 调整,但不能超过 -Xmx
设定的上限。
为什么堆是从低地址向高地址增长?
堆是动态分配的,大小不固定
-Xms
(最小堆)和 -Xmx
(最大堆)设置堆的大小。操作系统内存管理
malloc()
和 JVM
的 new
语义中,分配的堆空间通常从低地址向高地址增长。概述
存储类的元数据(方法信息、静态变量、运行时常量池)。关于类的信息存储在这里。
JDK 7 及以前
-XX:PermSize
和 -XX:MaxPermSize
限制。JDK 8 及以后
-XX:MetaspaceSize
控制其大小。常量池类型 | 存储位置(JDK 6 及以前) | 存储位置(JDK 7+) | 存储内容 | 作用 |
---|---|---|---|---|
类文件常量池(Class File Constant Pool) | .class 文件 |
.class 文件 |
字面量(数值、字符串)、符号引用 | 编译时生成,在运行时加载到运行时常量池 |
运行时常量池(Runtime Constant Pool) | 方法区(永久代 PermGen) | 方法区(元空间 Metaspace) | 从类文件加载的常量(字面量、符号引用)、运行时生成的常量(String.intern() ) |
动态解析符号引用、存储运行时常量 |
字符串常量池(String Pool) | 方法区(永久代 PermGen) | 堆(Heap) | String 字面量、intern() 方法存入的字符串 |
优化字符串存储,减少内存占用 |
常量类型 | 说明 |
---|---|
字面量 | 编译时生成的常量,如 final 修饰的常量、字符串字面量、数值(int、float、double、long)等。 |
符号引用 | 类名、字段名、方法名的符号引用(未解析为具体地址),用于支持动态链接。 |
方法引用 | 方法的符号引用,如方法的名称、描述符等。 |
内存区域 | 线程私有/共享 | 主要作用 | 可能抛出的异常 |
---|---|---|---|
程序计数器 | 线程私有 | 记录当前线程执行的字节码地址 | 无 |
JVM 栈 | 线程私有 | 存储局部变量表、操作数栈 | StackOverflowError 、OutOfMemoryError |
本地方法栈 | 线程私有 | 执行 Native 方法 | StackOverflowError 、OutOfMemoryError |
堆 | 线程共享 | 存储对象实例 | OutOfMemoryError: Java heap space |
方法区(元空间) | 线程共享 | 存储类信息、静态变量 | OutOfMemoryError: Metaspace |
直接内存 | 线程共享 | 用于高效 I/O(如 NIO) | OutOfMemoryError: Direct Buffer Memory |
(1)启动类加载器(Bootstrap ClassLoader)
rt.jar
,charsets.jar
等)。null
)。.class
文件。主要加载的类包括:
java.lang.*
(如 String
、Integer
、System
)java.util.*
(如 ArrayList
、HashMap
)java.io.*
(如 File
、InputStream
)java.nio.*
、java.net.*
等(2)扩展类加载器(ExtClassLoader)
lib/ext/
目录下的扩展类库(如 javax.crypto.*
)。ClassLoader.getSystemClassLoader().getParent()
获取。主要加载的类包括:
javax.crypto.*
(加密库)javax.sound.*
(声音处理库)javax.imageio.*
(图像处理库)(3)应用类加载器(AppClassLoader/ SystemClassLoader)
ClassLoader.getSystemClassLoader()
获取到它。主要加载的类包括:
com.example.MyClass
org.springframework.*
classpath
下的 .class
文件(4)自定义类加载器
classpath
下的类,如果需要从网络、数据库、加密文件中加载 .class
文件,必须使用自定义类加载器。 AppClassLoader
共享 classpath
,如果多个模块的类存在相同的包名,可能会发生类冲突。.class
文件容易被反编译,我们可以加密 .class
文件,并使用自定义类加载器在运行时解密。如何自定义类加载器?
方式 1:继承 ClassLoader。
Java 提供了 ClassLoader
抽象类,允许我们创建自己的类加载器。
方式 2:继承 URLClassLoader
。如果 .class
文件存放在 JAR 或远程服务器上,我们可以继承 URLClassLoader
来动态加载类。
1. 什么是双亲委派机制?
双亲委派机制 是指 类加载器在加载一个类时,先委托其父类加载器加载,只有当父类加载器无法加载该类时,子类加载器才会尝试自己加载。
2. 双亲委派机制的工作流程
ClassLoader
需要加载一个类时,它不会自己直接加载,而是先委托给父类加载器。Bootstrap ClassLoader
(顶层)。Bootstrap ClassLoader
无法加载该类(即不是核心类库),那么父类加载器会逐层返回,直到应用类加载器(AppClassLoader
)。3. 为什么要使用双亲委派机制?
✅ 保证 Java 运行时的安全性
java.lang.String
始终由 Bootstrap ClassLoader
加载)。✅ 提高类加载的效率
阶段 | 说明 |
---|---|
加载(Loading) | 读取 .class 文件,将字节码转换为 Class 对象。 |
验证(Verification) | 检查字节码是否合法,防止恶意代码执行。 |
准备(Preparation) | 分配静态变量的内存,初始化默认值(不赋具体值)。 |
解析(Resolution) | 将符号引用转换为直接引用(方法地址、变量地址)。 |
初始化(Initialization) | 执行 static 代码块,赋值静态变量。 |
步骤:
.class
文件(从硬盘、网络、JAR 包等加载类的字节码)。示例
Class> clazz = Class.forName("java.lang.String");
Class.forName()
方法会触发类加载。(1)验证(Verification)
.class
文件格式是否正确,防止恶意代码(字节码验证)。(2)准备(Preparation)
int
变量默认值为 0
)。public static int a = 10; // 在准备阶段 a=0,在初始化阶段才变成10
(3)解析(Resolution)
java.lang.String
)转换为直接引用(JVM 内存地址)。()
静态代码块,初始化静态变量(准备阶段是为静态变量赋默认值,而这里是要设置你所定义的值)。示例
class Example {
static {
System.out.println("Static block executed");
}
public static int value = 10;
}
public class Test {
public static void main(String[] args) {
System.out.println(Example.value);
}
}
当 Java 代码执行 new
关键字创建对象时,JVM 需要完成以下步骤:
.class
文件,并完成 类加载、验证、准备、解析、初始化 过程(即 类的五个生命周期阶段)。JVM 在堆(Heap)中为新对象分配内存,分配策略取决于 内存是否连续:
指针碰撞(Bump the Pointer)(内存连续时):
空闲列表(Free List)(内存不连续时):
线程私有分配缓冲区(TLAB, Thread Local Allocation Buffer):
class Example {
int x; // 默认值 0
boolean y; // 默认值 false
String s; // 默认值 null
}
这里需要注意,类加载过程中也会有对变量赋默认值的操作,但二者是不同的,类加载过程中的是为类的静态变量赋默认值,而这里是对对象的属性进行赋默认值。
Mark Word(标记字段)
Class Pointer(类指针)
Class
对象)。JVM 调用 构造方法
,执行初始化逻辑:
class Example {
int num;
Example() {
num = 10;
System.out.println("Constructor executed!");
}
}
public class Test {
public static void main(String[] args) {
Example obj = new Example();
}
}
num = 10
。这里需要注意,类加载过程中的赋值操作与这里不同,类加载过程中只是单纯为类的静态变量赋值,而这里是调用构造函数对对象的属性进行赋值。
Java 采用 堆 + 栈 的模式进行对象的内存管理。
存储位置 | 存储内容 |
---|---|
堆(Heap) | 对象本身(实例变量、数组) |
栈(Stack) | 对象引用(局部变量表) |
方法区(Method Area) | 类的元信息(方法、静态变量) |
方法区:
A 类的静态变量 b = 20
A 类的方法信息
栈:
obj1 -> 指向堆中的 A 对象
obj2 -> 指向另一个 A 对象
堆:
obj1 的实例变量 a = 10
obj2 的实例变量 a = 10
class Person {
static String species = "Human"; // 静态变量(方法区)
String name; // 实例变量(堆)
int age; // 实例变量(堆)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sayHello() {
System.out.println("Hello, my name is " + name);
}
}
public class MemoryDemo {
public static void main(String[] args) {
Person p1 = new Person("Alice", 25); // 在堆中创建对象
Person p2 = new Person("Bob", 30); // 在堆中创建另一个对象
p1.sayHello();
p2.sayHello();
}
}
直观的内存示意图
方法区(存储类信息 + 静态变量)
-------------------------------------------------
| 类名:Person
| 静态变量:species = "Human"
| 方法:sayHello()
-------------------------------------------------
栈(存储局部变量/对象引用)
---------------------------------
| main() 方法的栈帧
| p1 -> 指向 堆中的对象 1
| p2 -> 指向 堆中的对象 2
---------------------------------
堆(存储对象实例)
-------------------------------------
| Person 对象 1 (p1)
| name = "Alice"
| age = 25
-------------------------------------
| Person 对象 2 (p2)
| name = "Bob"
| age = 30
-------------------------------------
JVM 根据对象的生命周期和大小,决定其分配的位置:
新生代(Young Generation)
老年代(Old Generation)
栈上分配(逃逸分析)
-XX:+DoEscapeAnalysis
Java 采用自动内存管理
malloc()
/ free()
),容易导致 内存泄漏(Memory Leak) 或 悬空指针(Dangling Pointer)。解决对象生命周期管理问题
static
变量)基本原理
可达性分析法是基于图遍历(Graph Traversal)的方式进行垃圾对象检测:
什么是 GC Roots(垃圾回收根对象) / GC Roots 的来源
在可达性分析中,JVM 会选择一组特殊的对象作为根对象(GC Roots),从这些根开始查找所有可达对象。
GC Roots 类型 | 存储位置 | 示例 |
---|---|---|
栈帧中的局部变量 | 栈(Stack) | 方法内的局部变量 Object obj = new Object(); |
静态变量(Static) | 方法区(Metaspace) | static Object obj = new Object(); |
常量池中的引用 | 方法区(Metaspace) | String s = "Hello"; |
JNI(Native 方法)引用的对象 | 本地方法栈(Native Stack) | 通过 JNI 访问的 Java 对象 |
线程对象 | 线程管理区 | 运行中的线程对象 Thread.currentThread() |
JVM 采用不同的 GC 算法来优化垃圾回收,主要包括:
GC 算法 | 原理 | 优缺点 |
---|---|---|
标记-清除(Mark-Sweep) | 标记存活对象 → 清除未标记对象 | 产生内存碎片,影响分配效率 |
复制(Copying) | 复制存活对象到新区域,清空旧区域 | 内存利用率低(50% 内存浪费) |
标记-整理(Mark-Compact) | 标记存活对象 → 移动对象(向一端移动) → 回收未使用空间 | 解决碎片问题,性能较高 |
分代回收(Generational GC) | 新生代 采用复制算法,老年代 采用标记整理算法 | 适用于大规模应用 |
新生代(Young Generation)
老年代(Old Generation)
GC | 新生代算法 | 老年代算法 | 适用场景 |
---|---|---|---|
Serial GC | 复制 | 标记整理 | 单线程,适用于小型应用 |
Parallel GC | 复制 | 标记整理 | 多线程高吞吐量 |
CMS GC | 标记清除 | 标记清除 | 低延迟,适用于 Web 应用 |
G1 GC | Region 化管理 | Region 化管理 | 大内存应用,JDK 9+ 推荐 |
ZGC | 并发 | 并发 | 超低延迟,JDK 11+ |
STW(Stop-The-World)的概念
STW(Stop-The-World) 是指 JVM 在执行 GC 时,会暂停所有应用线程,以便垃圾回收器安全地回收对象。这意味着:
- 所有应用线程停止执行,等待 GC 完成后再继续运行。
- STW 发生时,Java 代码暂停执行,系统响应变慢,可能导致卡顿。
CMS(Concurrent Mark-Sweep)是 JDK 1.4 引入的 低延迟 GC,适用于Web 服务器、金融系统等低停顿时间应用。
✅ 最小化 STW(低延迟),适用于交互式应用。
✅ 并发执行 GC,不影响应用线程运行。
✅ "标记-清除" 算法,回收时不会整理堆内存(容易产生内存碎片)。
❌ 垃圾碎片问题严重,可能导致 Full GC(STW 变长)。
❌ CPU 资源开销大,GC 线程与应用线程竞争 CPU 资源。
1️⃣ CMS GC 的堆内存结构
2️⃣ CMS GC 的垃圾回收流程
CMS GC 的核心思想是:并发执行垃圾回收,尽可能减少 STW 时间。
这里的并发执行指的是和应用线程并发执行,不用暂停应用线程也能进行垃圾回收。
垃圾回收流程如下:
初始标记(Initial Mark,STW):标记 GC Roots 直接关联的对象,STW 时间短。
并发标记(Concurrent Marking):在应用程序运行的同时,遍历对象图,标记可达对象。
重新标记(Remark,STW):由于并发标记时,可能有对象状态发生变化,因此需要再次 STW,重新标记存活对象。
并发清除(Concurrent Sweep):在应用程序运行的同时,并发清除垃圾对象,释放内存。
Full GC(当 CMS GC 失败时触发,STW 时间长):由于 CMS 不进行内存整理(Compaction),可能导致碎片化问题。当大对象无法分配到连续空间时,触发 Full GC(可能造成严重 STW(通常几百毫秒到几秒))。
为什么 CMS GC 会产生垃圾碎片?
解决方案:
参数优化
-XX:+UseCMSCompactAtFullCollection
(在 Full GC 后进行整理)。-XX:CMSFullGCsBeforeCompaction=3
(每 3 次 Full GC 后执行一次内存整理)。改用 G1 GC
G1 GC(Garbage First GC)是 JDK 7u4 引入,并在 JDK 9 成为 默认 GC。
适用于大内存应用(4GB 以上),相比 CMS GC 减少了碎片化问题,提供更可预测的 GC 停顿时间。
✅ Region(分区化管理),动态调整新生代和老年代比例。
✅ 可预测的 GC 停顿时间(-XX:MaxGCPauseMillis
)。
✅ 并发执行回收,减少 STW 停顿时间。
✅ 自动整理内存(不会产生碎片化问题)。
❌ 相比 CMS,CPU 开销更高。
❌ 吞吐量略低于 Parallel GC。
1️⃣ G1 GC 的堆内存结构
2️⃣ G1 GC 的垃圾回收流程
年轻代 GC(Minor GC,STW):复制存活对象到 Survivor 或老年代,清空 Eden。
并发标记(Concurrent Marking,避免 STW):识别老年代中垃圾最多的 Region,准备回收。
混合回收(Mixed GC,减少 Full GC):同时回收年轻代和部分老年代,减少老年代空间不足问题。
Full GC(极少发生):只有当 G1 GC 失败时才会触发 Full GC。
JDK 9默认使用G1 GC
对比项 | CMS GC | G1 GC |
---|---|---|
适用场景 | 低延迟应用(Web 服务器) | 大内存应用(4GB+) |
回收策略 | 标记-清除,不整理内存 | Region 化管理,减少碎片 |
STW 时间 | 可能较长(Full GC) | 可预测(-XX:MaxGCPauseMillis ) |
碎片化问题 | 可能严重,影响 Full GC 频率 | 通过 Region 避免碎片 |
吞吐量 | 较高,但 Full GC 影响较大 | 较稳定,整体吞吐量较优 |
Full GC 触发 | 碎片化严重时容易触发 | 极少发生 |
堆(Heap)GC:
方法区(Method Area)GC:
方法区 GC 主要回收哪些内容?
(1)废弃的常量
(2)无用的类
类的卸载(Class Unloading) 发生在以下条件都满足时:
注意:JVM 默认不会主动卸载类,通常只有在动态加载和卸载 ClassLoader 时才会发生(如 Web 服务器动态部署)。
(3)JIT 编译缓存
JVM 的 JIT 编译器(Just-In-Time Compiler) 会将热点代码编译成本地机器码并缓存到 代码缓存(Code Cache)。当缓存空间不足时,JVM 可能会触发 GC 清除不常用的编译代码。
方法区 GC 触发时机
在 JVM 中,内存泄漏(Memory Leak) 指的是程序运行过程中,不再使用的对象仍然被引用,导致 GC 无法回收它们,进而导致堆内存(Heap)不断膨胀,最终可能触发 OutOfMemoryError(OOM)。
尽管 Java 有 垃圾回收机制(GC),但如果对象仍然被可达引用(Reachable),即使程序不再使用它们,GC 也不会回收这些对象。这就形成了内存泄漏。
1. 堆内存持续增长
2. 应用性能下降
3. OutOfMemoryError: Java heap space
Java 内存泄漏的根本原因是 无用对象仍然被引用,GC 无法回收它们。常见的几种情况如下:
原因
示例
import java.util.*;
public class StaticCollectionLeak {
private static final List memoryLeakList = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
byte[] largeObject = new byte[1024 * 1024]; // 1MB
memoryLeakList.add(largeObject);
}
}
}
问题:memoryLeakList
是 static
,导致所有对象即使不再需要,仍然不会被 GC 回收。
解决方案
WeakReference
或 SoftReference
原因
示例
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
}
interface EventListener {
void onEvent();
}
public class ListenerLeak {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
EventListener listener = () -> System.out.println("Event received!");
eventSource.addListener(listener);
}
}
问题:
listeners
集合会一直持有 EventListener
对象的引用,即使它们不再被使用,导致 GC 不能回收它们。解决方案:
WeakReference
弱引用:private final List> listeners = new ArrayList<>();
原因
ThreadLocal
绑定的变量存储在线程的 ThreadLocalMap
中,但如果不手动清理,线程池复用线程时可能会导致数据泄漏。示例
public class ThreadLocalLeak {
private static final ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
threadLocal.set(new byte[10 * 1024 * 1024]); // 10MB
});
thread.start();
}
}
问题:线程执行完后,ThreadLocal
变量没有被清理,导致占用 10MB 内存无法释放。
解决方案:
finally
语句中手动清理 ThreadLocal
变量:try {
threadLocal.set(new byte[10 * 1024 * 1024]);
} finally {
threadLocal.remove();
}
原因
示例
public class InnerClassLeak {
private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB
public void createAnonymousClass() {
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(largeArray.length);
}
};
new Thread(task).start();
}
public static void main(String[] args) {
new InnerClassLeak().createAnonymousClass();
}
}
问题:
task
作为匿名内部类会持有 InnerClassLeak
的引用?
在 Java 中,非静态内部类和匿名类会隐式地持有外部类实例的引用,这就是 隐式引用(Implicit Reference)。这意味着:
task
的 run()
方法内部,访问了 largeArray
(InnerClassLeak
的成员变量)。largeArray
是 InnerClassLeak
的实例变量,所以 匿名类 task
需要持有 InnerClassLeak
的引用,才能访问 largeArray
。InnerClassLeak
对象无法被 GC 回收,从而导致内存泄漏。解决方案:
public class InnerClassLeak {
private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB
public void createStaticInnerClass() {
new Thread(new StaticTask(largeArray)).start(); // 直接传递 largeArray
}
// 变为静态内部类
private static class StaticTask implements Runnable {
private final byte[] arrayRef;
StaticTask(byte[] arrayRef) {
this.arrayRef = arrayRef;
}
@Override
public void run() {
System.out.println(arrayRef.length); // 访问传入的参数,而不是外部类变量
}
}
public static void main(String[] args) {
new InnerClassLeak().createStaticInnerClass();
}
}
为什么静态内部类可以避免内存泄漏?
静态内部类不会持有外部类 InnerClassLeak
的隐式引用:
StaticTask
使用 static
修饰后,就不再与 InnerClassLeak
绑定,它变成了独立的类。StaticTask
不会自动持有 InnerClassLeak
的实例引用。显式传递 largeArray
,避免隐式引用
new Thread(new StaticTask(largeArray)).start();
largeArray
传递给 StaticTask
构造方法,这样 StaticTask
只持有 largeArray
的引用,而不是 InnerClassLeak
的整个实例。InnerClassLeak
被 GC 回收,StaticTask
仍然可以正常运行。
方案 是否会导致内存泄漏? 原因 匿名内部类 ✅ 可能会泄漏 持有外部类 InnerClassLeak
的隐式引用,导致largeArray
无法回收静态内部类 ❌ 不会泄漏 不再持有 InnerClassLeak
的引用,只持有largeArray
,可以安全回收最佳实践
- 避免匿名类访问外部类的实例变量,否则可能会无意间创建隐式引用,导致对象不能被 GC。
- 如果必须使用内部类,建议使用
static
内部类,并通过构造方法传递所需数据,避免隐式引用外部类。
原因
示例
public class ConnectionLeak {
public static void main(String[] args) throws Exception {
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
// 资源未关闭,泄漏
}
}
解决方案
try-with-resources
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
while (rs.next()) {
System.out.println(rs.getString("name"));
}
}
try-with-resources
确保所有资源自动关闭!(1) 使用 jmap
查看堆内存
jmap -histo:live
(2)使用 jconsole
监控 JVM 内存
(3)使用 VisualVM
进行 Heap Dump 分析
jmap -dump:format=b,file=heapdump.hprof
VisualVM
,分析对象引用关系,找出无法被 GC 回收的对象。(4)使用 MAT(Memory Analyzer Tool)
.hprof
文件,找出GC Root 保持的对象,定位泄漏点。JVM 内存溢出(OutOfMemoryError,简称 OOM) 是指 JVM 试图分配内存,但由于内存不足或内存无法回收,导致 JVM 运行失败并抛出 java.lang.OutOfMemoryError
异常。
JVM 主要的内存区域:
JVM 内存溢出 通常发生在以下几种区域:
OOM 错误类型 | 发生区域 | 主要原因 |
---|---|---|
java.lang.OutOfMemoryError: Java heap space |
堆(Heap) | - 对象过多,无法回收,导致堆空间耗尽(如集合无限增长,缓存未清理)。 - 单个大对象分配失败(如一次性分配超大数组)。 |
java.lang.OutOfMemoryError: GC overhead limit exceeded |
堆(Heap) | - GC 频繁运行但每次回收内存极少,导致 CPU 资源被大量消耗。 |
java.lang.OutOfMemoryError: Metaspace |
方法区(Metaspace) | - 类加载过多(如 Spring 频繁创建代理类,动态类加载)。 - 类无法卸载(如自定义 ClassLoader 造成内存泄漏)。 |
java.lang.StackOverflowError |
栈(Stack) | - 方法递归过深,导致栈帧溢出(如无限递归)。 - 每个线程栈空间不足,导致溢出。 |
java.lang.OutOfMemoryError: unable to create new native thread |
本地内存(OS 线程数) | - 线程创建过多,超出 OS 允许的最大线程数(如无限创建 new Thread() )。- 每个线程栈大小过大,导致系统无法分配新线程。 |
java.lang.OutOfMemoryError: Direct buffer memory |
直接内存(Direct Memory) | - NIO 直接缓冲区 (ByteBuffer.allocateDirect() ) 分配过多,超过 MaxDirectMemorySize 限制。 |
java.lang.OutOfMemoryError: Swap space |
操作系统 Swap 交换空间 | - 应用分配内存过多,导致 OS 交换空间耗尽(一般在物理内存不足时发生)。 |
java.lang.OutOfMemoryError: Requested array size exceeds VM limit |
堆(Heap) | - 试图分配超大数组(如 new int[Integer.MAX_VALUE] )。 |
java.lang.OutOfMemoryError: Compressed class space |
方法区(Metaspace, Class Space) | - JVM 运行时加载类过多,超出 CompressedClassSpaceSize 限制。 |
原因
示例
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
public static void main(String[] args) {
List list = new ArrayList<>();
while (true) {
list.add(new byte[10 * 1024 * 1024]); // 每次分配 10MB
}
}
}
解决方案
✅ 增大堆空间(适用于对象确实需要更多内存的情况):
java -Xms2g -Xmx4g HeapOOM
✅ 优化 GC 策略
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 HeapOOM
✅ 检测内存泄漏
jmap
jmap -histo:live
jmap -dump:format=b,file=heapdump.hprof
VisualVM
或 MAT
(Memory Analyzer Tool)分析 heapdump.hprof
原因
示例 1:递归调用导致 StackOverflowError
public class StackOverflowDemo {
public void recursiveMethod() {
recursiveMethod(); // 无限递归
}
public static void main(String[] args) {
new StackOverflowDemo().recursiveMethod();
}
}
示例 2:创建大量线程导致 OutOfMemoryError: Unable to create new native thread
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadOOM {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1000);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
while (true) {} // 每个线程执行无限循环
});
}
}
}
解决方案
✅ 减少递归深度 ✅ 增大栈空间
java -Xss1m StackOverflowDemo
✅ 控制线程池大小
ExecutorService executor = Executors.newFixedThreadPool(100);
原因
CGLIB
、Javassist
动态代理)。Metaspace
过满。Metaspace
持续增长。示例
import javassist.ClassPool;
public class MetaspaceOOM {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
classPool.makeClass("com.example.GeneratedClass" + i).toClass();
}
}
}
解决方案
✅ 增加 Metaspace
大小
java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m MetaspaceOOM
✅ 减少动态生成的类
原因
示例
import java.util.HashMap;
import java.util.Map;
public class GCOverheadOOM {
public static void main(String[] args) {
Map map = new HashMap<>();
int i = 0;
while (true) {
map.put(i, "OOM Test " + i++); // 不断填充 HashMap
}
}
}
解决方案
✅ 增大堆空间,减少 GC 触发
java -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 GCOverheadOOM
✅ 使用 -XX:-UseGCOverheadLimit
关闭 GC 限制
java -Xmx4g -XX:-UseGCOverheadLimit GCOverheadOOM
原因
NIO ByteBuffer
分配过多,导致 Direct Memory 耗尽。ByteBuffer.allocateDirect()
分配请求。示例
import java.nio.ByteBuffer;
public class DirectMemoryOOM {
public static void main(String[] args) {
while (true) {
ByteBuffer.allocateDirect(1024 * 1024); // 每次申请 1MB 直接内存
}
}
}
解决方案
✅ 增大直接内存
java -XX:MaxDirectMemorySize=512m DirectMemoryOOM
✅ 避免无限制分配
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
buffer.clear(); // 复用 Buffer,避免反复分配
在 JVM 运行 Java 应用时,我们可以使用 JVM 参数 来控制内存分配、垃圾回收(GC)策略、性能优化等。本文将详细介绍 JVM 常见参数的分类、作用、以及如何设置这些参数。
在生产环境中,JVM 参数通常通过以下方式进行设置:
(1)直接通过 java
命令行设置
适用于 独立 Java 应用、测试环境,例如:
java -Xms2g -Xmx4g -XX:+UseG1GC -jar myapp.jar
(2)在 JAVA_OPTS
或 JAVA_TOOL_OPTIONS
环境变量中设置
适用于 Web 服务器(Tomcat、Spring Boot、微服务):
export JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC"
(3)在 Docker 容器中设置
容器化部署时,一般通过环境变量 JAVA_OPTS
传递:
docker run -e "JAVA_OPTS=-Xmx4g -XX:+UseG1GC" my-java-app
(4)在 Kubernetes(K8s)中设置
对于 K8s 部署的 Java 应用,可以在 Deployment
配置文件中设置:
env:
- name: JAVA_OPTS
value: "-Xms2g -Xmx4g -XX:+UseG1GC"
作用:控制 JVM 的 堆(Heap)、栈(Stack)、方法区(Metaspace) 大小,影响 GC 频率和性能。
参数 | 作用 | 生产环境建议 |
---|---|---|
-Xms |
初始堆大小(默认 1/64 物理内存) | 设置与 -Xmx 相同,避免运行时扩展 |
-Xmx |
最大堆大小(默认 1/4 物理内存) | 根据可用内存大小设置,如 -Xmx4g |
-XX:NewRatio=n |
新生代:老年代 比例(默认 2 ,即 1:2 ) |
推荐 NewRatio=2 ,适用于吞吐量型应用 |
-XX:SurvivorRatio=n |
Eden:Survivor 比例(默认 8:1:1 ) |
保持默认 SurvivorRatio=8 |
-Xss |
每个线程的栈大小(默认 1MB) | 适用于高并发应用,如 -Xss512k 减少栈内存占用 |
-XX:MetaspaceSize=256m |
JDK 8+ 方法区大小 | 推荐 256m |
-XX:MaxMetaspaceSize=512m |
元空间最大值 | 防止 Metaspace OOM ,推荐 512m |
作用:选择合适的 GC 机制,降低 STW(Stop-The-World)
停顿时间,提高吞吐量。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:+UseSerialGC |
单线程 GC(适用于小型应用) | 不推荐用于生产环境 |
-XX:+UseParallelGC |
多线程吞吐量 GC | 适用于批处理任务、Kafka、Spark |
-XX:+UseG1GC |
低延迟 GC(默认) | 适用于 Web 服务器 / 微服务 |
-XX:+UseZGC |
超低延迟 GC(JDK 11+) | 适用于金融、超大堆(TB 级)应用 |
-XX:MaxGCPauseMillis=200 |
最大 GC 停顿时间 | 适用于 G1 GC,控制 STW 时长 |
作用:优化 JIT 编译,提高 Java 代码执行性能。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:+TieredCompilation |
分层 JIT 编译 | 默认启用,适用于高并发应用 |
-XX:+PrintCompilation |
打印 JIT 编译方法 | 调试时启用 |
作用:控制并发线程数,提高 CPU 资源利用率。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:ParallelGCThreads= |
GC 并行线程数 | 推荐 CPU 核心数 / 2 |
-XX:ConcGCThreads= |
G1 / ZGC 并发线程数 | 适用于低延迟应用 |
作用:启用 GC 日志,监控应用运行状态。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:+HeapDumpOnOutOfMemoryError |
OOM 生成 Heap Dump | 强烈建议启用 |
-XX:HeapDumpPath= |
Heap Dump 存储路径 | 推荐 /var/logs/heapdump.hprof |
-XX:+PrintGCDetails |
打印 GC 详情 | 生产环境推荐 |
-Xloggc:/var/logs/gc.log |
GC 日志存储路径 | 用于 GC 监控分析 |