一、深入理解JAVA虚拟机之内存初探

一、java内存区域划分

《一、深入理解JAVA虚拟机之内存初探》

1、程序计数器

程序计数器是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。每个线程有自己独立的程序计数器,所以该内存区域是线程私有的。

如果线程正在执行的是Java方法,那么这个计数器的值就是正在执行的虚拟机字节码指令的 地址;如果正在执行的是Native方法,这个计数器值为空( undefined) 。此内存区域是唯一 一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2、虚拟机栈

虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个 方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直到执行完成的过程,就是栈帧在虚拟机中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、 long、double)、对象引用和returnAddress类型( 指向了一条字节码指令的地址) 。 其中64位长度的long和double类型的数据会占用2个局部变量空间( slot) ,其余的数据类型 占1个。局部变量表所需的内存空间在编译期间分配完成,当进入一个方法时,这个方法需要 在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

如果线程请求栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;无法申请 到内存抛出OutOfMemoryError异常。

3、本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟 机执行java方法,而本地栈则为虚拟机使用到的Native方法服务。

4、Java堆

Java堆是线程共享的,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”。由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor空间、To Survivor空间等。

在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照 可扩展来实现的( 通过-Xmx和-Xms控制) 。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展,将会抛出OutOfMemoryError。

5、方法区(永久代)

线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码 等数据。在逻辑上是堆的一部分,但有一个名字是“非堆”。

这区域的内存回收目标主要是针对常量池的回收和对类型的卸载!

在jdk1.7中,已经把字符串常量池移出。

6、运行时常量池

他是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有 一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

二、对象探秘

1、对象的创建

虚拟机遇到一个new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符 号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那 必须先执行相应的类加载过程。接下来JVM将为新生对象分配内存。

如果Java堆是规整的( 所有用过的内存在一边,未使用的在另一边,维护麻烦) ,那么将使 用“指针碰撞”的分配方式,否则使用“空闲列表”的分配方式。Java堆是否规整由采用的垃圾收 集器是否带有压缩整理功能决定。

但是内存的分配是同步的,如果一个线程刚分配一个对象内存,但是还没有修改指针所指向 的位置,那么另一个线程分配对象的时候可能就出错了。解决方法有两个,一是对分配内存 空间的动作进行同步处理( CAS方式) 。另一种是把内存分配的动作按照线程划分在不同的 空间进行,每个线程在java堆中预分配一小块内存,称为本地线程分配缓冲( TLAB) 。只有 TLAB用完并分配新的TLAB时,才需要同步。JVM是否开启TLAB功能,可通过-XX:+/- UseTLAB参数来设定。

内存分配完之后,初始化零值( 不包括对象头) ,如果使用TLAB,这一工作过程也可以提前 至TLAB分配时进行。

接下来,JVM对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元 数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中,根 据JVM当前运行状态不同,如是否启用偏向锁等,对象头会有不同的设置方式。

执行完new指令后接着执行方法,把对象按照程序员的意愿进行初始化,这样一个对象就初始化完成了。

2、对象的内存布局

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

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据( 哈希 码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的 存储官方称为Mark Word) ,另一部分是类型指针( 即对象指向它的类元数据的指针,JVM通 过这个指针来确定这个对象是哪个类的实例) 。

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍(对象的大小必须是8字节的整数倍),不满8 个字节的时候占位。

3、对象的访问定位

Java程序需要通过栈上的Reference数据来操作堆上的具体对象。Reference访问对象 的方式目前主流的有两种:句柄和直接指针。

如果直接使用句柄访问,java堆中将会划分出一块内存来作为句柄池,reference中存储 的是对象的句柄地址,而句柄中包含了对象数据与类型数据各自的具体地址信息,如下 图所示。

《一、深入理解JAVA虚拟机之内存初探》

如果使用直接指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相 关信息,而reference中存储的直接就是对象地址,如下图所示。

《一、深入理解JAVA虚拟机之内存初探》

区别:

使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。

HotSpot虚拟机使用的是直接指针访问的方式。

三、OutOfMemoryError异常

1、java堆异常

/** * VM Args: -Xms20m 堆的最小值 * -Xmx20m 堆的最大值 * -XX:+HeapDumpOnOutOfMemoryError 以让虚拟机在出现内存溢出异常时,Dump当前的内 * 存堆转储快照 */
public class HeapOOM {

    public static void main(String[] args) {

        List<OOMObject> list = new ArrayList<>();
        while(true){
            list.add(new OOMObject());
        }
    }
}

运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid4400.hprof ...
Heap dump file created [28248158 bytes in 0.207 secs]

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

由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽然-Xoss 参数( 设置本地方法栈大小) 存在,但实际上是没有效果的,栈容量只由-Xss参数设置。

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

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

这两种异常其实存在着一些互相重叠的地方。在单个线程下,无论是由于栈 帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是 StackOverflowError异常。如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常。

/** * VM Args: -Xss128k 栈大小 */
public class JavaVMStack {

    private int count;
    public void stackLeak(){
        count++;
        stackLeak();
    }
    public static void main(String[] args) {
        JavaVMStack javaVMStack = new JavaVMStack();
        try {
            javaVMStack.stackLeak();
        } catch (Throwable e) {
            System.out.println(javaVMStack.count);
            e.printStackTrace();
        }
    }
}
运行结果
983
java.lang.StackOverflowError
    at com.bw.oom.test.JavaVMStack.stackLeak(JavaVMStack.java:10)
    at com.bw.oom.test.JavaVMStack.stackLeak(JavaVMStack.java:11)

3、方法区和运行时常量池溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

使用CGLib直接操作字节码运行时生成了大量的动态类,可以填满方法区,直至溢出。

public class JavaMethodAreaOOM {
    public static void main(final String[] args) {
        while(true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects,
                                    MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, args);
                }
            });
            enhancer.create();
        }
    } 
}
运行结果:
Caused by: java.lang.OutOfMemoryError: PermGen space
    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/wangbing25307/article/details/81781774
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞