JAVA虚拟机

JAVA运行时数据区对象创建原理

2018-09-22  本文已影响0人  _fan凡

Java与C++之间有一堵由内存动态分配和自动垃圾回收技术围城的墙,墙里面的人想出来,墙外面的人想出去。
关于虚拟机的内存区域和内存异常,分两个部分,第一个部分是运行时数据区及其对应的异常类型,第二个部分是在内存区域中创建对象的原理。本篇主要涉及第二个部分,第一个部分参考JAVA运行时数据区管理
Java是面向对象语言,在Java程序运行过程中无时无刻都有对象产生。在语言层面,创建对象通常就是一个new 关键字而已,但是在虚拟机层面,对象的创建过程是什么样的呢,创建对象后对象在内存中的分布存储是什么样的,如何根据对象引用找到对象实例。

对象创建

在代码层面,创建对象通常如下:Object obj = new Object();
但是这行代码虚拟机是如何执行的呢?虚拟机创建一个对象可分为5个步骤:

解析new指令

当虚拟机遇到一条new指令的时候,根据指令参数,去检查指令参数在常量池中能否找到一个类的符号引用。如果没有则说明编译阶段出错,是不会进入运行阶段的。同时检查这个符号引用代表的类是否被加载、解析和初始化过。如果没有则执行相应的类加载过程操作(类加载的过程,请参考虚拟机类加载机制)。

堆中分配内存

当类加载检查通过后,虚拟机就会为新对象在java堆中分配内存。分配内存就是根据对象的大小,划分相同大小的堆内存空间,然后将对象引用指向(并不一定是直接指向,也有可能通过句柄)该块内存。但是这里就会有几个问题:

对分配的内存空间初始化为零值

内存分配完成后,需要对所分配的内存空间都初始化为零值,这里不包括对象头(对象头是什么在下文的对象布局中会有详细的说明)。如果使用了TLAB,则这一步也有可能在分配TLAB是就完成。

虚拟机对对象进行设置

初始化零值完成后,虚拟机还需要对对象进行必要的设置,比如该对象是哪个类的实例,如何才能找到对象的类的元数据,对象的哈西码,GC分代年龄信息等。这些都是放在对象头中的。

执行<init>方法进行程序层面的初始化

执行到上一步,对虚拟机来说对象刚已经创建完成了,但是从java程序来看,对象还没有执行<init>方法,所有字段都为零。此时执行<init>方法,之后java程序就可以使用该对象了。

对象的创建总结来说就是上面的过程,那么创建完成后,对象在内存中的布局如何,如上文提到的对象头又是什么?

对象布局

这里以HotSpot虚拟机为例,对象在内存中的布局分为3块:

对象头

正如上文所说,我们是如何找到该对象实例,对象实例的类的元数据是什么,这些都是在对象头中设置的。对象头分为两部分信息

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁定记录的指针 00 轻量级锁定
指向中量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标志
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

实例数据

实例数据才是我们程序或者程序员需要的真正存储的有效信息。不管是从父类继承下来的,还是在子类中定义的,都需要记录下来。而这些数据存储顺序与虚拟机分配策略参数和在java代码中的定义声明顺序有关。HotSpot虚拟机的默认分配策略参数是long/double,ints、shorts/chas、bytes/booleans、oops(Ordinay Object Pointers),可以看出是相同宽度的数据类型放在一起,在满足该条件下,从父类继承的变量是会出现在子类之前。如果CompactField参数为true(默认就是true),那么子类中宽度较窄的变量也会插在父类变量的空隙中。

对齐填充

对齐填充不是必然存在的,没有太多的意义,只是作为占位符存在。HotSpot虚拟机规定对象的起始地址必须是8字节的整数倍,换句话说对象的大小必须是8字节的整数倍。因此当实例数据部分没有对齐时,就需要对齐填充。
看有的地方说对象头部分正好是8字节的1倍或者2倍,但是对象都是32bit后者64bit,其实应该是4字节的1倍或者2倍才对。

对象定位

对象被创建和分配到内存中后,我们就需要使用对象。java的程序需要通过在栈上的引用类型,找到该对象的真实地址,进而使用该实例的数据。那么这个过程在虚拟机中是如何的呢?
java虚拟机规范中之规定了这个reference类型指向对象的应用,并没有规定这个引用(其实就认为是一个地址)到底如何去定位、访问堆中对象的具体位置。所以不同的虚拟机实现也不相同。目前主流的有如下两种方式:

两种方式各有优势。使用句柄来访问最大的好处就是reference中存储的信息是稳定的句柄地址,在对象被移动改变的时候只会修改句柄中的数据,而reference数据不用修改。垃圾收集回收时,对象被移动是非常普遍的行为;使用直接内存访问的最大优势是速度更快。java程序在运行中创建对象是非常普遍并且高频发生的操作,那么对象的访问定位也是非常频繁的。直接内存相比于句柄方式,减少了一次访问内存的次数,在频繁的访问对象的过程中,积少成多这种减少内存访问次数带来的效率也是非常可观的。

上一篇下一篇

猜你喜欢

热点阅读