对象创建与内存分配


对象创建过程

当我们执行以下代码时:

Apple apple = new Apple();

大致经过三个步骤:

  1. 堆内存中为Apple对象开辟一块空间
  2. 初始化对象(成员变量赋值等)
  3. apple变量指向堆内存中的Apple对象的地址

其中第1、2部可以细化为:

  1. JVM到常量池中定位这个类的符号引用
  2. 确认这个类是否已加载,如未加载则进行加载
  3. 在堆中开辟对象内存区域
  4. 成员变量初始化0值
  5. 设置对象头中必要的信息(如GC年代信息、锁信息、对象哈希等)
  6. 执行<init>方法(初始化成员变量值等)

对象的内存布局

堆中的对象可以分为三个部分:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

对象头

对象头中存储了两类数据:markword和类型指针。

markword,在64位系统中占用8字节空间(64bit),具体包含:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

但markword中所包含的数据其实加起来已经超过8字节,为了节省存储空间,在不同锁状态下,markword的结构有所不同,以此达到复用存储空间的目的。

markword示意如下:

类型指针,指向方法区中该对象的class元数据。简单来说就是用来确认当前对象的class。如果开启压缩指针(JDK1.6开始默认开启),那么占用4字节,未开启则占用8字节

如果是数组对象,除了markword和类型指针之外,还有数组长度

我们可以借助一个工具类打印对象头,方法如下:

Gradle:

compile group: 'org.openjdk.jol', name: 'jol-core', version: '0.16'

Java:

public class Apple {

    private Integer weight;
    private byte content;
}

public class Test {

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new Apple()).toPrintable());
    }
}

打印结果如下:

实例数据

实例数据就是我们定义的类的成员变量数据,也就是我们开发者自定义的部分

对齐填充

JVM的内存管理要求对象起始地址必须是8字节的整数倍,所以对象头和实例数据的内存大小加起来如果不是8字节的整数倍,就用对齐填充来补全。

对象的访问定位

运行时数据区中介绍过栈中局部变量表reference类型的数据是用于指向堆中的对象。

实际上具体的实现方式分为两种:

  1. 使用句柄访问
  2. 直接指针访问

句柄访问

使用这种方式的话,堆中将有一个句柄池,存放了对象实例所在的地址以及对象类型地址。

示意图如下:

优点:reference指针是稳定地指向句柄池,如果垃圾回收器移动了对象实例,只需要改变句柄即可。

缺点:每次访问对象都需要经过句柄池,也就是需要两次地址访问才能访问到真正的对象实例

直接指针访问

这种方式就是reference直接指向了对象实例,我们常用的HotSpot虚拟机主要使用这种方式

优点:比句柄方式少了一次指针定位

缺点:如果移动了实例对象,reference需要改变

文章作者: 周君
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 周君 !
评论