【漫画】Java对象的创建和访问全过程详解

https://github.com/TangBean

漫画由小猿编写创作

仔细看下面的流程图,我们先来获取一个直观的认识,然后再一点一点的进行详细分析!

对象的创建(遇到一条 new 指令时)

  1. 检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,先把这个类加载进内存;

  2. 类加载检查通过后,虚拟机将为新对象分配内存,此时已经可以确定存储这个对象所需的内存大小;

  3. 在堆中为新对象分配可用内存;

  4. 将分配到的内存初始化;

  5. 设置对象头中的数据;

  6. 此时,从虚拟机的角度看,对象已经创建好了,但从 Java 程序的角度看,对象创建才刚刚开始,构造函数还没有执行。

1、如何在堆中为新对象划分可用的内存?

  • 指针碰撞(内存分配规整)

    • 用过的内存放一边,没用过的内存放一边,中间用一个指针分隔;

    • 分配内存的过程就是将指针向没用过的内存那边移动所需的长度;

  • 空闲列表(内存分配不规整)

    • 维护一个列表,记录哪些内存块是可用的;

    • 分配内存时,从列表上选取一块足够大的空间分给对象,并更新列表上的记录;

2、如何处理多线程创建对象时,划分内存的指针的同步问题?

  • 对分配内存空间的动作进行同步处理(CAS);

  • 把内存分配动作按照线程划分在不同的空间之中进行;

    • 每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB);

    • 哪个线程要分配内存就在哪个线程的 TLAB 上分配,TLAB 用完需要分配新的 TLAB 时,才需要同步锁定;

    • 通过 -XX:+/-UseTLAB 参数设定是否使用 TLAB。

对象的内存布局

在 HotSpot 虚拟机中,对象在内存中存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding),下面详细讲解各部分内容。

  • 对象头:

    • 第一部分:存储对象自身运行时的数据,HashCode、GC分代年龄等(Mark Word);

    • 第二部分:类型指针,指向它的类元数据的指针,虚拟机通过这个指针来判断这个对象是哪个类的实例(HotSpot 采用的是直接指针的方式访问对象的);

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

  • 实例数据:

    • 默认分配顺序:longs/doubles、ints、shorts/chars、bytes/booleans、oops (Ordinary Object Pointers),相同宽度的字段会被分配在一起,除了 oops,其他的长度由长到短;

    • 默认分配顺序下,父类字段会被分配在子类字段前面。

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

注:HotSpot VM要求对象的起始地址必须是8字节的整数倍,所以不够要补齐。

对象的访问

Java 程序需要通过虚拟机栈上的 reference 数据来操作堆上的具体对象,reference 数据是一个指向对象的引用,不过如何通过这个引用定位到具体的对象,目前主要有以下两种访问方式:句柄访问和直接指针访问。

句柄访问

句柄访问会在 Java 堆中划分一块内存作为句柄池,每一个句柄存放着到对象实例数据和对象类型数据的指针。

优势:对象移动的时候(这在垃圾回收时十分常见)只需改变句柄池中对象实例数据的指针,不需要修改reference本身。

直接指针访问

直接指针访问方式在 Java 堆对象的实例数据中存放了一个指向对象类型数据的指针,在 HotSpot 中,这个指针会被存放在对象头中。

优势:减少了一次指针定位对象实例数据的开销,速度更快。

今日话题

大家对于今天Java对象的创建和访问,有没有更深刻的理解呢?请在评论区留言和作者一起讨论!每日话题就是希望大家多交流,每个人都有在公众号发言的权力!希望你和我一起在这里成长!

点击「写留言」分享你的看法吧~

(0)

相关推荐