jvisualvm定位JVM内存溢出,死锁,分析GC日志

OOM定位

  1. 创造一个会OutOfMemoryError的程序
import java.util.LinkedList;
import java.util.List;

public class OutOfMemoryDump {

    /**
     * JVM 参数
     * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Volumes/mac
     */

    public static void main(String[] args) {

        List<Byte[]> bytes = new LinkedList<>();
        while(true){

            Byte[] byteNew = new Byte[1024];
            bytes.add(byteNew);
        }
    }
}

运行时添加虚拟机运行参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Volumes/mac/oomdump.dump

然后可以在/Volumes/mac/中看到有个oomdump.dump的文件。

  1. 打开jvisualvm工具
    打开终端,输入
jvisualvm

会启动一个界面,如下
jvisualvm定位JVM内存溢出,死锁,分析GC日志_第1张图片
这个界面可以查看到当前已经在运行的jvm进程。点击左上角载入,然后选择我们的dump文件。
jvisualvm定位JVM内存溢出,死锁,分析GC日志_第2张图片
可以看到概要信息,我们出现的错误是OOM,线程是main主线程,然后点“类”按钮,会显示出现OOM时的类的信息:
jvisualvm定位JVM内存溢出,死锁,分析GC日志_第3张图片
可以很明显的看到有个类占据了95.9%的空间,是java.lang.Byte类,回看我们的代码,在List中不断增加大小为1024的Byte数组,那么这个地方就是引起内存溢出的主要地方。

线程死锁定位

  1. 模拟一个死锁
public class DeadLock {

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {

        new Thread(() ->{
            synchronized (lock1){
                System.out.println("第一条线程开始运行");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){

                    System.out.println("第一条线程开始结束");
                }
            }
        }).start();

        new Thread(() ->{
            synchronized (lock2){
                System.out.println("第二条线程开始");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){

                    System.out.println("第二条线程结束");
                }
            }
        }).start();
    }
}

第一条线程持有锁lock1,尝试去获取锁lock2,第二条线程持有锁lock2,尝试去获取第一条线程的锁lock1,两条线程都持有自己的锁,想获取对方的锁,造成死锁。

  1. 打开jvisualvm,左边可以看到我们运行的程序:
    jvisualvm定位JVM内存溢出,死锁,分析GC日志_第4张图片
    右键单击,然后选择线程Dump,可以看到:
    jvisualvm定位JVM内存溢出,死锁,分析GC日志_第5张图片
    翻到最下面发现日志:
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fcce103bca8 (object 0x000000076af74480, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fcce1037c08 (object 0x000000076af74490, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at net.blf2.testmac.DeadLock.lambda$main$1(DeadLock.java:35)
        - waiting to lock <0x000000076af74480> (a java.lang.Object)
        - locked <0x000000076af74490> (a java.lang.Object)
        at net.blf2.testmac.DeadLock$$Lambda$2/1508395126.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at net.blf2.testmac.DeadLock.lambda$main$0(DeadLock.java:20)
        - waiting to lock <0x000000076af74490> (a java.lang.Object)
        - locked <0x000000076af74480> (a java.lang.Object)
        at net.blf2.testmac.DeadLock$$Lambda$1/2136344592.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

提示发现死锁,继续阅读发现Thread-1持有锁0x000000076af74490,等待锁0x000000076af74480,Thread-0持有锁0x000000076af74480,等待锁0x000000076af74490,至此发现死锁并定位到死锁位置,即at net.blf2.testmac.DeadLock.lambda$main 1 ( D e a d L o c k . j a v a : 35 ) 和 a t n e t . b l f 2. t e s t m a c . D e a d L o c k . l a m b d a 1(DeadLock.java:35)和at net.blf2.testmac.DeadLock.lambda 1(DeadLock.java:35)atnet.blf2.testmac.DeadLock.lambdamain$0(DeadLock.java:20)就是死锁发生的位置。
若是在服务器环境下,并没有图形界面,可以使用jvisualvm的远程(在左侧侧边栏里面可以找到)功能,添加远程jvm实时分析,也可以使用命令:

# jps 用来查看当前机器运行的jvm进程情况,执行后类似如下显示
# 689 RemoteMavenServer
# 1105 Launcher
# 674 
# 1124 Jps
# 1063 Main
jps 
# 然后使用命令jstack 进程id > 路径 导出线程快照
jstack 1063 > /home/Main.txt 

# 把Main.txt 下载到本地进行分析即可

GC日志查看

在运行内存溢出那段代码的时候,GC的日志会在控制台输出:
输出日志如下:

Connected to the target VM, address: '127.0.0.1:50139', transport: 'socket'
[GC (Allocation Failure) [PSYoungGen: 2048K->496K(2560K)] 2048K->665K(9728K), 0.0021886 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2542K->496K(2560K)] 2712K->1669K(9728K), 0.0026511 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2541K->512K(2560K)] 3714K->3717K(9728K), 0.0020411 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2557K->480K(2560K)] 5762K->5748K(9728K), 0.0019036 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 5268K->5518K(7168K)] 5748K->5518K(9728K), [Metaspace: 3552K->3552K(1056768K)], 0.0078083 secs] [Times: user=0.04 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2045K->508K(2560K)] [ParOldGen: 5518K->6960K(7168K)] 7564K->7469K(9728K), [Metaspace: 3552K->3552K(1056768K)], 0.0087421 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2044K->2007K(2560K)] [ParOldGen: 6960K->6950K(7168K)] 9005K->8957K(9728K), [Metaspace: 3553K->3553K(1056768K)], 0.0110771 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2048K->2047K(2560K)] [ParOldGen: 6950K->6950K(7168K)] 8998K->8998K(9728K), [Metaspace: 3553K->3553K(1056768K)], 0.0092453 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7083K->7083K(7168K)] 9131K->9131K(9728K), [Metaspace: 3555K->3555K(1056768K)], 0.0070404 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7164K->7164K(7168K)] 9212K->9212K(9728K), [Metaspace: 3558K->3558K(1056768K)], 0.0049998 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7164K->7146K(7168K)] 9212K->9194K(9728K), [Metaspace: 3558K->3558K(1056768K)], 0.0103918 secs] [Times: user=0.04 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7166K->7166K(7168K)] 9214K->9214K(9728K), [Metaspace: 3558K->3558K(1056768K)], 0.0053740 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7166K->7166K(7168K)] 9214K->9214K(9728K), [Metaspace: 3558K->3558K(1056768K)], 0.0049872 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Volumes/mac/OOM.dump ...
Heap dump file created [19048922 bytes in 0.061 secs]
Exception in thread "main" [Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7167K->599K(7168K)] 9215K->599K(9728K), [Metaspace: 3562K->3562K(1056768K)], 0.0042563 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
java.lang.OutOfMemoryError: Java heap space
	at net.blf2.testmac.OutOfMemoryDump.main(OutOfMemoryDump.java:18)
Heap
 PSYoungGen      total 2560K, used 86K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 2048K, 4% used [0x00000007bfd00000,0x00000007bfd15850,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 7168K, used 599K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
  object space 7168K, 8% used [0x00000007bf600000,0x00000007bf695ed0,0x00000007bfd00000)
 Metaspace       used 3591K, capacity 4564K, committed 4864K, reserved 1056768K
  class space    used 397K, capacity 400K, committed 512K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:50139', transport: 'socket'

Process finished with exit code 1
  • GC日志:
[GC (Allocation Failure) [PSYoungGen: 2048K->496K(2560K)] 2048K->665K(9728K), 0.0021886 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 

这是一条Minor GC日志或者说是Young GC,PSYoungGen中的PS是垃圾收集器,2048K->496K(2560K)是从2048K收集完成后变成了496K,总的容量是2560K,然后2048K->665K(9728K)这一句是计算上了老年代的空间,从496K变成了665K说明新生代中有些存活的对象进入到了老年代,然后0.0021886 secs是收集时间,看起来延迟还是比较低的,大概2毫秒。

  • GC日志:
[Full GC (Ergonomics) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 5268K->5518K(7168K)] 5748K->5518K(9728K), [Metaspace: 3552K->3552K(1056768K)], 0.0078083 secs] [Times: user=0.04 sys=0.00, real=0.00 secs] 

这是一条Major GC日志或者说是Full GC,会先进行一次Minor GC(Young GC),然后进行老年代的GC,ParOldGen中的Par是垃圾收集器,信息和Young GC的差不多,都是收集前后大小和容量,Metaspace: 3552K->3552K(1056768K)是指元空间的手机信息,元空间是JDK8移除了永久代,在本地内存开辟的一块区域,用来代替永久代的一个区域,主要存放类加载的信息,和类加载器绑定,某个类加载器被回收的话,相应的元空间也会被回收,元空间会动态调整大小。

  • GC日志:
[Full GC (Allocation Failure) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7166K->7166K(7168K)] 9214K->9214K(9728K), [Metaspace: 3558K->3558K(1056768K)], 0.0049872 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

发现新生代,老年代都没法继续回收了,但程序还在申请内存,于是抛出了OOM异常。

你可能感兴趣的:(jvm,JAVA)