漫谈 对象创建的过程

前情提要,Java 创建对象的方式很简单,就是一句简单的 new Demo() 。然而,我们是否深入思考过,仅仅从 JVM 层面来讨论,这一行代码的背后的底层原理究竟什么呢?JVM 是如何创建对象的?

创建对象

创建对象的方式有4种:new 关键字、反射机制、Object 类的 clone 方法、反序列化。

针对 new 关键字的方式,来谈谈对象创建的过程,例如 Demo 类:

// 创建Demo类的实例对象
Demo demo = new Demo();
// 定义Demo类
public class Demo {
    private int i;
    private String str = "初始值";

    // 构造代码块
    {
        /* do something */
    }

    // 静态代码块
    static {
        /* do something */
    }

    // 构造方法
    public Demo() {

    }

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }
}

创建对象的过程一共5个步骤,如下所示:

一、检查类是否已经被加载

当 JVM 执行到 字节码 new 指令时,首先会到 常量池 中定位到 Demo 类的符号引用,通过符号引用检查 Demo 类是否已被加载。

如果 Demo 类没有被被加载,就必须先执行加载过程。
 

二、为对象分配内存空间

Demo 类的加载检查通过后,JVM 将会从堆中为 new Demo() 创建的实例对象分配确定大小的内存。

分配内存的方式有两种:指针碰撞、空闲列表

  • 指针碰撞

垃圾回收算法是标记-压缩算法,堆内存的空间很整齐的,没有碎片空间,使用中的内存放在一端,空闲的内存在另一端,在中间放置一个指针作为分界点。

分配内存时,只需要将指针往空闲内存的那一端挪动一段与对象大小的距离。

图1
图1

  • 空闲列表

垃圾回收算法是标记-清除算法,堆内存的空间很凌乱的,存在碎片空间,使用的内存和空闲的内存相互交错的在一起。

这种情况下,JVM 需要维护一个空闲列表,记录哪些是空闲内存,分配内存的时候从空闲列表中找到一块足够大的内存空间存放对象,并更新空闲列表。

图2
图2

三、将分配到的内存空间初始化零值(不包括对象头)

为对象的字段进行初始化零值,保证对象即使没有赋初值也可以直接使用。

例如 Demo 类中的 i 初始化为 0,str  初始化为 null。

四、为对象进行必要的设置

设置对象头(Object Head),包括这个对象所属的类,类的元数据信息,对象的哈希码,对象的 GC 分代年龄等信息。

五、执行构造方法

按照顺序执行:

  • 执行 Class 文件中的 <init>() 方法

  • 初始化对象的字段:例如 str 初始化为 "初始值" 

  • 静态代码块

  • 构造代码块

  • 构造方法

对象的内存布局

对象在堆内存中的存储布局划分为三个部分:对象头、实例数据、对齐填充。

对象头包括两部分信息:对象自身的运行时数据、类型指针。

图3
图3

一、对象头

对象头包括两部分信息:对象自身的运行时数据、类型指针。

  • 对象自身的运行时数据(Mark Word)

这些数据包括:哈希码、GC 分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等。

这部分数据的长度在32位和64位的 JVM 中分别为:32 bit、64 bit。        

  • 类型指针

就是对象指向它的类型元数据的指针,可以理解为 demo 指向 Demo.class。

二、实例数据

实例数据就是对象中的所有字段内容,包括从父类继承的所有字段内容。

例如 demo 中的 i 和 str 。

三、对齐填充

对齐填充并不是必然存在的,也没有特别的含义,它仅仅是起到占位符的作用。

因为 JVM 存储对象的内存大小必须是8字节的整数倍。

那么,当对象的大小不是8字节的整数倍,意味着对象申请到的内存大小,必然大于对象的大小,剩余的内存空间就是对齐填充。

图4
图4

对象的访问定位

Demo demo = new Demo();

这一行代码,demo 只是对象的引用,并不是真正创建的对象,demo 在内存中存储的是 new Demo() 实例对象的地址。

demo  存储在栈,new Demo() 所创建的实例对象存储在堆。

demo 访问 new Demo() 所创建的实例对象有两种方式:句柄、直接指针。

一、句柄

句柄池存在堆,那么 demo 存储的就是 new Demo() 的句柄地址,句柄中包含了对象的实例数据和类型数据的实际地址。

以 Demo 类为例,如下图所示:

图5
图5

 

二、直接指针

直接指针 就是 demo 存储的就是 new Demo() 的实际地址。

 以 Demo 类为例,如下图所示:

图5
图6

句柄 相比于 直接访问 需要多一次间接访问的开销,不过句柄访问最大的好处就是灵活,因为引用存储的是句柄地址,句柄地址是不会发生改变的。

这相当于多了一层抽象,句柄就是这一层抽象。

这意味着,即使对象的实际地址发生改变,只需要修改句柄中的实例数据指针的地址,不需要修改引用的值,即 demo 存储的值。

垃圾回收在使用 标记-压缩算法 的情况下,对象的实际地址发生改变是十分普遍的。

再说几句

Demo demo = new Demo(); 这一句简单的代码,仅仅从 JVM 的层面,它的底层原理包含了多少我们不曾了解的知识?就像我朋友说的一句话:你知道的越多,你不知道的越多!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值