在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,本文总结了若干实例来验证异常及发生的场景。

  下文代码的开头都注释了执行时所需要设置的虚拟机启动参数(注释中VM Args后面跟着的参数),如果使用控制台命令来执行程序,那直接跟在Java命令之后书写就可以。如果使用Eclipse IDE,则可以参考下图在Debug/Run页签中设置。本文的代码都是基于Sun公司的HotSpot虚拟机运行的,对于不同公司的不同版本的虚拟机,参数和程序运行的结果可能会有所差异。

1.Java堆溢出

  Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数据量到达最大堆的容量限制后就会产生内存溢出异常。如下代码清单中,限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前内存堆转储快照以便事后分析。

//代码清单1-1,Java堆内存溢出异常测试
/**
* 虚拟机参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM{
static class OOMObject{}
List<OOMObject> list = new ArrayList<OOMObject>();
while(true) {
list.add(new OOMObject());
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid10544.hprof ...
Heap dump file created [28023233 bytes in 0.083 secs]

  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),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

  以上是处理Java堆内存问题的简单思路,处理这些问题需要实战经验的积累。

2.虚拟机栈和本地方法栈溢出

  关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

  测试代码如下:

/**
* 代码清单2-1,虚拟机参数: -Xss128k
*/
public class QuickTest {
private int stackLength = 1;
public static void main(String[] args) throws Throwable{
QuickTest qu = new QuickTest();
try {
qu.stackLeak();
}catch(Throwable e) {
System.out.println("stack length:" + qu.stackLength);
throw e;
}
}
public void stackLeak() {
stackLength++;
stackLeak();
}
}
运行结果:
Exception in thread "main" stack length:992
java.lang.StackOverflowError
at sort.QuickTest.stackLeak(QuickTest.java:78)
at sort.QuickTest.stackLeak(QuickTest.java:79)

  如上实验是在单线程下测试,表明当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,如下代码所示。但是这样产生的内存溢出异常就和栈空间是否足够大并不存在任何联系,或者说,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

  其实原因不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈"瓜分"了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。

  这一点在开发多线程的应用时特别注意,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到问题所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说在大多数情况下)达到1000-2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64为虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。

3.运行时常量池溢出

  如果要向运行时常量池中添加内容,最简单的做法就是使用 string.intern( )这个Native方法。该方法的作用是:如果池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 string 对象;否厕,将此 string 对象包含的字符串添加到常最池中,并且返回此 string 对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容最,如代码清单3-1所示。

/**
* 代码清单3-1,VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public static void main(String[] args){
List<String> list = new ArrayList<String>();
int i = 0;
while(true) {
System.out.println("hello");
list.add(String.valueOf(i++).intern());
}
}

  对于如上的代码,书中的结果是:

Exception in thread "main" java.lang.OutOfMemoryError:PerGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

  解释是:从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是"PermGen space",说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。

  可是我自己同样的代码在自己电脑上实验的结果却是---》死循环O__O",这是什么情况,想一想,问题应该是出在环境上,我的JDK是1.8,作者当时的环境可能是低版本的JDK(1.6),而java6中,JVM字符串常量值使用了固定大小的内存区域(PermGen),java7和8字符串常量池在堆内存中,问题应是出在这里,通过调整虚拟机参数-Xms10m -Xmx10m,然后报OOM了,显示是堆内存溢出了,说明分析正确,结果如下。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.io.PrintStream.write(PrintStream.java:530)
at java.io.PrintStream.print(PrintStream.java:669)
at java.io.PrintStream.println(PrintStream.java:806)
at testPackage.Test.main(Test.java:89)

4.方法区溢出

  方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用 Java SE API 也可以动态产生类(如反射时的 GeneratedconstructorAccessor 和动态代理等),但操作起来比较麻烦。在代码清单4-1中,借助 CGLib 。直接操作字节码运行时,生成了大量的动态类。

/**
* 代码清单4-1,VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class Test{
public static void main(String[] args) {
while(true) {
System.out.println("hello");
Enhancer en = new Enhancer();
en.setSuperclass(OOMObject.class);
en.setUseCache(false);
en.setCallback(new MethodInterceptor(){
public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy)throws Throwable{
return proxy.invokeSuper(obj,args);
}
});
en.create();
}
}
static class OOMObject{};
}

  方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收掉,判定条件是非常苛刻的。在经常动态生成大量 class 的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了 GCLib 字节码增强外,常见的还有:大量 JSP 或动态产生 JSP 文件的应用( JSP 第一次运行时需要编译为 Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

5.总结

  总结到这里,明白了在一些基本场景中什么样的代码和操作可能导致内存溢出异常,虽然Java有垃圾回收机制,但内存溢出异常离我们其实并不遥远,本文也只是学习了一些导致内存溢出异常的常见操作,并未对出涉及到Java的垃圾收集机制,后续文章将总结Java为了避免内存溢出异常的出现做了哪些努力。

JVM读书笔记之OOM的更多相关文章

  1. java内存区域——深入理解JVM读书笔记

    本内容由<深入理解java虚拟机>的部分读书笔记整理而成,本读者计划连载. 通过如下图和文字介绍来了解几个运行时数据区的概念. 方法区:它是各个线程共享的区域,用于内存已被VM加载的类信息 ...

  2. JVM读书笔记之内存管理

    对于从事C.C++程序开发人员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”又是从事最基础工作的“劳动人民”--既拥有每一个对象的“所有权”,又负责每一个对象生命开始到终结的维护责任. 对于Ja ...

  3. JVM读书笔记

    1 概念 java virtual machine为java虚拟机,运行使用jdk中编译器编译的java程序. 2 JVM内存模型 程序计数器:线程私有.当前线程正在执行的行号指示器. Java虚拟机 ...

  4. 垃圾回收算法简单介绍——JVM读书笔记&lt;二&gt;

    垃圾回收的过程主要包含两部分:找出已死去的对象.移除已死去的对象. 确定哪些对象存活有两种方式:引用计数算法.可达性分析算法. 方案一:引用计数算法 给对象中加入一个引用计数器.每当有一个地方引用它时 ...

  5. JVM读书笔记PART3

    一.早期(编译器)优化 语法糖 c#和java的泛型截然不同看似相同,c#是真实的泛型 编译运行一直存在 List<string> 和List<int> 就完全是两个类 而Ja ...

  6. JVM读书笔记之垃圾收集与内存分配

    1 概述 说起垃圾收集( Garbage Collection , GC ) ,大部分人都把这项技术当做 Java 语言的伴生产物.事实上, GC 的历史远远比 Java 久远,1960 年诞生于 M ...

  7. 深入理解JVM读书笔记思维导图

    为了证明我已经啃完这本书然后买新书不用剁手...脑图画了8个钟,感觉整个人都不好了T_T 脑细胞不知道死了多少... 其实没吃透,估计若干年后要重新翻开来看...

  8. 深入理解Java虚拟机 -- 读书笔记(1):JVM运行时数据区域

    深入理解Java虚拟机 -- 读书笔记:JVM运行时数据区域 本文转载:http://blog.csdn.net/jubincn/article/details/8607790 本系列为<深入理 ...

  9. 《深入分析Java Web技术内幕》读书笔记之JVM内存管理

    今天看JVM的过程中收获颇丰,但一想到这些学习心得将来可能被遗忘,便一阵恐慌,自觉得以后要开始坚持做读书笔记了. 操作系统层面的内存管理 物理内存是一切内存管理的基础,Java中使用的内存和应用程序的 ...

随机推荐

  1. 招聘ETL开发工程师

    上班地点徐汇 本科以上学历 3年以上ETL开发经验熟悉Oracle数据库,精通PL  SQL开发与优化,熟悉Vertica或者GreenPlum库优先 熟悉数据库性能优化,有海量数据处理经验优先 自荐 ...

  2. 探索微信小程序之路

    记录一下每日的知识点,时不时温习一下. 视图与渲染对于页面中的数据,以json的方式存放在js文件的data中 判断的使用: <view wx:if='{{true}}'> 为真时显示 & ...

  3. restful状态码常用

    在进行后端接口API封装的过程中,需要考虑各种错误信息的输出.一般情况下,根据相应问题输出适合的HTTP状态码,可以方便前端快速定位错误,减少沟通成本. HTTP状态码有很多,每个都有对应的含义,下面 ...

  4. IMDb、烂番茄、MTC、各种电影行业评分名字整理

    这篇不是技术文章,就是对总是看到但是不知道具体是什么的一些电影名词.评分.来源,学习一下. IMDb 互联网电影资料库(Internet Movie Database,简称IMDb)是一个关于电影演员 ...

  5. [swarthmore cs75] Lab 1 — OCaml Tree Programming

    课程回顾 Swarthmore学院16年开的编译系统课,总共10次大作业.本随笔记录了相关的课堂笔记以及第2大次作业. 比较两个lists的逻辑: let rec cmp l ll = match ( ...

  6. pageHelper的使用步骤,省略sql语句中的limit

    1.引架包.注意版本问题 <dependency> <groupId>com.github.pagehelper</groupId> <artifactId& ...

  7. mysql实现多实例

    > mariadb安装    yum install mariadb-server > 创建相关目录,及设置权限    mkdir /mysqldb; mkdir /mysqldb/{33 ...

  8. 初见SDN

    软件定义网络(Software Defined Network, SDN ),是一种新型网络架构. SDN=OpenFlow:因为Openflow是大多数人唯一看得到的具体化的SDN的实现形式(实际上 ...

  9. js-图片轮播(极简)

    <!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" ...

  10. Node.js 开发指南

    1.Node.js 简介 Node.js 其实就是借助谷歌的 V8 引擎,将桌面端的 js 带到了服务器端,它的出现我将其归结为两点: V8 引擎的出色: js 异步 io 与事件驱动给服务器带来极高 ...