第二部分 自动内存管理机制
第二章 Java内存区域与内存区域异常
简述

  • 概述
  • 运行时数据区域
    • 程序计数器
    • Java虚拟机栈
    • 本地方法栈
    • Java堆
    • 方法区
    • 直接内存
  • HotSpot对象
    • 对象的创建
    • 对象的内存分布
    • 对象的访问定位
  • OutOfMemoryError异常
    • Java堆溢出
    • 虚拟机栈和本地方法栈溢出
    • 方法区溢出
    • 本机直接内存溢出

正文
1074438-20161212205708433-757048343.png

概述

Java虚拟机自动内存管理机制,能够让程序员不必为每个对象new/delete,不容易出现内存泄露和内存溢出。

运行时数据区域

根据Java虚拟机规范,运行时数据区域如下图所示:

1074438-20161212205734167-1290111217.png

程序计数器

  • 当前线程锁执行的字节码的行号指示器,用来指示下一条字节码指令。
  • 线程私有。
  • 唯一一个Java虚拟机规范没有规定任何OutOfMemoryError的区域

Java虚拟机栈

  • 描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等。局部变量表存放了编译器可知的各种基础数据类型、对象引用和returnAddress类型。当进入一个方法时,方法所需帧中分配的局部变量空间完全确定。
  • 线程私有。
  • 虚拟机规范定义了两种异常:StackOverFlow-线程请求深度大于允许值;OutOfMemoryError-无法申请到更多内存

本地方法栈

  • 与虚拟机栈类似,区别是虚拟机栈执行Java方法,而本地方法栈执行Native方法。
  • 线程私有
  • 抛出StackOverFlow和OutOfMemoryError两种异常

Java堆

  • Java堆是JVM管理内存中最大的一块,几乎所有的对象都在这里分配。是垃圾回收管理的主要区域,也被称为GC堆。可以处于物理上不连续的内存空间,只要逻辑上连续即可。
  • 线程共享
  • 抛出OutOfMemoryError

方法区

  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JVM规范描述为堆的一个逻辑部分,别名Non-Heap。习惯Hotspot上叫做永久带,原因是设计团队把GC分带收集扩展至方法区,使用永久带来实现罢了。
  • 运行时常量池是方法区的一部分,用于存放Class文件编译期生成的各种字面量和符号引用。常量池具有动态性,可以运行时期间将新的常量放入池中。用于存放JDK1.7中把字符串常量池移除。
  • 线程共享
  • 抛出OutOfMemoryError

直接内存

  • 直接内存不是JVM运行时数据区的一部分,但是也常被使用,例如NIO的DirectByteBuffer操作方式。也可导致OutOfMemoryError。

HotSpot对象

对象的创建

在语言层面,创建对象仅需要一个new关键字。

虚拟机层面,当遇到一个new时:

  • 首先检查常量池中能否定位到一个符号引用。符号引用所代表的类如果没有被加载,需要先加载、解析、初始化改类。
  • 然后,类加载同构后,在堆上为新对象分配内存。内存分配又分为“指针碰撞”和“空闲列表”两种方式,取决于Java堆是否规整,而Java堆是否规整又取决于垃圾回收器是否带有压缩整理功能。
  • 然后,将分配的内存空间初始化为零值(不包括对象头)。
  • 接着,对这个对象进行设置,比如是哪个类的实例,如何找到类元数据、对象哈希码、GC分代年龄信息等。这些对象信息放在对象头中。
  • 最后,一般情况下都会执行方法,按照程序员的意愿进行初始化。至此,虚拟机层面对象创建完成。

对象的内存分布

HotSpot中,对象的内存中存储布局可以划分为3块区域:对象头,实例数据和对齐填充。

  • 对象头

对象头包含两部分数据:

  • 自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,也称为Mark Word。

  • 类型指针,即对象指向它的元数据的指针,JVM通过这个指针确定这个对象是哪个类的实例。

  • 实例数据

也即程序代码中定义的各种类型的字段内容,包括父类继承下来的和子类中定义的。

  • 对齐填充

不是必然存在的,也没有特别含义,仅起占位符作用。由于HotSpot VM自动内存管理要求对象起始地址必须是8字节的倍数。因此当对象数据没对齐时,需要填充补全。

对象的访问定位

Java程序通过栈上的reference数据来操作堆上的具体对象。目前主流reference定位对象的方式包括以下两种:

  • 句柄方式

Java堆中会划分出来一块内存作为句柄池。reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体地址。

1074438-20161216154250964-814328329.png

  • 直接指针访问

Java堆对象中放置访问类型数据的相关信息,而reference存储的就是对象地址。

1074438-20161216154319354-286475820.png

直接指针访问方式的好处是速度更快,比句柄访问方式减少了一次指针定位时间开销。Sun HotSpot使用的这种方式

OutOfMemoryError异常

通过手动产生溢出的方式,加深对运行时数据区的理解

Java堆溢出

Java堆用于存放实例对象,因此制造溢出的思路是限制堆大小的情况下,不断生成对象进行填充,直至溢出。设置参数-Xms最小值,-Xmx最大值,当两个值相同时表示不可扩张。

另外,通过设置-XX:+HeapDumpOnOutOfMemoryError可以让JVM在内存溢出时Dump出当前的内存堆转储快照便于事后分析。

/**
 * -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }

    static class OOMObject {
    }
}

运行几秒种后输出下面的结果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid8328.hprof ...
Heap dump file created [2474990 bytes in 0.009 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.ArrayList.grow(Unknown Source)
    at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
    at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
    at java.util.ArrayList.add(Unknown Source)
    ...

1074438-20161216154342776-566822180.png.bmp
虚拟机栈和本地方法栈溢出

栈中以线程为单位,存放的方法调用的各类数据。HotSpot中并不区分虚拟机栈和本地方法栈,因此虽然存在设置本地方法栈的参数-Xoss,但是实际上无效。栈容量只由参数-Xss设定。JVM规范中定义了两种异常:

  • 如果线程请求的栈深度大于JVM允许的最大深度,抛出StackOverFlowError异常
  • 如果JVM在扩展栈空间时无法申请到足够的空间,抛出OutOfMemoryError异常

首先测试StackOverFlowError,代码如下:

/**
 * -Xss128k
 */
public class VmSOF {

    int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

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

输出:

stack length:981
Exception in thread "main" java.lang.StackOverflowError
    at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:11)
    at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
    at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
    at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
    ...

上述代码只会抛出StackOverFlowError异常,而调整-Xss参数变大变小只影响方法栈调用深度变多变少,而并不能产生出OutOfMemoryError。

通过分析,运行时区域中虚拟机栈和本地方法栈是线程隔离的,当栈空间一定时,支持的线程数量是一定的。因此OutOfMemoryError可以通过不断生成线程来制造出来。

测试OutOfMemoryError的代码:

/**
 * -Xss128k
 */
public class VmsOOM {
    public static void main(String[] args) {
        while (true) {
            Thread t = new Thread(new Unstoppable());
            t.start();
        }
    }

    static class Unstoppable implements Runnable {
        @Override
        public void run() {
            while (true);
        }
    }
}

结果如下

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Unknown Source)
    at edu.uestc.l08.VmsOOM.main(VmsOOM.java:15)

注:上述代码容易造成系统假死,需要慎重测试

方法区溢出

方法区用于存放Class的类型元数据,测试的思路是在限定方法区大小的情况下,产生大量的类去填充直至溢出。

本机测试使用JDK1.8,此版本不在使用永久带来实现方法区,有运行提示为证:

Java HotSpot(TM) Client VM warning: ignoring option PermSize=10m; support was removed in 8.0

Java HotSpot(TM) Client VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0

JDK1.8后使用称为元空间的MetaSpace来实现,因此JVM启动参数设置为-XX:MaxMetaspaceSize=10m -XX:MaxMetaspaceSize=10m,测试使用CGLib填充方法区,代码如下:

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * -XX:MaxMetaspaceSize=10m  -XX:MaxMetaspaceSize=10m
 */
public class MethodAreaOOM {
    
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
                    return arg3.invokeSuper(arg0, arg2);
                }
            });
            enhancer.create();
        }
    }
    
    static class OOMObject {
    }
}

输出为:

Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
    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 edu.uestc.l08.MethodAreaOOM.main(MethodAreaOOM.java:22)
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:413)
    at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
    ... 6 more
Caused by: java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    ... 11 more

另外,由于运行时常量池作为方法区中特殊的一块,也有存在OOM的可能。在JDK1.6及之前的版本中,通常使用String.intern()的例子来制造溢出。主要代码为:

while (true) {
    list.add(String.valueOf(i++).intern());
}

在JDK1.7之后,intern()方法被修改,不在复制实例,而是在常量首次出现时仅保留堆中对象的引用,从而上例比较难制造溢出。具体不在赘述,详情可参考:http://blog.csdn.net/seu_calvin/article/details/52291082

本机直接内存溢出

直接内存通过-XX:MaxDirectMemorySize指定,如果不指定则与堆大小相同。周总的例子如下:

import java.lang.reflect.Field;
import sun.misc.Unsafe;

/**
 * -Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {

    private static final long _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        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 edu.uestc.l08.DirectMemoryOOM.main(DirectMemoryOOM.java:17)

由本地内存导致的内存溢出有一个特征,就在Heap Dump中不会看到明显的异常。如果OOM后dump文件很小,而程序使用了NIO,则可以考虑是这方面原因。