深入解析Java内存溢出:排查方法与优化技巧
- 作者
内存溢出是指程序申请内存时,没有足够的内存可以使用;内存溢出通常是由内存泄露引起的,常见的场景是由于有问题的代码申请完内存,却没有及时有效释放,长此以往,程序就申请不到内存,从而导致内存溢出。
一、内存溢出分类
Java 里面内存溢出主要分以下几种情况:
1.1 栈溢出
StackOverflowError
主要是指方法调用层次太深,内存不够新建栈帧,常见的场景是比如一个没有结束条件的递归。
OutOfMemoryError
主要是指同时启动的线程太多,内存不够新建线程。
1.2 堆溢出
内存溢出 - 无法申请到内存
可以使用 jstat 查看内存使用情况,检查堆参数,一般是程序申请的内存太大,比如一个特别大的 byte 数组。
内存泄漏 - 对象无法回收
可以使用 MAT/VisualVM 等工具里面的 Path to GC Roots 定位,一般是一个特大不能回收的对象。
1.3 MetaSpace 数据区溢出
主要是指动态生成大量 Class 导致 MetaSpace 数据区内存不够,常见的场景比如动态编译加载脚本代码。
1.4 本地直接内存溢出
常见的场景是程序申请一个大的直接内存。
1.5 数据超限内存溢出
常见的场景是分配的数据结构在此平台不可寻址。
二、主要排查方法
2.1 内存分析工具
内存溢出主要会通过内存分析工具类分析代码的问题,常见的分析工具如下,可以根据实际的需求选择对应的工具来进行排查,这里面用得最多的是 MAT。
产品功能 | MAT | JProfiler | Visual VM | jhat | jmap | hprof |
---|---|---|---|---|---|---|
对象关联分析、深浅堆、GC ROOT、内存泄漏检测、线程分析 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
离线全局分析 | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
内存实时分配情况 | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
OQL | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
内存分配堆栈、热点比例 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
堆外内存分析 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
2.2 JDK 自带命令
除了使用内存分析工具外,JDK 还提供了一些命令行工具来帮助排查内存溢出问题。以下是一些常用的命令示例:
# 监控 JVM 的内存使用情况
jstat -gcutil <pid> 250 20
# 输出虚拟机启动时传递给主类 main() 的参数,输出主类的全名
jps -ml
# 生成堆转储快照,可以使用 VisualVM/MAT 等工具分析
jmap -F -dump:live,format=b,file=dump.bin <pid>
# 显示虚拟机快照
jstack -F -l -m <pid>
jstack -l <pid> >> thread_dump.txt
# 实时查看 JVM 对象大小的排序结果,无法看到对象的具体内容
jmap -histo <pid>
2.3 参数检查
在调优内存溢出问题时,还可以通过调整 JVM 启动参数来优化内存的分配和使用。以下是一些常用的参数:
# 初始堆大小
-Xms
# 最大堆大小
-Xmx
# 新生代大小
-Xmn
# 元数据区大小
XX:MaxMetaspaceSize
# Eden 与 Survivor 区的大小比值
-XX:SurvivorRatio
# 并行垃圾回收线程数
-XX:ParallelGCThreads
三、小结
本文介绍了内存溢出的分类以及一些常用的排查方法。了解不同类型的内存溢出和对应的排查方法,可以帮助开发人员更好地定位和解决内存溢出问题。另外,熟悉内存分析工具和 JDK 提供的命令行工具也是必要的,它们可以提供详细的内存使用信息和线程堆栈信息,帮助开发人员准确定位问题所在。通过不断学习和实践,开发人员可以提高对内存溢出问题的诊断和解决能力。
分享内容