JVM内存区域详解
一、知识概述
Java虚拟机(JVM)在执行Java程序时,会将其管理的内存划分为若干个不同的数据区域。这些区域有各自的用途、创建时间和销毁时间。理解JVM内存区域是掌握Java性能优化、排查内存问题的基础。
JVM内存区域划分
根据《Java虚拟机规范》,JVM管理的内存分为以下几个区域:
scss
┌─────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 程序计数器 │ │ Java虚拟机栈 │ │ 本地方法栈 │ │
│ │ (线程私有) │ │ (线程私有) │ │ (线程私有) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Java堆 (Heap) │ │
│ │ (线程共享,GC主要区域) │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 新生代 │ │ 老年代 │ │ │
│ │ │ Eden/S0/S1 │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 方法区 (Method Area) │ │
│ │ (线程共享,存储类信息) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 运行时常量池 │ │
│ │ (方法区的一部分) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 直接内存 (Direct Memory) │ │
│ │ (NIO使用,非JVM规范定义) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
线程私有 vs 线程共享
| 区域 | 类型 | 生命周期 | 存储内容 |
|---|---|---|---|
| 程序计数器 | 线程私有 | 线程生命周期 | 当前执行的字节码行号 |
| Java虚拟机栈 | 线程私有 | 线程生命周期 | 栈帧(局部变量表、操作数栈等) |
| 本地方法栈 | 线程私有 | 线程生命周期 | Native方法调用信息 |
| Java堆 | 线程共享 | JVM生命周期 | 对象实例、数组 |
| 方法区 | 线程共享 | JVM生命周期 | 类信息、常量、静态变量 |
二、知识点详细讲解
2.1 程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
特点
- 线程私有:每个线程都有独立的程序计数器
- 唯一不会OOM的区域:如果执行的是Java方法,计数器记录的是字节码指令地址;如果执行的是Native方法,计数器值为空
- 生命周期:随线程创建而创建,随线程结束而销毁
工作原理
java
/**
* 程序计数器工作原理演示
*/
public class ProgramCounterDemo {
public static void main(String[] args) {
int a = 1; // PC: 0
int b = 2; // PC: 1
int c = add(a, b); // PC: 2 -> 跳转到add方法
System.out.println(c); // PC: 3
}
public static int add(int x, int y) {
return x + y; // PC: add方法中的指令地址
}
}
/*
字节码视角:
public static void main(java.lang.String[]);
Code:
0: iconst_1 // PC = 0
1: istore_1 // PC = 1
2: iconst_2 // PC = 2
3: istore_2 // PC = 3
4: iload_1 // PC = 4
5: iload_2 // PC = 5
6: invokestatic #2 // PC = 6, 调用add方法
9: istore_3 // PC = 9, 从方法返回后继续
10: getstatic #3 // PC = 10
...
*/
多线程执行
less
线程A 线程B
┌─────────────┐ ┌─────────────┐
│ PC: 0x1000 │ │ PC: 0x2000 │
│ 执行方法A │ │ 执行方法B │
└─────────────┘ └─────────────┘
↓ ↓
时间片切换 时间片切换
↓ ↓
┌─────────────┐ ┌─────────────┐
│ PC: 0x1004 │ │ PC: 0x2008 │
│ 恢复执行 │ │ 恢复执行 │
└─────────────┘ └─────────────┘
每个线程有独立的PC,切换后能恢复到正确的执行位置
2.2 Java虚拟机栈(Java Virtual Machine Stack)
Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
栈帧结构
scss
┌─────────────────────────────────────┐
│ 栈帧 (Stack Frame) │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ 局部变量表 (Local Variables) │
│ │ [0] this │ │
│ │ [1] arg1 │ │
│ │ [2] arg2 │ │
│ │ [3] local_var1 │ │
│ │ [4] local_var2 │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ 操作数栈 (Operand Stack) │
│ │ ┌───┐ │ │
│ │ │ 5 │ <- 栈顶 │ │
│ │ ├───┤ │ │
│ │ │ 3 │ │ │
│ │ └───┘ │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ 动态连接 (Dynamic Linking) │
│ │ -> 指向运行时常量池的方法引用 │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ 方法返回地址 (Return Address) │
│ │ -> 方法正常退出或异常退出的地址 │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ 附加信息 (附加信息) │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘
局部变量表
java
/**
* 局部变量表示例
*/
public class LocalVariableTableDemo {
// 局部变量表分析
public static int calculate(int a, int b) {
// 局部变量表:
// Slot[0] = a (参数)
// Slot[1] = b (参数)
int c = a + b; // Slot[2] = c (局部变量)
int d = c * 2; // Slot[3] = d (局部变量)
{
int e = d + 1; // Slot[4] = e (作用域内有效)
// e 在此作用域结束后,Slot[4] 可被复用
}
int f = c - 1; // Slot[4] = f (复用之前的Slot)
return f;
}
// 实例方法的局部变量表
public void instanceMethod(String name, int age) {
// 局部变量表:
// Slot[0] = this (实例方法隐含参数)
// Slot[1] = name
// Slot[2] = age
System.out.println(name + ": " + age);
}
// 长整型和双精度浮点占用两个Slot
public void doubleSlot(long id, double value) {
// 局部变量表:
// Slot[0] = this
// Slot[1-2] = id (long占用两个Slot)
// Slot[3-4] = value (double占用两个Slot)
}
}
操作数栈
java
/**
* 操作数栈工作原理
* 计算: (1 + 2) * 3
*/
public class OperandStackDemo {
public static int calculate() {
/*
字节码执行过程:
0: iconst_1 // 将常量1压入操作数栈
操作数栈: [1]
1: iconst_2 // 将常量2压入操作数栈
操作数栈: [1, 2]
2: iadd // 弹出两个值相加,结果压栈
操作数栈: [3]
3: iconst_3 // 将常量3压入操作数栈
操作数栈: [3, 3]
4: imul // 弹出两个值相乘,结果压栈
操作数栈: [9]
5: ireturn // 返回栈顶值
*/
return (1 + 2) * 3;
}
public static void main(String[] args) {
int result = calculate();
System.out.println(result); // 输出: 9
}
}
栈溢出错误
java
/**
* 栈溢出演示
*/
public class StackOverflowDemo {
private static int count = 0;
/**
* 无限递归导致StackOverflowError
*/
public static void recursiveCall() {
count++;
recursiveCall(); // 无限递归
}
/**
* 通过减少局部变量延迟溢出
*/
public static void smallFrame() {
count++;
smallFrame(); // 局部变量少,栈帧小
}
/**
* 大量局部变量加速溢出
*/
public static void largeFrame() {
// 大量局部变量增加栈帧大小
long a1, a2, a3, a4, a5, a6, a7, a8, a9, a10;
long b1, b2, b3, b4, b5, b6, b7, b8, b9, b10;
long c1, c2, c3, c4, c5, c6, c7, c8, c9, c10;
long d1, d2, d3, d4, d5, d6, d7, d8, d9, d10;
count++;
largeFrame();
}
public static void main(String[] args) {
try {
recursiveCall();
} catch (StackOverflowError e) {
System.out.println("StackOverflowError at depth: " + count);
System.out.println("栈深度取决于:");
System.out.println(" 1. -Xss 参数设置(默认1MB)");
System.out.println(" 2. 每个栈帧的大小");
System.out.println(" 3. 栈帧中局部变量的数量");
}
// 演示不同栈帧大小的影响
count = 0;
try {
largeFrame();
} catch (StackOverflowError e) {
System.out.println("\nLargeFrame overflow at: " + count);
}
}
}
/*
运行示例:
java -Xss256k StackOverflowDemo # 小栈,快速溢出
java -Xss1m StackOverflowDemo # 默认大小
java -Xss2m StackOverflowDemo # 大栈,深度更大
*/
2.3 本地方法栈(Native Method Stack)
本地方法栈与Java虚拟机栈类似,区别在于Java虚拟机栈为Java方法服务,而本地方法栈为Native方法服务。
java
/**
* 本地方法栈示例
*/
public class NativeMethodStackDemo {
public static void main(String[] args) {
// Object.hashCode() 是native方法
Object obj = new Object();
int hash = obj.hashCode(); // 调用本地方法
// System.currentTimeMillis() 是native方法
long time = System.currentTimeMillis(); // 调用本地方法
// Thread.start() 最终调用native方法启动线程
Thread thread = new Thread(() -> {
System.out.println("Thread running");
});
thread.start(); // 内部调用native start0()
// Class.forName() 内部使用native方法
try {
Class<?> clazz = Class.forName("java.lang.String");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// Unsafe类大量使用native方法
// sun.misc.Unsafe 提供直接内存访问能力
}
}
/*
本地方法示例:
// Object类
public native int hashCode();
protected native Object clone() throws CloneNotSupportedException;
// System类
public static native long currentTimeMillis();
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos, int length);
// Thread类
private native void start0();
private native boolean isInterrupted(boolean clearInterrupted);
// Class类
private native String getName0();
*/
2.4 Java堆(Java Heap)
Java堆是Java虚拟机管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
堆内存结构
scss
┌───────────────────────────────────────────────────────────────┐
│ Java Heap │
├───────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ ┌─────────────────────┐ │
│ │ 新生代 (Young Gen) │ │ 老年代 (Old Gen) │ │
│ │ (约占堆的1/3) │ │ (约占堆的2/3) │ │
│ │ ┌───────────┬──────┬──────┐ │ │ │ │
│ │ │ Eden │ S0 │ S1 │ │ │ │ │
│ │ │ (8/10) │(1/10)│(1/10)│ │ │ │ │
│ │ └───────────┴──────┴──────┘ │ │ │ │
│ │ │ │ │ │
│ │ 对象先在Eden分配 │ │ 长期存活的对象 │ │
│ │ GC后存活对象进入Survivor │ │ 从Survivor晋升 │ │
│ │ 达到年龄阈值后晋升老年代 │ │ │ │
│ └─────────────────────────────────┘ └─────────────────────┘ │
│ │
│ 新生代GC (Minor GC) 老年代GC (Major/Full GC) │
│ 频繁、速度快 较少、速度慢 │
│ │
└───────────────────────────────────────────────────────────────┘
相关参数:
-Xms: 堆初始大小
-Xmx: 堆最大大小
-Xmn: 新生代大小
-XX:NewRatio: 新生代与老年代比例
-XX:SurvivorRatio: Eden与Survivor比例
-XX:MaxTenuringThreshold: 晋升老年代年龄阈值
对象分配过程
java
/**
* 对象在堆中的分配过程
*/
public class HeapAllocationDemo {
public static void main(String[] args) {
/*
对象分配流程:
1. 新对象先尝试在Eden区分配
┌─────────────────────────────────┐
│ Eden: [obj1][obj2][obj3][... ] │
└─────────────────────────────────┘
2. Eden区满了,触发Minor GC
存活对象被复制到Survivor区(假设S0)
┌─────────────────────────────────┐
│ Eden: [空] │
└─────────────────────────────────┘
┌──────┐
│ S0: │ [存活obj1][存活obj2]
└──────┘
3. 下次Minor GC,Eden和S0中存活对象复制到S1
┌─────────────────────────────────┐
│ Eden: [空] │
└─────────────────────────────────┘
┌──────┐ ┌──────┐
│ S0: │ [空] │ S1: │ [存活对象]
└──────┘ └──────┘
4. Survivor中对象年龄达到阈值,晋升老年代
┌──────┐
│ Old: │ [年龄足够的老对象]
└──────┘
5. 大对象直接进入老年代
-XX:PretenureSizeThreshold 设置阈值
*/
// 普通对象在Eden分配
for (int i = 0; i < 100; i++) {
byte[] data = new byte[1024]; // 小对象,Eden分配
}
// 大对象可能直接进入老年代
byte[] largeData = new byte[10 * 1024 * 1024]; // 10MB
// 长期存活对象最终进入老年代
List<byte[]> longLived = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
longLived.add(new byte[1024]);
}
}
}
堆内存溢出
java
import java.util.*;
/**
* 堆内存溢出演示
* java -Xms20m -Xmx20m HeapOOMDemo
*/
public class HeapOOMDemo {
static class OOMObject {
private byte[] data = new byte[1024 * 1024]; // 1MB
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
System.out.println("开始创建对象...");
System.out.println("堆大小: 20MB");
System.out.println("每个对象: 1MB + 对象头");
System.out.println();
try {
int count = 0;
while (true) {
list.add(new OOMObject());
count++;
if (count % 5 == 0) {
System.out.println("已创建 " + count + " 个对象");
}
}
} catch (OutOfMemoryError e) {
System.out.println("\nOutOfMemoryError: Java heap space");
System.out.println("总共创建了约 " + list.size() + " 个对象");
System.out.println("\n解决方法:");
System.out.println(" 1. 增大堆内存: -Xmx");
System.out.println(" 2. 检查是否存在内存泄漏");
System.out.println(" 3. 使用内存分析工具: jvisualvm, MAT");
}
}
}
/*
运行结果:
开始创建对象...
堆大小: 20MB
每个对象: 1MB + 对象头
已创建 5 个对象
已创建 10 个对象
已创建 15 个对象
OutOfMemoryError: Java heap space
总共创建了约 18 个对象
*/
堆内存分析
java
import java.lang.management.*;
import java.util.*;
/**
* 堆内存使用情况监控
*/
public class HeapMonitorDemo {
public static void main(String[] args) throws InterruptedException {
// 获取内存管理Bean
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
System.out.println("=== 堆内存配置 ===");
System.out.println("初始大小: " + toMB(heapUsage.getInit()) + " MB");
System.out.println("当前使用: " + toMB(heapUsage.getUsed()) + " MB");
System.out.println("已提交: " + toMB(heapUsage.getCommitted()) + " MB");
System.out.println("最大可用: " + toMB(heapUsage.getMax()) + " MB");
// 获取各个内存池信息
System.out.println("\n=== 内存池详情 ===");
List<MemoryPoolMXBean> memoryPools = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean pool : memoryPools) {
if (pool.getType() == MemoryType.HEAP) {
MemoryUsage usage = pool.getUsage();
System.out.println("\n" + pool.getName() + ":");
System.out.println(" 使用: " + toMB(usage.getUsed()) + " MB");
System.out.println(" 已提交: " + toMB(usage.getCommitted()) + " MB");
System.out.println(" 最大: " + toMB(usage.getMax()) + " MB");
}
}
// 模拟内存使用
System.out.println("\n=== 创建对象后 ===");
List<byte[]> data = new ArrayList<>();
for (int i = 0; i < 100; i++) {
data.add(new byte[1024 * 1024]); // 1MB
}
heapUsage = memoryMXBean.getHeapMemoryUsage();
System.out.println("当前使用: " + toMB(heapUsage.getUsed()) + " MB");
// GC前后的内存变化
System.out.println("\n=== 执行GC ===");
long beforeGC = memoryMXBean.getHeapMemoryUsage().getUsed();
System.out.println("GC前: " + toMB(beforeGC) + " MB");
System.gc();
Thread.sleep(100);
long afterGC = memoryMXBean.getHeapMemoryUsage().getUsed();
System.out.println("GC后: " + toMB(afterGC) + " MB");
System.out.println("回收: " + toMB(beforeGC - afterGC) + " MB");
}
private static String toMB(long bytes) {
return String.format("%.2f", bytes / 1024.0 / 1024.0);
}
}
2.5 方法区(Method Area)
方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区存储内容
markdown
┌─────────────────────────────────────────────────────────────┐
│ 方法区 (Method Area) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 类型信息 │ │
│ │ - 类名、访问修饰符 │ │
│ │ - 父类、实现的接口 │ │
│ │ - 字段信息(名称、类型、修饰符) │ │
│ │ - 方法信息(名称、返回类型、参数、字节码) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 运行时常量池 │ │
│ │ - 字面量(字符串、final常量) │ │
│ │ - 符号引用(类和接口的全限定名、字段和方法描述) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 静态变量 │ │
│ │ - static 修饰的类变量 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ JIT编译代码 │ │
│ │ - 即时编译器生成的本地代码 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
JDK版本差异:
JDK 7及之前: 永久代 (Permanent Generation)
- -XX:PermSize, -XX:MaxPermSize
- 固定大小,容易OOM
JDK 8及之后: 元空间 (Metaspace)
- -XX:MetaspaceSize, -XX:MaxMetaspaceSize
- 使用本地内存,默认无上限
运行时常量池
java
/**
* 运行时常量池示例
*/
public class RuntimeConstantPoolDemo {
public static void main(String[] args) {
// === 字符串常量池 ===
// 字面量创建,放入字符串常量池
String s1 = "Hello";
String s2 = "Hello";
System.out.println("s1 == s2: " + (s1 == s2)); // true
// new创建,堆中创建新对象
String s3 = new String("Hello");
System.out.println("s1 == s3: " + (s1 == s3)); // false
System.out.println("s1.intern() == s3.intern(): " +
(s1.intern() == s3.intern())); // true
// === 编译期常量 ===
System.out.println("\n编译期常量:");
System.out.println("MAX_VALUE = " + Integer.MAX_VALUE);
System.out.println("PI = " + Math.PI);
// === 符号引用 -> 直接引用 ===
System.out.println("\n符号引用解析:");
// 解析前:java.lang.Object 是符号引用
// 解析后:指向方法区中Object类的直接指针
Object obj = new Object();
// 方法调用的符号引用
// 编译时:只知道方法名和描述符
// 运行时:解析为方法的直接引用(入口地址)
String str = obj.toString();
System.out.println(str);
// === 常量池溢出 (JDK 7之前) ===
// JDK 7之后,字符串常量池移到堆中
System.out.println("\n常量池容量测试:");
testStringPool();
}
/**
* 测试字符串常量池
* JDK 7+: 池在堆中,受堆大小限制
* JDK 6: 池在永久代,受PermGen限制
*/
private static void testStringPool() {
Set<String> pool = new HashSet<>();
long start = System.currentTimeMillis();
try {
int i = 0;
while (true) {
// intern() 将字符串加入常量池
String str = "String" + i;
str.intern();
pool.add(str);
i++;
if (i % 100000 == 0) {
System.out.println("已添加 " + i + " 个字符串到常量池");
}
}
} catch (OutOfMemoryError e) {
long end = System.currentTimeMillis();
System.out.println("OOM after adding " + pool.size() + " strings");
System.out.println("Time: " + (end - start) + "ms");
}
}
/**
* Class常量池内容
*/
public static void showClassConstantPool() throws Exception {
// 使用javap查看常量池
// javap -v RuntimeConstantPoolDemo.class
/*
Constant pool:
#1 = Methodref #10.#29 // java/lang/Object."<init>":()V
#2 = String #30 // Hello
#3 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #33 // java/lang/StringBuilder
#5 = Methodref #34.#35 // ...StringBuilder.append:(Ljava/lang/String;)...
#6 = Methodref #34.#36 // ...StringBuilder.toString:()Ljava/lang/String;
...
*/
}
}
方法区溢出
java
import java.lang.reflect.*;
import java.util.*;
/**
* 方法区溢出演示
* JDK 8: java -XX:MaxMetaspaceSize=10m MetaspaceOOMDemo
* JDK 7: java -XX:MaxPermSize=10m PermGenOOMDemo
*/
public class MetaspaceOOMDemo {
public static void main(String[] args) {
System.out.println("=== 方法区/元空间溢出演示 ===");
System.out.println("限制: 10MB");
System.out.println();
// 使用CGLib动态生成大量类
Enhancer enhancer = new Enhancer();
try {
int count = 0;
while (true) {
// 每次创建一个新的类
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
Object obj = enhancer.create();
count++;
if (count % 100 == 0) {
System.out.println("已生成 " + count + " 个类");
}
}
} catch (OutOfMemoryError e) {
System.out.println("\nOutOfMemoryError: Metaspace");
System.out.println("\n原因:");
System.out.println(" 1. 加载太多类(CGLib、动态代理)");
System.out.println(" 2. 元空间/永久代设置过小");
System.out.println(" 3. 类加载器泄漏");
System.out.println("\n解决方法:");
System.out.println(" JDK 8+: 增大 -XX:MaxMetaspaceSize");
System.out.println(" JDK 7: 增大 -XX:MaxPermSize");
System.out.println(" 使用类加载器分析工具");
}
}
static class OOMObject {
public void method() {}
}
}
// 需要CGLib依赖
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
2.6 直接内存(Direct Memory)
直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常。
java
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
import java.util.*;
/**
* 直接内存示例
*/
public class DirectMemoryDemo {
public static void main(String[] args) throws Exception {
// === 1. 直接内存 vs 堆内存 ===
System.out.println("=== 直接内存 vs 堆内存 ===\n");
// 堆内存Buffer
ByteBuffer heapBuffer = ByteBuffer.allocate(1024 * 1024); // 1MB堆内
System.out.println("Heap Buffer:");
System.out.println(" isDirect: " + heapBuffer.isDirect());
System.out.println(" 在Java堆中分配");
System.out.println(" IO时需要复制到本地内存");
// 直接内存Buffer
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB堆外
System.out.println("\nDirect Buffer:");
System.out.println(" isDirect: " + directBuffer.isDirect());
System.out.println(" 在本地内存中分配");
System.out.println(" IO时直接使用,零拷贝");
// === 2. 性能对比 ===
System.out.println("\n=== IO性能对比 ===\n");
// 准备测试文件
File tempFile = File.createTempFile("test", ".dat");
tempFile.deleteOnExit();
writeTestData(tempFile, 100 * 1024 * 1024); // 100MB
// 堆内存读写
long heapTime = testHeapIO(tempFile);
System.out.println("Heap Buffer IO: " + heapTime + "ms");
// 直接内存读写
long directTime = testDirectIO(tempFile);
System.out.println("Direct Buffer IO: " + directTime + "ms");
System.out.println("\n直接内存快 " +
String.format("%.1f", (double)heapTime / directTime) + " 倍");
// === 3. 直接内存监控 ===
System.out.println("\n=== 直接内存使用 ===\n");
// 查看直接内存限制
long maxDirect = sun.misc.VM.maxDirectMemory();
System.out.println("最大直接内存: " + toMB(maxDirect) + " MB");
// 分配直接内存
List<ByteBuffer> buffers = new ArrayList<>();
for (int i = 0; i < 10; i++) {
buffers.add(ByteBuffer.allocateDirect(10 * 1024 * 1024)); // 10MB each
}
// 查看已使用
long usedDirect = getUsedDirectMemory();
System.out.println("已使用直接内存: " + toMB(usedDirect) + " MB");
// === 4. 直接内存溢出 ===
System.out.println("\n=== 直接内存溢出测试 ===");
System.out.println("限制: -XX:MaxDirectMemorySize=50m");
System.out.println();
try {
// 分配超过限制的直接内存
ByteBuffer.allocateDirect(100 * 1024 * 1024); // 100MB
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError: Direct buffer memory");
System.out.println("\n原因:");
System.out.println(" 直接内存超过 -XX:MaxDirectMemorySize 限制");
System.out.println("\n解决方法:");
System.out.println(" 1. 增大 -XX:MaxDirectMemorySize");
System.out.println(" 2. 及时释放DirectBuffer");
System.out.println(" 3. 使用 Cleaner 清理");
}
// 清理临时文件
tempFile.delete();
}
private static void writeTestData(File file, long size) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
raf.setLength(size);
}
}
private static long testHeapIO(File file) throws IOException {
long start = System.currentTimeMillis();
try (FileChannel channel = new FileInputStream(file).getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
while (channel.read(buffer) != -1) {
buffer.clear();
}
}
return System.currentTimeMillis() - start;
}
private static long testDirectIO(File file) throws IOException {
long start = System.currentTimeMillis();
try (FileChannel channel = new FileInputStream(file).getChannel()) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
while (channel.read(buffer) != -1) {
buffer.clear();
}
}
return System.currentTimeMillis() - start;
}
private static long getUsedDirectMemory() {
try {
Class<?> bitsClass = Class.forName("java.nio.Bits");
Method method = bitsClass.getDeclaredMethod("reservedMemory");
method.setAccessible(true);
return (Long) method.invoke(null);
} catch (Exception e) {
return -1;
}
}
private static String toMB(long bytes) {
return String.format("%.2f", bytes / 1024.0 / 1024.0);
}
}
/*
运行示例:
java -XX:MaxDirectMemorySize=100m DirectMemoryDemo
输出:
=== 直接内存 vs 堆内存 ===
Heap Buffer:
isDirect: false
在Java堆中分配
IO时需要复制到本地内存
Direct Buffer:
isDirect: true
在本地内存中分配
IO时直接使用,零拷贝
=== IO性能对比 ===
Heap Buffer IO: 1523ms
Direct Buffer IO: 421ms
直接内存快 3.6 倍
最大直接内存: 100.00 MB
已使用直接内存: 100.16 MB
*/
三、可运行Java代码示例
完整示例:JVM内存监控工具
java
import java.lang.management.*;
import java.util.*;
import java.util.concurrent.*;
/**
* JVM内存监控工具
* 综合监控各个内存区域的使用情况
*/
public class JVMMemoryMonitor {
private final MemoryMXBean memoryMXBean;
private final List<MemoryPoolMXBean> memoryPools;
private volatile boolean running = true;
public JVMMemoryMonitor() {
this.memoryMXBean = ManagementFactory.getMemoryMXBean();
this.memoryPools = ManagementFactory.getMemoryPoolMXBeans();
}
/**
* 打印内存概览
*/
public void printMemoryOverview() {
System.out.println("╔════════════════════════════════════════════════════════════╗");
System.out.println("║ JVM 内存概览 ║");
System.out.println("╚════════════════════════════════════════════════════════════╝");
// 堆内存
MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
System.out.println("\n【堆内存 Heap】");
printMemoryUsage(heapUsage);
// 非堆内存(方法区等)
MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage();
System.out.println("\n【非堆内存 Non-Heap】");
printMemoryUsage(nonHeapUsage);
// 各内存池详情
System.out.println("\n【内存池详情】");
for (MemoryPoolMXBean pool : memoryPools) {
System.out.println("\n " + pool.getName());
System.out.println(" ├─ 类型: " + pool.getType());
System.out.println(" ├─ 内存管理器: " +
String.join(", ", pool.getMemoryManagerNames()));
printMemoryUsage(pool.getUsage(), " └─ ");
}
}
/**
* 打印内存使用情况
*/
private void printMemoryUsage(MemoryUsage usage) {
printMemoryUsage(usage, "");
}
private void printMemoryUsage(MemoryUsage usage, String prefix) {
long used = usage.getUsed();
long committed = usage.getCommitted();
long max = usage.getMax();
System.out.println(prefix + "已使用: " + formatBytes(used));
System.out.println(prefix + "已提交: " + formatBytes(committed));
System.out.println(prefix + "最大值: " +
(max == -1 ? "未定义" : formatBytes(max)));
if (max != -1) {
double usagePercent = (double) used / max * 100;
System.out.println(prefix + "使用率: " +
String.format("%.2f%%", usagePercent));
printUsageBar(usagePercent, prefix);
}
}
/**
* 打印使用率条形图
*/
private void printUsageBar(double percent, String prefix) {
int barLength = 40;
int filled = (int) (percent / 100 * barLength);
StringBuilder bar = new StringBuilder(prefix + "[");
for (int i = 0; i < barLength; i++) {
if (i < filled) {
bar.append("█");
} else {
bar.append("░");
}
}
bar.append("] ").append(String.format("%.1f%%", percent));
System.out.println(bar);
}
/**
* 格式化字节大小
*/
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
if (bytes < 1024 * 1024 * 1024)
return String.format("%.2f MB", bytes / 1024.0 / 1024.0);
return String.format("%.2f GB", bytes / 1024.0 / 1024.0 / 1024.0);
}
/**
* 实时监控内存
*/
public void startMonitoring(long intervalMs) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
if (!running) return;
MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
double heapUsagePercent = (double) heapUsage.getUsed() /
heapUsage.getMax() * 100;
System.out.printf("[%s] 堆内存: %.1f%% | 非堆: %s%n",
new java.text.SimpleDateFormat("HH:mm:ss").format(new Date()),
heapUsagePercent,
formatBytes(memoryMXBean.getNonHeapMemoryUsage().getUsed()));
// 警告
if (heapUsagePercent > 80) {
System.out.println(" ⚠️ 警告: 堆内存使用率超过80%!");
}
}, 0, intervalMs, TimeUnit.MILLISECONDS);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
running = false;
scheduler.shutdown();
}));
}
/**
* 模拟内存使用
*/
public void simulateMemoryUsage() {
System.out.println("\n╔════════════════════════════════════════════════════════════╗");
System.out.println("║ 内存使用模拟 ║");
System.out.println("╚════════════════════════════════════════════════════════════╝\n");
List<byte[]> memoryHog = new ArrayList<>();
Random random = new Random();
try {
for (int i = 0; i < 100; i++) {
// 随机分配不同大小的对象
int size = random.nextInt(1024 * 1024); // 0-1MB
memoryHog.add(new byte[size]);
if (i % 10 == 0) {
System.out.println("分配了 " + (i + 1) + " 个对象");
MemoryUsage usage = memoryMXBean.getHeapMemoryUsage();
System.out.println(" 堆内存使用: " + formatBytes(usage.getUsed()));
}
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (OutOfMemoryError e) {
System.out.println("\n❌ OutOfMemoryError: " + e.getMessage());
}
// 触发GC
System.out.println("\n触发GC...");
memoryHog.clear();
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
MemoryUsage usage = memoryMXBean.getHeapMemoryUsage();
System.out.println("GC后堆内存: " + formatBytes(usage.getUsed()));
}
/**
* 线程栈信息
*/
public void printThreadStackInfo() {
System.out.println("\n╔════════════════════════════════════════════════════════════╗");
System.out.println("║ 线程栈信息 ║");
System.out.println("╚════════════════════════════════════════════════════════════╝\n");
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
System.out.println("活动线程数: " + threadMXBean.getThreadCount());
System.out.println("峰值线程数: " + threadMXBean.getPeakThreadCount());
System.out.println("总启动线程数: " + threadMXBean.getTotalStartedThreadCount());
System.out.println("守护线程数: " + threadMXBean.getDaemonThreadCount());
// 线程详情
System.out.println("\n线程列表:");
for (ThreadInfo info : threadMXBean.dumpAllThreads(false, false)) {
System.out.println(" - " + info.getThreadName() +
" [ID: " + info.getThreadId() +
", State: " + info.getThreadState() + "]");
}
}
public static void main(String[] args) throws InterruptedException {
JVMMemoryMonitor monitor = new JVMMemoryMonitor();
// 打印内存概览
monitor.printMemoryOverview();
// 打印线程信息
monitor.printThreadStackInfo();
// 启动实时监控(每2秒)
System.out.println("\n开始实时监控(每2秒)...\n");
monitor.startMonitoring(2000);
// 模拟内存使用
monitor.simulateMemoryUsage();
// 持续运行一段时间
Thread.sleep(10000);
System.out.println("\n监控结束");
System.exit(0);
}
}
/*
运行示例:
java -Xms100m -Xmx200m -XX:MaxMetaspaceSize=50m JVMMemoryMonitor
*/
四、实战应用场景
场景1:内存泄漏分析
java
import java.util.*;
import java.lang.ref.*;
/**
* 内存泄漏场景与诊断
*/
public class MemoryLeakDemo {
// 场景1:静态集合导致的内存泄漏
static class StaticCollectionLeak {
private static final Map<String, Object> CACHE = new HashMap<>();
public void addToCache(String key, Object value) {
CACHE.put(key, value);
// 问题:只添加不删除,导致内存泄漏
}
}
// 场景2:未关闭的资源
static class ResourceLeak {
public void readData() throws IOException {
FileInputStream fis = new FileInputStream("data.txt");
// 问题:未关闭流,导致native内存泄漏
// 应该使用 try-with-resources
}
public void readDataCorrect() throws IOException {
try (FileInputStream fis = new FileInputStream("data.txt")) {
// 正确:自动关闭资源
}
}
}
// 场景3:监听器未注销
static class ListenerLeak {
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
// 问题:从未移除监听器
}
public void removeListener(EventListener listener) {
listeners.remove(listener);
// 解决:提供移除方法
}
}
// 场景4:ThreadLocal未清理
static class ThreadLocalLeak {
private static final ThreadLocal<byte[]> threadLocal =
new ThreadLocal<>();
public void processData() {
threadLocal.set(new byte[1024 * 1024]); // 1MB
// 问题:线程池场景下,线程复用导致数据残留
// 解决:finally中清理
}
public void processDataCorrect() {
try {
threadLocal.set(new byte[1024 * 1024]);
// 处理数据
} finally {
threadLocal.remove(); // 清理
}
}
}
// 使用弱引用避免泄漏
static class WeakReferenceCache {
private final Map<String, WeakReference<Object>> cache =
new HashMap<>();
public void put(String key, Object value) {
cache.put(key, new WeakReference<>(value));
}
public Object get(String key) {
WeakReference<Object> ref = cache.get(key);
return ref != null ? ref.get() : null;
}
}
// 内存泄漏诊断
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 内存泄漏诊断示例 ===\n");
StaticCollectionLeak leak = new StaticCollectionLeak();
// 记录初始内存
long initialMemory = Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory();
// 模拟内存泄漏
for (int i = 0; i < 10000; i++) {
leak.addToCache("key" + i, new byte[1024]);
}
// 触发GC
System.gc();
Thread.sleep(1000);
// 检查内存变化
long afterGC = Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory();
System.out.println("初始内存: " + (initialMemory / 1024 / 1024) + " MB");
System.out.println("GC后内存: " + (afterGC / 1024 / 1024) + " MB");
System.out.println("内存增长: " + ((afterGC - initialMemory) / 1024 / 1024) + " MB");
if (afterGC > initialMemory * 2) {
System.out.println("\n⚠️ 可能存在内存泄漏!");
System.out.println("诊断步骤:");
System.out.println(" 1. 使用 jmap -histo <pid> 查看对象分布");
System.out.println(" 2. 使用 jvisualvm 或 MAT 分析堆转储");
System.out.println(" 3. 查找大对象和GC Root路径");
}
}
}
场景2:OOM应急处理
java
import java.util.*;
import java.lang.management.*;
/**
* OOM应急处理工具
*/
public class OOMHandler {
/**
* 注册OOM处理器
* 注意:仅用于应急,不能保证一定能执行
*/
public static void registerOOMHandler() {
// 方法1:捕获OutOfMemoryError
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
if (throwable instanceof OutOfMemoryError) {
handleOOM((OutOfMemoryError) throwable);
}
});
// 方法2:使用MXBean监控
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
// 方法3:预分配应急内存
final byte[] emergencyBuffer = new byte[10 * 1024 * 1024]; // 10MB
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\n=== OOM 应急处理 ===");
System.out.println("正在生成诊断信息...");
// 释放应急内存
// emergencyBuffer = null;
// 生成诊断信息
generateDiagnosticReport();
}));
}
/**
* 处理OOM
*/
private static void handleOOM(OutOfMemoryError error) {
System.out.println("\n╔════════════════════════════════════════════════════════════╗");
System.out.println("║ OutOfMemoryError ║");
System.out.println("╚════════════════════════════════════════════════════════════╝");
String message = error.getMessage();
if (message.contains("Java heap space")) {
System.out.println("\n类型: 堆内存溢出");
System.out.println("原因:");
System.out.println(" 1. 对象过多,内存不足");
System.out.println(" 2. 存在内存泄漏");
System.out.println(" 3. 堆大小设置不合理");
System.out.println("\n解决方法:");
System.out.println(" - 增大堆内存: -Xmx");
System.out.println(" - 分析内存泄漏");
System.out.println(" - 优化对象使用");
} else if (message.contains("Metaspace")) {
System.out.println("\n类型: 元空间溢出");
System.out.println("原因:");
System.out.println(" 1. 加载类过多");
System.out.println(" 2. 动态代理、CGLib生成类过多");
System.out.println(" 3. 元空间设置过小");
System.out.println("\n解决方法:");
System.out.println(" - 增大元空间: -XX:MaxMetaspaceSize");
System.out.println(" - 检查类加载器泄漏");
System.out.println(" - 限制动态类生成");
} else if (message.contains("GC overhead limit exceeded")) {
System.out.println("\n类型: GC时间过长");
System.out.println("原因:");
System.out.println(" 应用花费了过多时间在GC上但回收很少");
System.out.println("\n解决方法:");
System.out.println(" - 增大堆内存");
System.out.println(" - 优化对象生命周期");
System.out.println(" - 禁用此限制: -XX:-UseGCOverheadLimit");
} else if (message.contains("Direct buffer memory")) {
System.out.println("\n类型: 直接内存溢出");
System.out.println("原因:");
System.out.println(" NIO直接内存使用过多");
System.out.println("\n解决方法:");
System.out.println(" - 增大限制: -XX:MaxDirectMemorySize");
System.out.println(" - 及时释放DirectBuffer");
}
// 生成诊断报告
generateDiagnosticReport();
}
/**
* 生成诊断报告
*/
private static void generateDiagnosticReport() {
System.out.println("\n╔════════════════════════════════════════════════════════════╗");
System.out.println("║ 诊断报告 ║");
System.out.println("╚════════════════════════════════════════════════════════════╝\n");
// JVM信息
Runtime runtime = Runtime.getRuntime();
System.out.println("【JVM内存】");
System.out.println(" 最大内存: " + toMB(runtime.maxMemory()) + " MB");
System.out.println(" 总内存: " + toMB(runtime.totalMemory()) + " MB");
System.out.println(" 空闲内存: " + toMB(runtime.freeMemory()) + " MB");
System.out.println(" 已用内存: " +
toMB(runtime.totalMemory() - runtime.freeMemory()) + " MB");
// 内存池
System.out.println("\n【内存池】");
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
MemoryUsage usage = pool.getUsage();
System.out.println(" " + pool.getName() + ": " +
toMB(usage.getUsed()) + " / " +
(usage.getMax() == -1 ? "无限制" : toMB(usage.getMax())) + " MB");
}
// 线程信息
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
System.out.println("\n【线程】");
System.out.println(" 活动线程: " + threadMXBean.getThreadCount());
System.out.println(" 峰值线程: " + threadMXBean.getPeakThreadCount());
// 建议
System.out.println("\n【排查建议】");
System.out.println(" 1. 生成堆转储: jmap -dump:format=b,file=heap.hprof <pid>");
System.out.println(" 2. 查看对象分布: jmap -histo <pid>");
System.out.println(" 3. 使用MAT分析: https://www.eclipse.org/mat/");
System.out.println(" 4. 检查GC日志: -Xlog:gc*:file=gc.log");
}
private static String toMB(long bytes) {
return String.format("%.2f", bytes / 1024.0 / 1024.0);
}
public static void main(String[] args) {
registerOOMHandler();
System.out.println("OOM处理器已注册");
System.out.println("模拟OOM将在3秒后开始...\n");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 模拟OOM
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]);
}
}
}
五、总结与最佳实践
核心要点回顾
| 区域 | 作用 | 线程 | 异常类型 |
|---|---|---|---|
| 程序计数器 | 字节码行号 | 私有 | 无 |
| 虚拟机栈 | 方法调用栈 | 私有 | StackOverflowError、OOM |
| 本地方法栈 | Native方法 | 私有 | StackOverflowError、OOM |
| Java堆 | 对象存储 | 共享 | OutOfMemoryError |
| 方法区 | 类信息、常量 | 共享 | OutOfMemoryError |
| 直接内存 | NIO缓冲 | 共享 | OutOfMemoryError |
最佳实践
-
合理设置内存参数
bash# 生产环境推荐 java -Xms4g -Xmx4g \ # 初始和最大堆相同,避免动态扩展 -Xmn2g \ # 新生代大小 -XX:MetaspaceSize=256m \ # 元空间初始大小 -XX:MaxMetaspaceSize=512m \ # 元空间最大大小 -XX:+UseG1GC \ # 使用G1垃圾收集器 -XX:MaxDirectMemorySize=1g \ # 直接内存限制 -Xlog:gc*:file=gc.log # GC日志 -jar app.jar -
避免内存泄漏
- 及时关闭资源(try-with-resources)
- 清理ThreadLocal
- 注销监听器
- 限制缓存大小
-
监控内存使用
- 定期检查内存使用率
- 设置内存告警阈值
- 保留GC日志用于分析
-
栈深度优化
- 避免过深的递归调用
- 可适当增大-Xss参数
- 考虑使用迭代替代递归
-
直接内存使用
- 大文件IO使用DirectBuffer
- 及时释放直接内存
- 监控直接内存使用量
常见问题排查
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| StackOverflowError | 递归太深 | 检查代码逻辑,增大-Xss |
| Java heap space OOM | 内存不足或泄漏 | jmap分析,MAT诊断 |
| Metaspace OOM | 类加载过多 | 检查动态代理,增大元空间 |
| Direct buffer OOM | NIO使用过多 | 增大MaxDirectMemorySize |
| GC overhead | 频繁GC效果差 | 增大堆内存,优化对象 |
扩展阅读
- 《Java虚拟机规范》:官方规范文档
- 《深入理解Java虚拟机》:周志明著
- JVM参数大全 :www.oracle.com/java/techno...
- MAT工具:Eclipse Memory Analyzer
- JVisualVM:JDK自带监控工具