《深入理解Java虚拟机》读书笔记(二)

BF,技术帖,Java Virtual Machine 2017-05-07

第二部分 自动内存管理机制
第二章 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,则可以考虑是这方面原因。


本文由 BF 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论

bst g22 jinniu lilai opebet orange88 vinbet xbet yuebo zunlong shijiebei bet007 hg0088 ju111 letiantang m88 mayaba qg777 qianyiguoji sbf777 tengbohui tlc ule weilianxier waiweitouzhu xingfayule xinhaotiandi yinheyule youfayule zhongying 2018shijiebei w88 18luck 188bet beplay manbet 12bet 95zz shenbo weide1946 ca88 88bifa aomenxinpujing betway bodog bt365 bwin tongbao vwin weinisiren 88jt fenghuangyule hongyunguoji 918botiantang huanyayule jianada28 jixiangfang libo long8 hongzuyishi zuqiutouzhu