JVM内存溢出及死锁定位及分析

2022/4/10 7:12:30

本文主要是介绍JVM内存溢出及死锁定位及分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1 OutOfMemoryError

在《Java虚拟机规范》里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)可能。
如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正常还是非正常情况,如果是正常的需求,就应该考虑加大内存的设置,如果是非正常需求,那么就要对代码进行修改,修复这个bug。
处理问题的大致思路:1、定位问题;2、分析;3、解决问题。
定位问题可以通过MemoryAnalyzer(如Eclipse Memory Analyzer或者JProfiler)工具进行定位分析。
内存溢出与内存泄露

  • 内存溢出,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
  • 内存泄露,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

1.1 Java堆溢出

Java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机清楚这些对象,那么随着对象数量的增加,总容量限制后就会产生内存溢出异常。
1)编写Java代码

/**
 * VM Args: -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
 * @author: dvomu
 * @create: 2022-03-26
 */
public class HeapOOM {

    public static void main(String[] args) {
        List<HeapOOM> list = new ArrayList<>();
        while (true){
            list.add(new HeapOOM());
        }
    }
}

2)配置运行JVM参数

3)运行

可以看到,当发生内存溢出时,会dump内存到java_pid13465.hprof中,该文件在项目的根目录下。
4)通过内存映像分析工具(JProfiler)对Dump文件分析


5)问题分析
要解决这个内存区域的异常,常规发处理方式是首先通过内存映射分析工具(如JProfiler)对Dump出来的堆转存快照进行分析。先确定内存中导致OOM的对象是否有必要。
如果是内存泄露可进一步查看对象到GC Roots的引用链,找到泄露对象是通过怎样的应用路径、与哪些GC Roots关联才导致GC无法回收。
如果不是内存泄漏即内存中的对象确实都是必须存活的,那么检查虚拟机堆参数设置与机器内存对比,根据情况增加内存,再从代码检查是否存在某些对象生命周期太长,持有对象时间太长,存储结构不合理等。

1.2 虚拟机栈和本地方法栈溢出

HotSpot中不区分虚拟机栈和本地方法栈,因此-Xoss设置本地方法栈没有任何效果,栈容量只能通过-Xss参数设置。
在HotSpot中不支持动态扩展,所以只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
1)代码编写

/**
 * VM Args: -Xss160k
 * @author: dvomu
 * @create: 2022-03-26
 */
public class StackSOF {
    private int stackLength = 1;

    private void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Throwable{
        StackSOF sof = new StackSOF();
        try {
            sof.stackLeak();
        }catch (Throwable e){
            System.out.println("stackLength:"+sof.stackLength);
            e.printStackTrace();
            throw e;
        }
    }
}

2) 设置栈容量

注意:对于不同操作系统栈容量最小值不同,由操作系统内存分页大小确定。32位Windows可以设置-Xss128k,64位Windows最小设置-Xss180k,MacOS最小设置-Xss160k,Linux最小设置-Xss220k。低于此值会报错,错误信息如下:


3)运行代码

结论:无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。

1.3 方法区和运行时常量池溢出

虚拟机运行时常量池是方法区一部分。方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件比较苛刻。经常运行时生成大量动态类,比如程序使用了CGLib字节码增强和动态语言、大量jsp或动态产生jsp文件的应用、基础OSGI的应用(同一个类被不同类加载器加载会视为不同的类)等。

2 死锁排查与定位

有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等。由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要看下jvm的内部线程的执行情况,然后再进行分析查找出原因。
这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程 情况进行快照,并且打印出来,用法: jstack <pid>
1) 编写死锁代码

public class TestDeadLock {
    private static Object o1 = new Object();
    private static Object o2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (o1) {
                System.out.println("T1 拿到了 o1 的锁!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("T1 拿到了 o2 的锁!");
                }
            }
        },"T1").start();

        new Thread(() -> {
            synchronized (o2) {
                System.out.println("T2 拿到了 o2 的锁!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("T2 拿到了 o1 的锁!");
                }
            }
        },"T2").start();
    }
}

运行上面的代码,可以发现程序会一没有结束,并且T1拿不到o2的锁,T2拿不到o1的锁。

2)排查问题
通过jps查看运行的进程ID

$ jps -l
25392 com.dvomu.jvm.TestDeadLock
20498
21268 com.install4j.runtime.launcher.MacLauncher
25620 sun.tools.jps.Jps
25391 org.jetbrains.jps.cmdline.Launcher

通过jstack查看进程详情,语法:jstack <进程ID>

$ jstack 25392
2022-03-26 13:14:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):
.......
# 关键信息:发现死锁
Found one Java-level deadlock:
=============================
"T2":
 # T2等待锁Object类型的对象0x00007fcee1016ea8,但是被T1持有着
  waiting to lock monitor 0x00007fcee1016ea8 (object 0x000000076abb1d30, a java.lang.Object),
  which is held by "T1"
"T1":
 # T1等待锁Object类型的对象0x00007fcee1014618,但是被T2持有着
  waiting to lock monitor 0x00007fcee1014618 (object 0x000000076abb1d40, a java.lang.Object),
  which is held by "T2"

Java stack information for the threads listed above:
===================================================
"T2":
      # 具体代码行数 
	at com.dvomu.jvm.TestDeadLock.lambda$main$1(TestDeadLock.java:35)
	# T2 等待锁定0x000000076abb1d30
	- waiting to lock <0x000000076abb1d30> (a java.lang.Object)
	
	# T2已锁定0x000000076abb1d40
	- locked <0x000000076abb1d40> (a java.lang.Object)
	at com.dvomu.jvm.TestDeadLock$$Lambda$2/1283928880.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)
"T1":
      # 具体代码行数 
	at com.dvomu.jvm.TestDeadLock.lambda$main$0(TestDeadLock.java:21)
	# T1 等待锁定0x000000076abb1d40
	- waiting to lock <0x000000076abb1d40> (a java.lang.Object)
	
	# T1已锁定0x000000076abb1d30
	- locked <0x000000076abb1d30> (a java.lang.Object)
	at com.dvomu.jvm.TestDeadLock$$Lambda$1/455659002.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

可以清晰的看到:
T2获取了 <0x000000076abb1d40> 的锁,等待获取 <0x000000076abb1d30> 这个锁
T1获取了 <0x000000076abb1d30> 的锁,等待获取 <0x000000076abb1d40> 这个锁
由此可见,发生了死锁。

3 VisualVM故障处理可视化工具

VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。 VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。

  • 内存信息
  • 线程信息
  • Dump堆(本地进程)
  • Dump线程(本地进程)
  • 打开堆Dump。堆Dump可以用jmap来生成。
  • 打开线程Dump
  • 生成应用快照(包含内存信息、线程信息等等) 性能分析。CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类对象占用的内存,检查 哪些类占用内存多)
  • ......

3.1 启动Visualvm

在jdk的安装目录的bin目录下,找到jvisualvm,双击打开即可。
Mac在/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/bin

3.2 VisualVM远程连接

VisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现。

3.3 VisualVM检测死锁


通过线程Dump可以查看到详细的错误信息

3.3VisualVM检测堆内存

检测堆内存的具体使用情况,需要安装插件Visual GC进行检测:
1)安装Visual GC
JDK自带的jvisualvm在安装插件时报代无法连接Java VisualVM插件中心

可以通过将插件中心地址换成GitHub地址后再连接。在修改url之前,先到GitHub这个网址上找到与你jdk版本相对应的url。
jdk8可以用:https://visualvm.github.io/archive/uc/7u60/updates.xml.gz

然后就可以正常搜索插件

2)编写测试代码

/**
 * VM Args: -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
 *
 * @author: dvomu
 * @create: 2022-03-26
 */
public class TestHeap {
    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        while (true) {
            User user = new User();
            user.setId(1L);
            user.setUsername("user");
            user.setPassword("pass");
            if (System.currentTimeMillis() % 2 == 0) {
                //加入到集合,不符合条件的就成了垃圾对象 
                userList.add(user);
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
@Data
class User{
    private Long id;
    private String username;
    private String password;
}

3) 设置运行参数

启动应用。

4)查看Visual GC

可以看到,年轻代、老年代中的内存使用情况,运行一段时间后观察效果更佳明显。
当老年代满了后就会报OOM异常



这篇关于JVM内存溢出及死锁定位及分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程