JVM
在弄清楚OOM产生的原因之前,我们务必要搞清楚Java的内存模型,也就是JVM,这可以参考我之前的一个学习笔记 JVM学习笔记
先来看一下要用到的JVM的一些参数:
| 参数名称 | 含义 |
|---|---|
| -Xms | 初始堆大小 |
| -Xmx | 最大堆大小 |
| -Xmn | 年轻代大小 |
| -Xss | 每个线程的堆栈大小 |
| -XX:+HeapDumpOnOutOfMemoryError | 目录下生成堆的Dump文件 |
当初始化堆内存容量小于MinHeapFreeRatio 时,JVM会增大堆直到Xmx最大限制,当空余内存大于MaxHeapFreeRatio时,JVM会减小堆直到Xms最小限制,其中MinHeapFreeRatio和MaxHeapFreeRatio都是可以配置的。
OOM
OOM全称Out Of Memory,被称为是内存溢出(OutOfMemoryError),在JVM运行时的内存区域里,除了程序计数器,其它几个区域都可能会发生内存溢出的异常,下面主要来分析堆内存上面的OOM。
堆内存的OOM
接下来模拟一个堆上发生的OOM,编写Java代码,其思路就是往List集合里面无限放匿名对象:
package com.mezjh.test;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author ZJH
* @date 2020/8/12 14:48
*/
public class TestHeapOOM {
public static void main(String[] args) {
List<TestObject> list = new ArrayList<>();
while (true) {
list.add(new TestObject());
}
}
}
其中TestObject是一个什么都没有的类:
package com.mezjh.test;
/**
* @author ZJH
* @date 2020/8/12 15:25
*/
public class TestObject {
}
接下来将最大的堆内存设置为100m,然后运行代码,会很快发生OOM异常

出现的异常如下,即OutOfMemoryError:

我们分析OutOfMemoryError异常时一般需要借助一些工具,以便于更快定位问题所在。
Heap Dump
它是Java进程所使用的内存情况在某一时间的一次快照,以文件的形式持久化到磁盘中。其一般包括如下信息:
- 所有对象信息
- 所有的类信息
- 垃圾回收的根对象
- 线程栈及局部变量
接下来我们使用JVM的配置参数生成Dump文件,修改JVM配置,如下图所示,这样会在发生OutOfMemoryError异常时生成堆的Dump文件:

在异常信息上方可以看到生成了该文件

在项目文件目录下,可以找到刚才生成的Dump文件:

直接打开的话,里面全部都是二进制,这里需要使用如下图所示的jdk自带的可视化工具jvisualvm.exe(Java性能分析工具)打开该文件。

打开之后可以看到一些基本信息:

点进类选项卡,这里可以很清楚的看到TestObject对象几乎占了所有内存,实例数占了99.7%。

同时jvisualvm也可以分析运行中的Java的内存的使用情况,接下来可以看看如何分析在运行时候的内存使用情况,先打开该工具,为了分析Java的内存使用情况,需要安装以下插件

然后启动一个Java线程,如下图所示,左侧栏会显示该线程,我们打开此线程:

选中Visual GC,会清楚的看到在何时发生了GC(JVM学习笔记中有提到堆内存的GC过程):

生成Dump文件的方式除了上述修改JVM参数之外,还可以通过命令行的方式来生成该文件: - jmap(Memory Map for Java):生成虚拟机的内存转储快照(heapdump文件);
- jstack(Stack Trace for Java):显示虚拟机的线程快照;
jmap
是一个多功能命令,它可以生成Java程序的dump文件,也可以查看堆内对象信息,查看ClassLoader的信息以及finalizer队列。
要使用jmap,需要用jps查看当前Java程序的进程ID:

jmap [pid] 查看当前共享对象的信息,从左到右一次为起始地址,大小,路径

jmap -heap [pid]
这个命令可以看到堆的配置信息,可以看到这是我们设置的最大堆大小MaxHeapSize为100M

除此之外,还可以看到堆中各个区域的内存使用情况

jmap -histo:live [pid]
这个命令可以查看类的使用情况,如下图,TestObject对象一共有98970个,其占用内存是1583520 byte。

jmap -clstats [pid]
该命令可以查看类加载器的信息,如下图,根类加载器加载了1475个类…

jmap -finalizerinfo [pid]
查看finalizer队列,即要被执行垃圾回收的队列,此时 Number of objects pending for finalization为0,就代表要被回收的对象为0个。

jmap -dump:live,format=b,file=jmap.bin [pid]
这个命令可以打印Dump文件,live代表存活的对象,b代表以2进制的形式,file代表目录,


jstack
查看Java应用程序中线程堆栈信息,我们的程序在运行时卡顿或长时间未响应时可以使用该命令。
-F 当线程挂起时,使用jstack -l [pid] 不被响应时,强制输出线程堆栈;
-l 除堆栈外,显示关于锁的附加信息;
jstack -F [pid]
查看堆栈信息,No deadlocks found ,说明没有死锁发生,state代表线程的状态

接下来编写一个死循环代码:用jstack查看堆栈信息
package com.mezjh.test;
/**
*
* @author ZJH
* @date 2020/8/12 14:48
*/
public class TestHeapOOM {
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1() {
while(true) {
}
}
}
如果cpu占用过高导致卡顿,堆栈并没有溢出,此时jstack这个命令就有用了,它能排查出绝大多数死循环或者死锁问题。

接下来用jstack检查一下死锁:
新增方法2,代码如下,在方法2中制造了一个死锁场景:
public static void test2() {
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
new Thread(() -> {
try {
lock1.lock();
Thread.sleep(300);
lock2.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread1").start();
new Thread(() -> {
try {
lock2.lock();
Thread.sleep(300);
lock1.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();
}
运行程序之后,发现程序并没有停止,这个时候使用jstack查看线程的堆栈情况,会很明显的看到Found 1 deadlock:

这里可以查看死锁详情,thread1在等待thread2释放锁,thread2在等待thread1释放锁:


本文介绍了Java内存模型JVM,重点分析了堆内存溢出(OOM)的原因,并提供了使用jmap和jstack等工具进行故障排查的方法。通过模拟堆内存溢出,展示了如何生成Heap Dump文件,以及利用jvisualvm分析内存使用情况和查找问题。

被折叠的 条评论
为什么被折叠?



