在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(也就是大家常说的OOM)异常的可能,大概分类有以下几种:
- Java堆溢出
- 虚拟机栈和本地方法栈溢出
- 运行时常量池溢出
- 方法区溢出
- 本机直接内存溢出
Java堆溢出
Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。
下面代码中限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDump OnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。
import java.util.ArrayList;
import java.util.List;
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author jianshi.dlw
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid4248.hprof ...
Heap dump file created [10878947 bytes in 0.137 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2760)
at java.util.Arrays.copyOf(Arrays.java:2734)
at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
at java.util.ArrayList.add(ArrayList.java:351)
at HeapOOM.main(HeapOOM.java:14)
Java堆内存的OOM异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
虚拟机栈和本地方法栈溢出
由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分成两种情况看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
如果将实验范围限制于单线程中的操作,尝试了下面两种方法均无法让虚拟机产生OutOfMemoryError异常,尝试的结果都是获得StackOverflowError异常,测试代码如下所示。
使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小。
定义了大量的本地变量,增加此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的栈深度相应缩小。
/**
* VM Args:-Xss128k
* @author jianshi.dlw
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行结果:
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
.......
实验结果表明:在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,如下面的代码所示。但是,这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
在开发多线程应用的时候特别注意,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到问题的所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说大多数情况下)达到1000~2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。如果没有这方面的经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。
/**
* VM Args:-Xss128k
* @author jianshi.dlw
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
注意:特别提示一下,如果需要尝试运行上面这段代码,记得要先保存当前的工作,由于在Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上的,所以上述代码执行时有较大的风险,可能会导致操作系统假死。
运行结果:
Exception in thread "main" java.lang.
OutOfMemoryError: unable to create new native thread
运行时常量池溢出
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量,如代码所示。
import java.util.ArrayList;
import java.util.List;
/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* @author jianshi.dlw
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer范围内足够产生OOM了
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)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:16)
从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。
方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等),但在本次实验中操作起来比较麻烦。在下面代码中,借助CGLib直接操作字节码运行时,生成了大量的动态类。
值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前的很多主流框架,如Spring和Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author jianshi.dlw
*/
public class JavaMethodAreaOOM {
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 obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
} static class OOMObject {
}
}
运行结果:
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8 more
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收掉,判定条件是非常苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了GCLib字节码增强外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。
本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样。下面的代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例并进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* @author jianshi.dlw
*/
public class DirectMemoryOOM {
private static final int _1MB = 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(_1MB);
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)
分享到:
相关推荐
JVM一直是java知识里面进阶阶段的重要部分,如果希望在java领域研究的更深入,则JVM则是如论如何也避开不了的话题,本系列试图通过简洁易读的方式,讲解JVM必要的知识点 本资源是一个合集,需要的朋友可以自行下载...
JVM学习笔记
内容包含: 1、JAVA SE 2、MySQL + JDBC 3、JAVA WEB 4、Mybatis 5、SSM 6、大前端 7、Vue 8、SpringBoot 9、Spring Security ...12、JVM 13、JUC 14、Git 15、Linux 16、Redis 17、ElasticSearch 18、Docker
JAVA学习笔记最新ppt版1 JAVA 是一种面向对象的程序语言,具有更高的跨平台可能性。它是 Sun 公司 GreenProject 中撰写 Star7 应用程序的一个程序语言,由 James Gosling 创造。Java 的特性包括简单、面向对象、...
从本篇开始的系列博文,记录本人的JVM深入学习总结,其中结合了本人自己的一些经验,也参考了一些书籍和网络资源,然后根据自己的理解写出这些博文。如有版权问题,请伊妹儿我:)谨以此系列博文分享给我的朋友们。
大家好,我是韩数,这里收录了我的大多数学习笔记,包括Nginx,JAVA,Springboot,SpringCloud,Docker等后端常用的技术,大多数文章都经过markdown精心排版,总之八个字: 思路清奇,写法风骚 后续预告: Spark ...
java筑基(基础)面试专题系列(二):并发+Netty+JVM JVM JVM与性能优化知识点整理 JVM和性能优化学习思维笔记.xmind kafka知识导图笔记.xmind MQ MySQL优化学习思维笔记.xmind MySQL性能优化的21个最佳实践 Redis
Java架构面试专题及架构学习笔记 Dubbo服务框架面试专题及答案整理文档 BAT面试常问80题 23种设计模式知识要点整理 面试必备—API接口安全 面试必备—服务器推送技术 面试必备—匠心独运手写MyBatis框架 面试必备—...
学习笔记(持续更新中) 所有文章均同步发布到微信公众号【JavaRobot】,关注微信公众号,及时得到文章推送,谢谢支持。 说明:如无特别说明,所有代码都基于JDK8 JavaSE(Java基础) Java Core 关键字 synchronized...
学习操作系统也是一波三折,中途放弃了好几次,我主攻Java方向,最后找到的方法是结合JVM进行学习,所以文章中对操作系统的理论方面不会很深入,更多的是结合Java语言的特点去加深理解。 算法与数据结构 计算机网络
[Java] 对象类型转换和运算符 instanceof 学习笔记 欢迎大家收看 把简单的问题变复杂系列 : ) 一、类型转换使用的情景: 在需要将一个继承链下把一个类的对象转换为另一个类的对象(注意:这里强调转换的是对象...
MongoDB学习笔记.docx mybatis原理.docx MyBatis面试专题.docx MyBatis面试专题及答案.pdf Mybatis面试题(含答案).pdf MySQL性能优化的21个最佳实践.pdf mysql面试专题.docx MySQL面试题(含答案).pdf Netty面试...
本文主要是我最近复习Java基础原理过程中写的Java基础学习总结。...本系列技术文章首发于我的个人博客: 更多关于Java后端学习的内容请到我的CSDN博客上查看: Java基础学习总结 每部分内容会重点写一些常见
此项目是利用业余时间,对一些技术知识点进行整理,用来记录个人学习笔记。这个项目和 项目的不同在于 是用来动手实践,对于一些技术的实际搭建和造轮子的项目,正所谓实践出真知。相关的源码都会在上边。两个项目...
项目起源于自学Java过程中所做的300+系列笔记,点点滴滴的积累成就了本项目的体系结构... 随着我学习的不断深入,项目也在不断更新、完善中... 非常欢迎大家参与共建~~ 关于作者 就读于华南理工大学电子与信息学院 ...
双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题。不建议使用) 静态内部类式(线程安全,调用效率高。但是可以延时加载) 枚举单例(线程安全,调用效率高,不能延时加载) : 简单工厂模式:用来生产同一...
直流笔记引言Dreamcats的公众号:面经助手包含面经,算法题和知识点,持续维护小程序涉及的源文件->秋招经历校招Java职位面试经历面经汇总知识体系刷题系列 :个人建议,如果时间不充足的情况下,将CS-Notes的...
直流笔记 引言 Dreamcats的公众号: 常用网站 :一种新闻聚合的产品,个人感觉还不错,闲时可以看一下新闻,可选择订阅哦 面经助手 包含面经和算法题,持续维护 秋招经历 校招Java职位面试经历 面经汇总 知识...