JVM中的对象创建、内存布局和访问定位

对象的创建

从语言层面来讲,我们创建对象不过是使用了一个new关键字而已,在虚拟机中到底发生了什么呢?

1、虚拟机遇到一条new指令的时候,首先检查这个指令的参数能否在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析、初始化过。如果没有,执行相应的类加载过程(后面的文章会提到)。

2、类加载检查通过后,虚拟机为新生对象分配内存,对象所需内存大小在类加载完成后便可完全确定。Java堆中内存是绝对规整的,所有用过的内存放一边,空闲的内存放在另一半,中间放着一个指针作为分界点的指示器。分配内存的过程就相当于把这个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式叫做“指针碰撞”(Bump the pointer)。如果Java堆中内存不是规整的,已使用的内存和空闲的内存相互交错就没有办法简单地进行指针碰撞了,而是需要维护一张表,记录哪些内存块可用,分配的时候从列表找出一块足够大的空间分配给对象,然后更新列表记录,这种分配方式叫做“空闲列表”(Free List)。

3、内存分配完成之后将分配到的内存空间都初始化为零值

4、JVM对对象头(Object Header)进行必要的设置。比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存储在对象头里。

5、到目前为止,对象产生了,但是字段还全是零值,接下来会执行<init>方法,把对象按照程序员的意愿进行初始化。然后才算完成了一个真正可用的对象。


对象的内存布局

对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding

1、对象头包含两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等);另外一部分是类型指针,指向它的类元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例。不是所有的JVM实现都必须在对象数据上保留类型指针。

2、实例数据部分是对象真正存储的有效信息,也是程序代码种所定义的各种类型的字段内容。无论是从父类继承下来的还是子类中定义的,都需要记录起来,相同宽度的字段总是被分配到一起。

3、对齐填充部分不是必然存在的,没有别的含义,仅仅是占位符,因为对象的大小需要是8的整数倍,此时就需要用对其填充来补全。


对象的访问定位

我们的Java程序需要栈上的reference来操作堆上的具体对象。由于reference类型在JVM中只规定了一个指向对象的引用,没有规定这个引用应该通过什么样的方式区定位访问堆中的对象,所以对象访问方式也是通过JVM决定的,目前主流的访问方式有使用句柄直接指针两种。

首先来看使用句柄的访问方式:

JVM中的对象创建、内存布局和访问定位

如果使用句柄访问方式的话,java堆会划分出一部分内存用来做句柄池,这种情况下reference存储的就是对象的句柄地址,而句柄中包含了对象实例数据和对象类型数据的地址

再来看看直接指针的访问方式:

JVM中的对象创建、内存布局和访问定位

如果使用直接指针访问的话,那么Java堆对象的布局中就必须加*问类型的数据的相关信息,而reference中存储的直接就是对象地址

优缺点比较:

使用句柄访问的最大好处在于reference中存储的是稳定的句柄地址,在对象被移动的时候(这在GC的时候简直太常见了)只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用直接指针的最大好处在于速度更快,节省了以此指针定位的时间开销,这种开销积少成多也很可观。Sun HotSpot使用的是直接指针访问方式。