Java8虚拟机(JVM)内存溢出实战
前言
相信很多JAVA中高级的同学在面试的时候会经常碰到一个面试题
你是如何在工作中对JVM调优和排查定位问题的?
事实上,如果用户量不大的情况下,在你的代码还算正常的情况下,在工作中除非真正碰到与JVM相关的问题是少之又少,就算碰到了也是由公司的一些大牛去排查解决,那么我们又如何积累这方面的经验呢?下面由冲锅带大家一起来实践JVM的调优吧
注意我们平常所说的JVM调优一般指Java堆,Java虚拟机栈参数调优
Java堆溢出
先来一段代码示例,注意笔者用的是IDEA工具,需要配置一下VM options 为-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError,如果不清楚的百度一下如何配置idea的JVM运行参数
package com.example.demo.jvm; import java.util.ArrayList; import java.util.List; /** * @Author: Wang Chong * @Date: 2019/9/22 9:37 * @Version: V1.0 */ public class HeapOutMemoryTest { static class ChongGuo { } public static void main(String[] args) { List<ChongGuo> chongGuos = new ArrayList<>(); while (true) { chongGuos.add(new ChongGuo()); } } }
运行结果如下:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid9352.hprof ... Heap dump file created [28701160 bytes in 0.122 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.example.demo.jvm.HeapOutMemoryTest.main(HeapOutMemoryTest.java:18) Disconnected from the target VM, address: '127.0.0.1:54599', transport: 'socket'
可以看到控制台出现java.lang.OutOfMemoryError: Java heap space的错误,这是为什么呢,首先先解释一下上面的运行参数
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
- -Xms20m:设置JVM最小内存为20m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
- -Xmx20m:设置JVM最大可用内存20M
- -XX:+HeapDumpOnOutOfMemoryError 表示当JVM发生OOM时,自动生成DUMP文件
下面我们分析一下出错的原因,用JProfiler分析一下,打开刚才生成的名为java_pid9352.hprof的dump文件。可以看到根据(InstanceXcount和Size)基本可以确定哪个类的对象出现问题,在上面示例中,可以是ChongGuo这个实例生在数量的大小已经超过12M,但没有超过20M,那么新问题又来了?没到20M为啥会报堆内存溢出呢?
答案就是JDK8中堆内存中还包括Metaspace,即元内存空间,在元空间出现前JDK1.7之前在JDK7以及其前期的JDK版本号中。堆内存通常被分为三块区域Nursery内存(young generation)、长时内存(old generation)、永久内存(Permanent Generation for VM Matedata),如下图
当中最上一层是年轻代,一个对象被创建以后首先被放到年轻代中的Eden内存中,假设存活期超两个Survivor之后就会被转移到长时内存中(Old Generation)中永久内存中存放着对象的方法、变量等元数据信息。通过假设永久内存不够。我们就会得到例如以下错误:java.lang.OutOfMemoryError: PermGen
而在JDK8中情况发生了明显的变化,就是普通情况下你都不会得到这个错误,原因
在于JDK8中把存放元数据中的永久内存从堆内存中移到了本地内存(native memory)
中,JDK8中JVM堆内存结构就变成了例如以下:
如果我启动VM参数加上:-XX:MaxMetaspaceSize=1m,重新运行一下上面的程序,
Connected to the target VM, address: '127.0.0.1:56433', transport: 'socket' java.lang.OutOfMemoryError: Metaspace Dumping heap to java_pid9232.hprof ... Heap dump file created [1604635 bytes in 0.024 secs] FATAL ERROR in native method: processing of -javaagent failed Exception in thread "main" Disconnected from the target VM, address: '127.0.0.1:56433', transport: 'socket' Process finished with exit code 1
可以发现报错信息变成了java.lang.OutOfMemoryError: Metaspace,说明元空间不够,我改成到大概4m左右才能满足启动条件。
虚拟机栈和本地方法栈栈溢出
在Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
-
如果虚拟机在扩展栈无法申请到足够的内存空间,则抛出OutOfMemoryError异常
StackOverflowError比较好测试,测试代码如下:
package com.example.demo.jvm; /** * @Author: Wang Chong * @Date: 2019/9/22 19:09 * @Version: V1.0 */ public class StackOverflowTest { /** * 栈大小 */ private int stackLength = 1; /** * 递归压栈 */ public void stackLeak() { stackLength++; stackLeak(); } public static void mai