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释放锁: