内存溢出从基础到架构实战

1. 概述

1.1 内存溢出的重要性

内存溢出(OutOfMemoryError,OOM)是Java应用程序中最常见和严重的问题之一。理解内存溢出的原因、诊断方法和解决方案,对于开发高性能、稳定的Java应用至关重要。

内存溢出的影响

  • 应用崩溃:导致应用程序无法正常运行
  • 性能下降:频繁GC,响应时间变长
  • 用户体验差:服务不可用,影响业务
  • 数据丢失:可能导致数据不一致

1.2 内存溢出类型

Java内存溢出主要类型

类型 错误信息 发生区域 常见原因
堆溢出 java.lang.OutOfMemoryError: Java heap space 堆内存 对象过多、内存泄漏
栈溢出 java.lang.StackOverflowError 虚拟机栈 递归过深、局部变量过多
方法区溢出 java.lang.OutOfMemoryError: Metaspace 方法区(元空间) 类加载过多
直接内存溢出 java.lang.OutOfMemoryError: Direct buffer memory 直接内存 NIO使用不当
GC开销超限 java.lang.OutOfMemoryError: GC overhead limit exceeded 堆内存 GC效率低
无法创建线程 java.lang.OutOfMemoryError: unable to create new native thread 栈内存 线程过多

1.3 本文内容结构

本文将从以下几个方面全面解析内存溢出:

  1. JVM内存模型:内存区域划分和特点
  2. 堆内存溢出:原因、诊断、解决方案
  3. 栈内存溢出:原因、诊断、解决方案
  4. 方法区溢出:原因、诊断、解决方案
  5. 直接内存溢出:原因、诊断、解决方案
  6. 内存泄漏:常见内存泄漏场景和解决方案
  7. 诊断工具:jmap、jstat、MAT等工具使用
  8. 实战案例:真实的内存溢出案例和解决方案
  9. 架构设计:内存管理的最佳实践

2. JVM内存模型

2.1 JVM内存区域划分

2.1.1 内存区域概览

JVM内存区域(Java 8之前):

1
2
3
4
5
6
7
8
9
10
11
12
JVM内存
├── 程序计数器(Program Counter Register)
├── 虚拟机栈(VM Stack)
├── 本地方法栈(Native Method Stack)
├── 堆(Heap)
│ ├── 新生代(Young Generation)
│ │ ├── Eden区
│ │ ├── Survivor From区
│ │ └── Survivor To区
│ └── 老年代(Old Generation)
└── 方法区(Method Area)
└── 运行时常量池(Runtime Constant Pool)

JVM内存区域(Java 8+):

1
2
3
4
5
6
7
8
9
10
11
12
JVM内存
├── 程序计数器(Program Counter Register)
├── 虚拟机栈(VM Stack)
├── 本地方法栈(Native Method Stack)
├── 堆(Heap)
│ ├── 新生代(Young Generation)
│ │ ├── Eden区
│ │ ├── Survivor From区
│ │ └── Survivor To区
│ └── 老年代(Old Generation)
└── 元空间(Metaspace)
└── 运行时常量池(Runtime Constant Pool)

2.1.2 各内存区域说明

程序计数器

  • 线程私有
  • 记录当前执行的字节码指令地址
  • 唯一不会发生OOM的区域

虚拟机栈

  • 线程私有
  • 存储局部变量、方法参数、返回地址
  • 可能发生StackOverflowError和OOM

本地方法栈

  • 线程私有
  • 为Native方法服务
  • 可能发生StackOverflowError和OOM

  • 线程共享
  • 存储对象实例
  • 可能发生OOM

方法区/元空间

  • 线程共享
  • 存储类信息、常量、静态变量
  • 可能发生OOM

2.2 堆内存结构

2.2.1 堆内存划分

新生代(Young Generation)

  • Eden区:新对象分配区域
  • Survivor From区:GC后存活对象
  • Survivor To区:GC后存活对象
  • 比例:Eden:Survivor = 8:1:1

老年代(Old Generation)

  • 长期存活的对象
  • 大对象直接进入老年代

堆内存参数

1
2
3
4
5
-Xms512m          # 初始堆内存
-Xmx2048m # 最大堆内存
-Xmn256m # 新生代大小
-XX:SurvivorRatio=8 # Eden:Survivor比例
-XX:NewRatio=2 # 老年代:新生代比例

2.3 方法区/元空间

2.3.1 方法区演变

Java 7及之前

  • 方法区(Method Area)
  • 永久代(PermGen)
  • 参数:-XX:PermSize-XX:MaxPermSize

**Java 8+**:

  • 元空间(Metaspace)
  • 使用本地内存
  • 参数:-XX:MetaspaceSize-XX:MaxMetaspaceSize

变化原因

  • 永久代大小难以确定
  • 容易发生OOM
  • 元空间使用本地内存,更灵活

3. 堆内存溢出

3.1 堆溢出概述

3.1.1 错误信息

1
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

3.1.2 发生原因

主要原因

  1. 对象过多:创建了大量对象,超出堆内存限制
  2. 内存泄漏:对象无法被GC回收,持续占用内存
  3. 堆内存设置过小-Xmx设置不合理
  4. 大对象:创建了过大的对象

3.2 堆溢出示例

3.2.1 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
static class OOMObject {
// 创建一个较大的对象
private byte[] data = new byte[1024 * 1024]; // 1MB
}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject()); // 持续创建对象
}
}
}

运行参数

1
2
java -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heap_dump.hprof HeapOOM

3.2.2 错误输出

1
2
3
4
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /tmp/heap_dump.hprof ...
Heap dump file created [12345678 bytes in 0.123 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

3.3 堆溢出诊断

3.3.1 使用jmap生成堆转储

1
2
3
4
5
6
7
8
# 查看堆内存使用情况
jmap -heap <pid>

# 生成堆转储文件
jmap -dump:format=b,file=/tmp/heap_dump.hprof <pid>

# 查看对象统计
jmap -histo <pid> | head -20

3.3.2 使用jstat监控GC

1
2
3
4
5
# 查看GC统计
jstat -gcutil <pid> 1000 10

# 查看堆内存使用
jstat -gccapacity <pid>

3.3.3 使用MAT分析堆转储

MAT(Memory Analyzer Tool)

  1. 下载MAT工具
  2. 打开堆转储文件
  3. 分析内存占用
  4. 查找内存泄漏

MAT分析步骤

  1. Histogram:查看对象数量和大小
  2. Dominator Tree:查看对象引用关系
  3. Leak Suspects:自动检测内存泄漏
  4. Thread Overview:查看线程内存占用

3.4 堆溢出解决方案

3.4.1 增加堆内存

1
2
3
4
5
# 增加最大堆内存
java -Xms512m -Xmx2048m YourApplication

# 根据服务器内存合理设置
# 建议:堆内存 = 物理内存的50%-70%

3.4.2 优化代码

避免创建过多对象

1
2
3
4
5
6
7
8
9
10
// 不推荐:在循环中创建大量对象
for (int i = 0; i < 1000000; i++) {
String str = new String("test" + i);
}

// 推荐:使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000000; i++) {
sb.append("test").append(i);
}

及时释放引用

1
2
3
4
5
6
7
8
9
// 不推荐:持有大对象引用
List<BigObject> list = new ArrayList<>();
// ... 使用list
// list = null; // 忘记置空

// 推荐:及时释放
List<BigObject> list = new ArrayList<>();
// ... 使用list
list = null; // 及时置空,帮助GC

3.4.3 优化GC

1
2
3
4
5
6
7
8
# 使用G1 GC(大堆内存)
java -XX:+UseG1GC -Xmx4g YourApplication

# 调整GC参数
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=16m \
-Xmx4g YourApplication

4. 栈内存溢出

4.1 栈溢出概述

4.1.1 错误信息

1
Exception in thread "main" java.lang.StackOverflowError

4.1.2 发生原因

主要原因

  1. 递归过深:递归调用层次太深
  2. 局部变量过多:方法中局部变量占用空间过大
  3. 栈深度设置过小-Xss设置不合理

4.2 栈溢出示例

4.2.1 递归过深示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StackOverflow {
private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak(); // 无限递归
}

public static void main(String[] args) {
StackOverflow oom = new StackOverflow();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("栈深度: " + oom.stackLength);
throw e;
}
}
}

运行参数

1
java -Xss128k StackOverflow

4.2.2 局部变量过多示例

1
2
3
4
5
6
7
8
9
public class StackOverflow {
public static void main(String[] args) {
// 创建大量局部变量
int[] array1 = new int[10000];
int[] array2 = new int[10000];
int[] array3 = new int[10000];
// ... 更多局部变量
}
}

4.3 栈溢出诊断

4.3.1 查看线程栈

1
2
3
4
5
# 生成线程转储
jstack <pid> > /tmp/thread_dump.txt

# 查看栈深度
jstack <pid> | grep -A 50 "Thread-1"

4.3.2 分析栈信息

查看线程转储文件

  • 查找递归调用
  • 查看方法调用链
  • 分析栈深度

4.4 栈溢出解决方案

4.4.1 增加栈深度

1
2
3
4
5
# 增加栈大小
java -Xss512k YourApplication

# 或
java -Xss1m YourApplication

4.4.2 优化递归

将递归改为迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不推荐:深度递归
public int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

// 推荐:迭代方式
public int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}

使用尾递归优化

1
2
3
4
5
// 尾递归
public int factorial(int n, int acc) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc);
}

4.4.3 减少局部变量

1
2
3
4
5
6
7
8
9
10
11
12
// 不推荐:大量局部变量
public void method() {
int[] array1 = new int[10000];
int[] array2 = new int[10000];
// ...
}

// 推荐:使用对象封装
public void method() {
DataHolder holder = new DataHolder();
// ...
}

5. 方法区溢出

5.1 方法区溢出概述

5.1.1 错误信息

Java 7及之前

1
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

**Java 8+**:

1
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

5.1.2 发生原因

主要原因

  1. 类加载过多:动态生成大量类
  2. 常量池过大:字符串常量过多
  3. 方法区设置过小-XX:MaxPermSize-XX:MaxMetaspaceSize设置不合理

5.2 方法区溢出示例

5.2.1 动态类生成示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;

public class MetaspaceOOM {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
for (int i = 0; i < 100000; i++) {
// 动态生成类
CtClass cc = pool.makeClass("GeneratedClass" + i);
Class<?> clazz = cc.toClass();
}
}
}

运行参数

1
2
3
4
5
# Java 7
java -XX:PermSize=10m -XX:MaxPermSize=10m MetaspaceOOM

# Java 8+
java -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m MetaspaceOOM

5.2.2 字符串常量池示例

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.ArrayList;
import java.util.List;

public class StringConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
// intern()将字符串加入常量池
list.add(String.valueOf(i++).intern());
}
}
}

5.3 方法区溢出诊断

5.3.1 查看类加载信息

1
2
3
4
5
# 查看类加载统计
jstat -class <pid>

# 查看元空间使用
jstat -gc <pid> | grep Metaspace

5.3.2 使用jmap查看类信息

1
2
3
4
5
# 查看类加载器
jmap -clstats <pid>

# 生成堆转储分析
jmap -dump:format=b,file=/tmp/heap_dump.hprof <pid>

5.4 方法区溢出解决方案

5.4.1 增加方法区大小

1
2
3
4
5
# Java 7
java -XX:PermSize=256m -XX:MaxPermSize=256m YourApplication

# Java 8+
java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m YourApplication

5.4.2 优化类加载

避免动态生成过多类

1
2
3
4
5
6
7
// 不推荐:动态生成大量类
for (int i = 0; i < 100000; i++) {
generateClass("Class" + i);
}

// 推荐:复用类或使用其他方式
// 使用反射或代理

及时卸载类

1
2
3
4
5
// 使用自定义类加载器
// 在不需要时卸载类
ClassLoader loader = new CustomClassLoader();
// ... 使用
loader = null; // 帮助GC卸载类

5.4.3 优化字符串使用

1
2
3
4
5
// 不推荐:大量使用intern()
String str = new String("test").intern();

// 推荐:合理使用intern()
// 只对重复字符串使用intern()

6. 直接内存溢出

6.1 直接内存溢出概述

6.1.1 错误信息

1
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

6.1.2 发生原因

主要原因

  1. NIO使用不当:大量使用DirectByteBuffer
  2. 直接内存设置过小-XX:MaxDirectMemorySize设置不合理
  3. 直接内存未释放:DirectByteBuffer未及时回收

6.2 直接内存溢出示例

6.2.1 NIO使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class DirectMemoryOOM {
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
while (true) {
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB
list.add(buffer);
}
}
}

运行参数

1
java -XX:MaxDirectMemorySize=10m DirectMemoryOOM

6.3 直接内存溢出诊断

6.3.1 查看直接内存使用

1
2
3
4
5
6
# 使用jmap查看
jmap -histo <pid> | grep DirectByteBuffer

# 使用NMT(Native Memory Tracking)
java -XX:NativeMemoryTracking=summary YourApplication
jcmd <pid> VM.native_memory summary

6.4 直接内存溢出解决方案

6.4.1 增加直接内存

1
2
# 增加直接内存大小
java -XX:MaxDirectMemorySize=512m YourApplication

6.4.2 优化NIO使用

及时释放DirectByteBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
// 推荐:使用try-with-resources或手动释放
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
try {
// 使用buffer
} finally {
// 手动释放(通过反射调用cleaner)
if (buffer.isDirect()) {
sun.misc.Cleaner cleaner = ((sun.nio.ch.DirectBuffer) buffer).cleaner();
if (cleaner != null) {
cleaner.clean();
}
}
}

使用堆内存代替直接内存

1
2
// 如果不需要直接内存的优势,使用堆内存
ByteBuffer buffer = ByteBuffer.allocate(1024); // 堆内存

7. GC开销超限

7.1 GC开销超限概述

7.1.1 错误信息

1
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

7.1.2 发生原因

触发条件

  • 连续多次GC,回收的内存很少(<2%)
  • GC时间占比超过98%

主要原因

  1. 堆内存过小:频繁触发GC
  2. 内存泄漏:对象无法回收
  3. GC效率低:GC算法不适合

7.2 GC开销超限示例

7.2.1 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.ArrayList;
import java.util.List;

public class GCOverheadOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
// 创建对象但保留引用,导致GC效率低
list.add(String.valueOf(i++).intern());
if (i % 1000 == 0) {
list.remove(0); // 只移除少量,大部分无法回收
}
}
}
}

运行参数

1
2
3
4
java -Xmx10m -XX:+UseG1GC \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
GCOverheadOOM

7.3 GC开销超限诊断

7.3.1 查看GC日志

1
2
3
4
5
6
7
8
# 启用GC日志
java -Xloggc:/tmp/gc.log \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
YourApplication

# 分析GC日志
# 查看GC频率和回收效果

7.3.2 使用GCViewer分析

GCViewer工具

  1. 下载GCViewer
  2. 打开GC日志文件
  3. 分析GC统计信息
  4. 查看GC效率

7.4 GC开销超限解决方案

7.4.1 增加堆内存

1
2
# 增加堆内存,减少GC频率
java -Xms512m -Xmx2048m YourApplication

7.4.2 优化GC算法

1
2
3
4
5
6
7
# 使用G1 GC(适合大堆内存)
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Xmx4g YourApplication

# 或使用ZGC(Java 11+,低延迟)
java -XX:+UseZGC -Xmx4g YourApplication

7.4.3 修复内存泄漏

查找内存泄漏

  1. 使用MAT分析堆转储
  2. 查找无法回收的对象
  3. 修复代码中的内存泄漏

8. 无法创建线程

8.1 无法创建线程概述

8.1.1 错误信息

1
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

8.1.2 发生原因

主要原因

  1. 线程过多:创建了过多线程
  2. 栈内存不足:每个线程需要栈内存
  3. 系统限制:操作系统线程数限制

8.2 无法创建线程示例

8.2.1 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UnableToCreateThread {
public static void main(String[] args) {
int i = 0;
while (true) {
new Thread(() -> {
try {
Thread.sleep(Integer.MAX_VALUE); // 线程不退出
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("创建线程: " + (++i));
}
}
}

运行参数

1
java -Xss1m UnableToCreateThread

8.3 无法创建线程诊断

8.3.1 查看线程数

1
2
3
4
5
6
7
8
# 查看Java线程数
jstack <pid> | grep "java.lang.Thread.State" | wc -l

# 查看系统线程数
ps -eLf | grep java | wc -l

# 查看系统限制
ulimit -u # 最大用户进程数

8.4 无法创建线程解决方案

8.4.1 使用线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 不推荐:直接创建线程
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
// 任务
}).start();
}

// 推荐:使用线程池
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
// 任务
});
}

8.4.2 减少栈大小

1
2
# 减少每个线程的栈大小
java -Xss256k YourApplication

8.4.3 增加系统限制

1
2
3
4
5
6
7
8
# 增加最大用户进程数
ulimit -u 4096

# 或修改系统配置
vim /etc/security/limits.conf
# 添加:
# * soft nproc 4096
# * hard nproc 8192

9. 内存泄漏

9.1 内存泄漏概述

9.1.1 什么是内存泄漏

内存泄漏

  • 对象无法被GC回收
  • 持续占用内存
  • 最终导致内存溢出

内存泄漏 vs 内存溢出

  • 内存泄漏:对象无法回收(原因)
  • 内存溢出:内存不足(结果)

9.2 常见内存泄漏场景

9.2.1 静态集合持有引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不推荐:静态集合持有对象引用
public class MemoryLeak {
private static List<Object> list = new ArrayList<>();

public void add(Object obj) {
list.add(obj); // 对象永远不会被回收
}
}

// 推荐:使用弱引用或及时清理
public class MemoryLeak {
private static List<WeakReference<Object>> list = new ArrayList<>();

public void add(Object obj) {
list.add(new WeakReference<>(obj));
}
}

9.2.2 监听器未移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 不推荐:监听器未移除
public class MemoryLeak {
private List<Listener> listeners = new ArrayList<>();

public void addListener(Listener listener) {
listeners.add(listener);
// 忘记移除
}
}

// 推荐:及时移除监听器
public class MemoryLeak {
private List<Listener> listeners = new ArrayList<>();

public void addListener(Listener listener) {
listeners.add(listener);
}

public void removeListener(Listener listener) {
listeners.remove(listener);
}
}

9.2.3 内部类持有外部类引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 不推荐:内部类持有外部类引用
public class Outer {
private byte[] data = new byte[1024 * 1024]; // 大对象

class Inner {
void doSomething() {
// 内部类持有Outer引用
}
}
}

// 推荐:使用静态内部类
public class Outer {
private byte[] data = new byte[1024 * 1024];

static class Inner {
void doSomething() {
// 静态内部类不持有Outer引用
}
}
}

9.2.4 ThreadLocal未清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 不推荐:ThreadLocal未清理
public class MemoryLeak {
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

public void set(Object obj) {
threadLocal.set(obj);
// 忘记remove()
}
}

// 推荐:及时清理ThreadLocal
public class MemoryLeak {
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

public void set(Object obj) {
threadLocal.set(obj);
}

public void remove() {
threadLocal.remove(); // 及时清理
}
}

9.3 内存泄漏诊断

9.3.1 使用MAT分析

步骤

  1. 生成堆转储文件
  2. 使用MAT打开
  3. 查看Leak Suspects报告
  4. 分析Dominator Tree
  5. 查找无法回收的对象

9.3.2 使用jmap分析

1
2
3
4
5
# 查看对象统计
jmap -histo <pid> | head -20

# 对比两次统计,查看对象数量变化
# 如果对象数量持续增长,可能存在内存泄漏

10. 诊断工具

10.1 jmap工具

10.1.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
# 查看堆内存使用
jmap -heap <pid>

# 生成堆转储
jmap -dump:format=b,file=/tmp/heap_dump.hprof <pid>

# 查看对象统计
jmap -histo <pid>

# 查看存活对象统计
jmap -histo:live <pid>

10.1.2 堆转储分析

生成堆转储

1
2
3
4
5
6
7
8
9
10
11
# 方式1:使用jmap
jmap -dump:format=b,file=/tmp/heap_dump.hprof <pid>

# 方式2:JVM参数(OOM时自动生成)
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heap_dump.hprof \
YourApplication

# 方式3:使用jcmd
jcmd <pid> GC.run_finalization
jcmd <pid> VM.dump_heap /tmp/heap_dump.hprof

10.2 jstat工具

10.2.1 GC统计

1
2
3
4
5
6
7
8
# 查看GC统计
jstat -gcutil <pid> 1000 10

# 查看堆内存容量
jstat -gccapacity <pid>

# 查看类加载统计
jstat -class <pid>

10.2.2 GC日志分析

输出说明

1
2
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00 0.00 25.00 45.00 80.00 75.00 10 0.250 2 0.500 0.750
  • S0/S1:Survivor区使用率
  • E:Eden区使用率
  • O:老年代使用率
  • M:元空间使用率
  • YGC:Young GC次数
  • YGCT:Young GC总耗时
  • FGC:Full GC次数
  • FGCT:Full GC总耗时

10.3 MAT工具

10.3.1 MAT使用步骤

  1. 下载MAThttps://www.eclipse.org/mat/
  2. 打开堆转储文件
  3. 分析内存占用
    • Histogram:对象统计
    • Dominator Tree:对象引用树
    • Leak Suspects:内存泄漏检测
  4. 查找问题:定位内存泄漏源头

10.3.2 MAT分析技巧

查找大对象

  1. 打开Dominator Tree
  2. 按Retained Heap排序
  3. 查看占用内存最大的对象

查找内存泄漏

  1. 打开Leak Suspects报告
  2. 查看可疑对象
  3. 分析对象引用链
  4. 定位泄漏源头

10.4 VisualVM工具

10.4.1 VisualVM使用

功能

  • 实时监控JVM
  • 查看堆内存使用
  • 分析线程
  • 生成堆转储

使用步骤

  1. 启动VisualVM
  2. 连接到Java进程
  3. 查看监控信息
  4. 生成堆转储

10.5 jcmd工具

10.5.1 jcmd使用

1
2
3
4
5
6
7
8
9
10
11
12
# 查看所有命令
jcmd <pid> help

# 生成堆转储
jcmd <pid> GC.run_finalization
jcmd <pid> VM.dump_heap /tmp/heap_dump.hprof

# 查看JVM信息
jcmd <pid> VM.info

# 查看系统属性
jcmd <pid> VM.system_properties

11. 实战案例

11.1 案例1:Web应用堆溢出

11.1.1 问题描述

现象

  • 生产环境Web应用频繁OOM
  • 错误信息:java.lang.OutOfMemoryError: Java heap space
  • 应用重启后一段时间又出现

11.1.2 诊断过程

1
2
3
4
5
6
7
8
# 1. 查看堆内存使用
jmap -heap <pid>

# 2. 生成堆转储
jmap -dump:format=b,file=/tmp/heap_dump.hprof <pid>

# 3. 使用MAT分析
# 发现:大量Session对象无法回收

11.1.3 问题原因

原因

  • Session对象被静态Map持有
  • Session过期后未清理
  • 导致内存泄漏

11.1.4 解决方案

1
2
3
4
5
6
7
8
9
// 修复前
private static Map<String, Session> sessionMap = new HashMap<>();

// 修复后
private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
// 添加定时清理过期Session
scheduledExecutor.scheduleAtFixedRate(() -> {
sessionMap.entrySet().removeIf(entry -> entry.getValue().isExpired());
}, 0, 1, TimeUnit.HOURS);

11.2 案例2:大数据处理栈溢出

11.2.1 问题描述

现象

  • 处理大数据时出现栈溢出
  • 错误信息:java.lang.StackOverflowError
  • 递归深度过大

11.2.2 解决方案

1
2
3
4
5
6
7
8
9
10
11
12
// 修复前:递归实现
public void process(List<Data> dataList) {
if (dataList.isEmpty()) return;
process(dataList.subList(1, dataList.size()));
}

// 修复后:迭代实现
public void process(List<Data> dataList) {
for (Data data : dataList) {
// 处理数据
}
}

11.3 案例3:动态类生成元空间溢出

11.3.1 问题描述

现象

  • 使用CGLib动态生成代理类
  • 元空间持续增长
  • 最终OOM:java.lang.OutOfMemoryError: Metaspace

11.3.2 解决方案

1
2
3
4
5
6
7
8
9
10
// 修复前:每次都生成新类
for (int i = 0; i < 100000; i++) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TargetClass.class);
enhancer.setCallback(new MethodInterceptor() { ... });
enhancer.create(); // 生成新类
}

// 修复后:复用代理类或使用其他方式
// 使用JDK动态代理或缓存代理类

12. 架构设计最佳实践

12.1 内存管理策略

12.1.1 堆内存配置

配置原则

  • -Xms-Xmx设置为相同值,避免动态调整
  • 堆内存 = 物理内存的50%-70%
  • 根据应用特点调整新生代和老年代比例

配置示例

1
2
3
4
5
6
7
8
# 生产环境推荐配置
java -Xms2g \
-Xmx2g \
-XX:NewRatio=2 \
-XX:SurvivorRatio=8 \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
YourApplication

12.1.2 GC选择

GC选择原则

场景 推荐GC 参数
小堆内存(<4GB) Parallel GC -XX:+UseParallelGC
大堆内存(>4GB) G1 GC -XX:+UseG1GC
低延迟要求 ZGC(Java 11+) -XX:+UseZGC
超大堆内存(>32GB) ZGC或Shenandoah -XX:+UseZGC

12.2 内存监控

12.2.1 监控指标

关键指标

  • 堆内存使用率
  • GC频率和耗时
  • Full GC频率
  • 内存泄漏趋势

12.2.2 监控方案

使用Prometheus + Grafana

1
2
3
4
5
# prometheus.yml
scrape_configs:
- job_name: 'jvm'
static_configs:
- targets: ['localhost:9999']

使用JMX Exporter

1
2
java -javaagent:jmx_prometheus_javaagent.jar=9999:config.yml \
YourApplication

12.3 预防措施

12.3.1 代码层面

  1. 及时释放引用:对象使用完后及时置null
  2. 避免内存泄漏:注意静态集合、监听器、ThreadLocal
  3. 合理使用缓存:设置缓存大小和过期时间
  4. 优化数据结构:选择合适的数据结构

12.3.2 架构层面

  1. 限流:防止突发流量导致内存溢出
  2. 降级:内存不足时降级服务
  3. 扩容:根据负载动态调整资源
  4. 监控告警:及时发现内存问题

13. 总结

13.1 核心要点

  1. 内存溢出类型:堆溢出、栈溢出、方法区溢出、直接内存溢出等
  2. 诊断工具:jmap、jstat、MAT、VisualVM等
  3. 解决方案:增加内存、优化代码、优化GC
  4. 内存泄漏:常见场景和预防措施
  5. 架构设计:内存管理策略和监控方案

13.2 架构师建议

  1. 预防为主

    • 合理设置JVM参数
    • 代码层面避免内存泄漏
    • 架构层面做好限流和降级
  2. 监控告警

    • 实时监控内存使用
    • 设置合理的告警阈值
    • 及时发现问题
  3. 快速响应

    • 建立故障处理流程
    • 准备诊断工具和脚本
    • 定期演练

13.3 最佳实践

  1. JVM参数调优:根据应用特点合理配置
  2. 代码审查:避免常见的内存泄漏场景
  3. 压力测试:提前发现内存问题
  4. 监控告警:建立完善的监控体系

相关文章