JVM探秘:内存溢出
本系列笔记主要基于《深入理解Java虚拟机:JVM高级特性与最佳实践 第2版》,是这本书的读书笔记。
在 Java 虚拟机内存区域中,除了程序计数器外,其他几个内存区域都可能会发生OutOfMemoryError,这次通过一些代码来验证虚拟机各个内存区域存储的内容。
在实际工作中遇到内存溢出异常时,需要做到能根据异常信息快速判断是哪个内存区域的溢出,知道什么样的代码会导致这些区域内存溢出,并且知道出现内存溢出后如何处理。
Java堆溢出
Java 堆用于存储对象实例,只要不断的扩展对象,并且保证 GC Roots 到对象有可达路径来避免垃圾回收,那么对象数量到达堆的最大容量后就会发生内存溢出异常。
模拟堆内存溢出
以下代码会把堆大小限制在20M且不可扩展(将最小参数-Xms
和最大参数-Xmx
设为相同就会避免自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError
可以让虚拟机在发生内存溢出时Dump出内存快照用来分析。
参数 | 说明 |
---|---|
-XX:+HeapDumpOnOutOfMemoryError | 内存溢出时自动导出内存快照 |
-XX:HeapDumpPath=E:/dumps/ | 导出内存快照时保存的路径 |
/**
* Java堆内存溢出异常
* VM args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
* -Xms和-Xmx设为相同值避免堆内存自动扩展,
* -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在发生OOM时Dump出内存快照
* Run With JDK 1.8
* */
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args){
List<OOMObject> list = new ArrayList<>();
while(true){
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1344.hprof ...
Heap dump file created [29068691 bytes in 0.108 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 test.oom.HeapOOM.main(HeapOOM.java:21)
可以从异常信息中看到,OOM异常发生在“main”线程,发生的内存区域是“Java heap space”。
通过IntelliJ IDEA运行的话,可以点击Edit Configurations
配置VM参数,生成的堆Dump快照文件为hprof后缀,存放在Working directory
配置对应的目录下,如下图:
堆内存溢出分析
要分析 Java 堆的内存溢出,首先通过快照分析工具(如Java VisualVM)对 Dump 出来的的快照进行分析,确认内存中的对象是否是必要的。如果是不必要的而没有垃圾回收掉,则发生的是内存泄漏(Memory Leak);如果都是必要的,则是内存溢出(Memory Overflow)。
如果是内存泄漏,通过工具进一步查看对象实例到 GC Roots 的引用链,找到泄露对象是通过什么路径与 GC Roots 相关联导致垃圾收集器无法回收它们。根据泄露对象的类型信息和到 GC Roots 的引用链,就可以定位到泄露代码的位置。
如果是内存溢出,也就是说这些对象还都必须存活,那么就检查堆内存的大小参数(-Xms与-Xmx)与物理内存比较还是否可以调大,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
打开 JDK 自带的分析工具 Java VisualVM(bin目录下的jvisualvm.exe),点击文件->装入
选择堆快照java_pid1344.hprof
文件,打开后显示的是概述信息,这里会显示快照的一些基本信息、环境属性以及线程信息。
然后点击类
,打开后如下图:
从上图可以看到,数量最多的且占用内存最大的对象是OOMObject类型的实例,OOMObject类型共有实例810,326
个,大小总共12,965,216
个字节(byte),而这些对象都是在while循环中new出来加入到List中的,都是应该存活的对象,也就是说发生的OOM是内存溢出而不是内存泄漏。
然后在OOMObject
的记录上右键点击在实例试图中显示
,则会打开实例视图,见下图:
可以看到其中一个OOMObject
对象的引用链,它被一个Object[]
数组中的元素引用,我们都知道ArrayList
是基于数组实现的,而这个Object[]
数组对象就是一个 GC Root,它的内存地址是578296
。
虚拟机栈和本地方法栈溢出
在内存区域那篇文章讲到过,HotSpot虚拟机把本地方法栈和虚拟机栈合二为一了,栈容量由-Xss
参数设置。关于虚拟机栈和本地方法栈,虚拟机规范规定了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分为了两种,看似严谨实际上有相互重叠的地方,当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,本质上只是对同一个问题的不同描述而已。
有两种方法会抛出StackOverflowError异常,一种是通过-Xss
参数减小栈内存容量;一种是定义大量局部变量,从而增大此方法帧中的局部变量表的长度。以下代码是第一种:
/**
* Java栈内存溢出异常
* 通过减小栈内存容量抛出StackOverflowError
* VM args: -Xss128K
* Run With JDK 1.8
* */
public class StackOOM {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
StackOOM oom = new StackOOM();
try {
oom.stackLeak();
}catch(Throwable e){
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}
运行结果:
stack length: 998
Exception in thread "main" java.lang.StackOverflowError
at com.cellei.outofmemory.StackOOM.stackLeak(StackOOM.java:15)
at com.cellei.outofmemory.StackOOM.stackLeak(StackOOM.java:16)
at com.cellei.outofmemory.StackOOM.stackLeak(StackOOM.java:16)
...
at com.cellei.outofmemory.StackOOM.main(StackOOM.java:22)
实验结果表明,不论是减小栈容量大小还是增加栈帧大小,当内存无法分配时虚拟机抛出的都是StackOverflowError异常。
如果不限于单线程,不断的建立线程的情况下倒是会抛出OutOfMemoryError异常,但跟栈空间是否足够大没有直接关系,而且栈是线程私有的内存区域。这种情况下,每个线程的栈分配的内存越大,就越容易产生内存溢出异常。
虚拟机提供了参数来控制堆内存和方法区的最大容量,物理内存减去堆内存最大值,再减去方法区的最大值,程序计数器消耗内存很小忽略不计,剩下的就被虚拟机栈和本地方法栈瓜分了。所以每个线程分配到的栈容量越大,则可以建立的线程数量越少,建立线程时就越容易把剩下的内存耗尽。如果建立过多导致了内存溢出,在不能减少线程数的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
方法区和运行时常量池溢出
在JDK1.6及之前,运行时常量池是方法区的一部分,且方法区还使用永久代实现,那时候可以在限制永久代大小的情况下,循环调用String.intern()
方法造成运行时常量池溢出而导致方法区溢出。使用参数-XX:PermSize
和-XX:MaxPermSize
来限制永久代也就是方法区的大小。String.intern()
方法是一个Native方法,作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表常量池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中。
在JDK1.7的时候常量池挪到了堆内存中,到了JDK1.8就干脆取消了永久代,取而代之的是元空间(MetaSpace),且元空间是位于本地内存而不是虚拟机内存。
以下代码,在JDK1.6及之前的版本中会产生内存溢出:
/**
* 要求运行在 JDK1.6 或以前
* 导致常量池溢出从而产生永久代溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* Run With JDK 1.6
*/
public class ConstantPoolOverflowTest
{
public static void main(String[] args)
{
List<String> list = new ArrayList<String>();
int i = 0;
while (true)
{
list.add(String.valueOf(i++).intern());
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
...
可见运行结果提示了PermGen space
,表明是那个版本的永久代也就是方法区溢出。
既然JDK1.7及之后常量池挪到了 Java 堆中,在那之后的版本如何产生方法区溢出呢?既然方法区用于存放类的相关信息,基本思路就是在运行时产生大量的类去填充方法区,直到溢出。可以使用 JDK 的动态代理,也可以使用第三方库比如 CGLib 实现。
以下代码使用CGLib库,在运行时不断的产生类导致方法区溢出。由于JDK1.8的方法区改为了使用元空间实现,所以可以使用参数-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
限制方法区大小。
/**
* 限制元空间大小后
* 使用CGLib运行时产生类,导致元空间也就是方法区溢出
* VM Args:-XX:MetaspaceSize=8M -XX:MaxMetaspaceSize=28M
* Run With JDK 1.8
*/
public class MethodAreaOOM {
static class OOMObject{
}
public static void main(String[] args){
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at com.cellei.oom.MethodAreaOOM.main(MethodAreaOOM.java:29)
可见异常信息提示Metaspace
,就是说元空间(方法区)内存溢出了。方法区溢出也是一种比较常见的溢出,一个类要被垃圾收集器回收,判定条件是比较苛刻的。在经常动态产生大量 Class 的应用中,要特别注意类的回收情况。
本机内存直接溢出
DirectMemory容量可以通过参数-XX:MaxDirectMemorySize
指定,如果不指定,则默认与 Java 堆最大值(-Xmx)一样。通过反射获取Unsafe
实例进行内存分配,allocateMemory()
方法会真正申请分配内存。
/**
* 不断的申请内存,导致本机内存溢出
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* Run With JDK 1.8
* */
public class DirectMemoryOOM {
private static final int _1M = 1024 * 1024;
public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1M);
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.cellei.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)
由DirectMemory导致的内存溢出,有一个特点就是Heap Dump文件中不会看到明显异常,如果Dump文件非常小,又直接间接使用了NIO,则有可能是这方面的原因。
本文代码的 Github Repo 地址:https://github.com/cellei/JVM-Practice
JVM探秘:内存溢出的更多相关文章
- JVM:内存溢出OOM
JVM:内存溢出OOM 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 经典错误 JVM 中常见的两个 OOM 错误 StackoverflowError:栈溢出 ...
- Java JVM:内存溢出(栈溢出,堆溢出,持久代溢出以及 nable to create native thread)
转载自https://github.com/pzxwhc/MineKnowContainer/issues/25 包括:1. 栈溢出(StackOverflowError)2. 堆溢出(OutOfMe ...
- (转)JVM各种内存溢出是否产生dump
对于java的内存溢出,如果配置-XX:+HeapDumpOnOutOfMemoryError,很明确的知道堆内存溢出时会生成dump文件.但永久代内存溢出不明确是否会生成,今天来做一个实验: 永久代 ...
- Java8虚拟机(JVM)内存溢出实战
前言 相信很多JAVA中高级的同学在面试的时候会经常碰到一个面试题 你是如何在工作中对JVM调优和排查定位问题的? 事实上,如果用户量不大的情况下,在你的代码还算正常的情况下,在工作中除非真正碰到与J ...
- [JVM教程与调优] 了解JVM 堆内存溢出以及非堆内存溢出
在上一章中我们介绍了JVM运行时参数以及jstat指令相关内容:[JVM教程与调优] 什么是JVM运行时参数?.下面我们来介绍一下jmap+MAT内存溢出. 首先我们来介绍一下下JVM的内存结构. J ...
- 浅析软件测试人员如何对JVM进行内存溢出检测
一.什么是JVM,检测JVM的意义 JVM是java virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各 ...
- 对jvm虚拟机 内存溢出的思考
java内存溢出:当新产生对象时,新生代空间不够,导致无法申请到足够的空间,报内存溢出 内存泄漏:一些静态集合,静态常量没有被gc清理,越来越多,占用内存,最后导致无法申请到新的空间
- JVM虚拟机内存溢出垃圾收集及类加载机制总结
1.Java内存区域与内存溢出异常 虚拟机栈:为虚拟机执行Java方法服务 本地方法栈:为虚拟机使用到的native方法服务. Java堆:是Java虚拟机所管理的内存中最大的一块,被所有线程共享的一 ...
- JVM 发生内存溢出的 8 种原因、及解决办法
阅读本文大概需要 2.3 分钟. 出处:割肉机 cnblogs.com/williamjie/p/11164572.html Java 堆空间 GC 开销超过限制 请求的数组大小超过虚拟机限制 Per ...
- 性能分析 | JVM发生内存溢出的8种原因及解决办法
推荐阅读:史上最详细JVM与性能优化知识点综合整理 1.Java 堆空间 2.GC 开销超过限制 3.请求的数组大小超过虚拟机限制 4.Perm gen 空间 5.Metaspace 6.无法新建本机 ...
随机推荐
- php 处理 大并发
小谈php处理 大并发 大流量 大存储 一.判断大型网站的标准: 1.pv(page views)网页的浏览量: 概念:一个网站所有的页面,在24小时内被访问的总的次数.千万级别,百万级别 2. uv ...
- 第二次 C++作业
1.为什么要用函数? 函数是相对独立的,经常使用的功能抽象化表现形式,函数的优势在于,编写之后可以被重复使用,使用时可以只关心函数的功能和使用方法而不必关心函数的具体实现,这样可以有利于代码重用,可以 ...
- 一文告诉你Adam、AdamW、Amsgrad区别和联系 重点
**序言:**Adam自2014年出现之后,一直是受人追捧的参数训练神器,但最近越来越多的文章指出:Adam存在很多问题,效果甚至没有简单的SGD + Momentum好.因此,出现了很多改进的版本, ...
- SQLSTATE[HY000] [2002] 错误
http://www.thinkphp.cn/topic/36194.html 使用tp框架 3.2.3 ,在windows上跑的时候没有任何问题,但是部署到linux系统和是哪个,就会报这个错,不知 ...
- 微信小程序 view中的image水平垂直居中
当 display: flex 配合 justify-content: center 使用时可以让view水平居中 而配合 align-items: center 用时可以实现垂直居中效果 .card ...
- 5分钟了解为什么学习Go
1.什么是Go语言? Google开源 编译型语言 21世纪的C语言(主流编程语言都是单线程环境下发布的) 2.Go语言的特点? 简单易学习(类似python学习难度,自带格式化) 开发效率高 执行性 ...
- Python--day69--pythonDjango终端打印SQL语句、在Python脚本中调用Django环境
Django终端打印SQL语句 在Django项目的settings.py文件中,在最后复制粘贴如下代码: LOGGING = { 'version': 1, 'disable_existing_lo ...
- HDU 2602Bone Collector 01背包问题
题意:给出一个t代表有t组数据,然后给出n,n代表有n种石头,v代表旅行者的背包容量,然后给出n种石头的价值和容量大小,求能带走的最大价值 思路:01背包问题,每种石头只有拿与不拿两种状态.(其实我是 ...
- python模块之包
包:将解决一类问题的模块放在同一目录下就形成了一个包 为了更好的了解包,我们就模拟创建一个包 import os os.makedirs('glance/api') os.makedirs('glan ...
- P1002 Hello,World!
题目描述 输出"Hello Wolrd!". 输入格式 无. 输出格式 输出一行"Hello World!". 样例输入 无. 样例输出 Hello World ...