Java内存区域与内存溢出异常

  《深入理解Java虚拟机》第2章读书笔记与实验记录。

1、对象创建内存分配方式

  • 指针碰撞: Java堆内存绝对规整,使用指针作为分界点指示器
  • 空闲列表: 已使用的内存的空闲内存相互交错,虚拟机需要维护一个列表,记录哪些内存可用

2、对象内存布局

HotSpot虚拟机中对象内存布局可以分为三块区域:对象头、实例数据、对齐数据

3、对象访问定位

  • 句柄访问:Java堆划分出一块内存区域作为句柄池,句柄中包含了实例数据和类型数据的具体地址信息,reference则存储的是对象的句柄地址,如下图所示:
    通过句柄访问对象.png

优势:reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象时普遍的行为)时只改变句柄中实例数据指针,reference无需更改。

  • 直接指针访问:reference中存储的直接是对象地址,Java堆中对象的布局必须考虑如何存放访问类型数据相关信息,如下图所示:
    通过直接指针访问对象.png

    优势:访问速度快,节省了一次指针定位的开销,由于对象的访问在java中非常频繁,因此这类开销极少成多后也是一项非常可观的执行成本。Sun HotSpot也是使用该种方式进行对象访问的。

4、实战:OutOfMemoryError异常

4.1Java堆溢出

虚拟机参数配置: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError

代码清单:Java堆内存溢出异常测试

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_pid10816.hprof ...
Heap dump file created [28348501 bytes in 0.080 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:265)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
    at java.util.ArrayList.add(ArrayList.java:462)
    at com.mpoom.jvm.HeapOOM.main(HeapOOM.java:21)

通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析:


使用Eclipse Memory Analyzer打开的堆转储快照文件.png
4.2虚拟机栈和本地方法栈溢出

  在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。
关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

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

虚拟机参数配置:-Xss128K

代码清单:虚拟机栈和本地方法栈OOM测试(仅作为第1点测试程序)

public class JavaVMStackSOF {
    private int stackLength = 1;

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

运行结果:

stack length:19211
Exception in thread "main" java.lang.StackOverflowError
    at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
    at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
    at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
    at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
    at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
......

  实验结果表明:在单个线程下,无论是由于栈帧太大还是由于虚拟机容量太小,当内存无法分配的时候,虚拟机都抛出的是StackOverflowError异常。

如果测试不局限于单线程,通过不断建立线程倒是可以产生内存溢出异常

虚拟机参数配置:-Xss2M

代码清单: 创建线程导致内存溢出异常

public class JavaVMStackOOM {

    private void dontStop() {
        while(true) {}
    }
    public void stackLeakByThread() {
        while(true) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

注意:在windows平台上运行上述代码有风险,会导致操作系统假死(做好重启的准备— _ —)。因为在windows在平台的虚拟机中,Java的线程是映射到操作系统的内核线程上的。
本次实验由于系统假死,没有查看到,下面的实验结果是从别处获取的:

Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread
4.3方法区和运行时常量池溢出

  由于运行时常量池属于方法区的与部分,两个区域的溢出测试放在一起。

4.3.1运行时常量池溢出

  String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等
于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包
含的字符串添加到常量池中,并且返回此String对象的引用。

虚拟机参数配置:-XX:PermSize=10M-XX:MaxPermSize=10M
JDK:1.6

代码清单:运行时常量池导致的内存溢出异常

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用List保持着常量池引用, 避免Full GC回收常量池行为
        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)
    at com.mpoom.jvm.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。
更改JDK版本为1.8,代码中的while循环将一直执行下去。关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响,代码如下:

JDK:1.8

代码清单:String.intern()返回引用的测试

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        String str1=new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern()==str1);

        String str2=new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern()==str2);
    }
}

运行结果

true
false

这段代码在JDK 1.6中运行,会得到两个false,而在JDK 1.7中运行,会得到一个true和一个false。 产生差异的原因是:在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。 而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。 对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

4.3.2方法区溢出

  方法区用于存放Class的相关信息,如类名、 访问修饰符、 常量池、 字段描述、 方法描等。 对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。这里借助CGLib[1]直接操作字节码运行时生成了大量的动态类。

虚拟机参数配置:-XX:PermSize=10M-XX:MaxPermSize=10M
JDK:1.6

Maven依赖

<dependency>
     <groupId>cglib</grupId>
     <artifactId>cglib</artifactId>
     <version>2.1_3</version>
</dependency>

代码清单:借助CGLib使方法区出现内存溢出异常

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:631)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
    ... 8 more

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。 在经常动态生成大量Class的应用中,需要特别注意类的回收状况。
注意:上述在JDK1.6环境下实验的,JDK1.8下while循环将一直运行下去。

4.4本机直接内存溢出

  DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。下面代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。 因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。

虚拟机参数配置:-Xmx20M -XX:MaxDirectMemorySize=10M
JDK:1.8

代码清单:使用unsafe分配本机内存

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 com.mpoom.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:21)

总结:本章主要学习了虚拟机的内存是如何划分的,哪部分区域、什么样的代码和操作会导致内存溢出异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335