对象是如何被创建的?
(本文章为本人自媒体账号“小刘不是程序员”发布视频的文字版,方便大家阅读)
这篇文章以Java为例,浅显地解释一下JVM当中,Java对象是如何被创建的。
我们以这段代码为例子:
一个再普通不过的Person类,包含name和age两个字段。在main中我们创建一个Person类的实例,然后把这个实例的name打印出来:
编译这段代码会生成一个class文件,我们用 javap 工具可以把class文件当中的字节码打印出来:
可以看到在字节码中,关于对象创建第一条也是最重要的一条指令,就是 new。后面的 #55 是对类型的引用。输出也展示了,指向的就是代码里写的Person类型。字节码的任务到这儿就算完成了,具体怎么 new 这个对象呢?这就是 JVM 需要解决的问题了。
Hotspot 虚拟机的代码量巨大。为了让大家更好的理解创建对象的过程,我们先不看具体的代码,而是了解一下从高层角度看,Hotspot 中 Java 对象是如何存在的。
我们知道在 Java 中,除了基础类型之外的对象都继承自
为什么要拆成两个东西呢?在 Hotspot 源码里有解释。klass 头文件里的注释是这样说的:
分开 oop/klass 的一个原因是,我们不想让每个对象里都有 C++ vtbl 指针。正常的oop对象里没有任何虚方法,它们将所有的”虚“方法转发给对应的 klass。而 klass 中包含 vtbl,并且可以根据实际类型进行 C++ 的动态调用。
由于拆分了权责,oop的实现相当简单。这就是 oop 的实现类,名字叫做 oopDesc:
它里面就俩字段,一个是 MarkWord,另一个是 metadata。这俩东西就是很多文章中都会提到的 Java 对象头。其中 metadata 里有一个指向 Klass 的指针,对象跟类就这么被联系起来了。
聪明的同学可能已经想到一个问题了,对象也不能只有头啊。比方我们的 Person 里面有 name 有 age,这些东西存哪儿呢?答案是直接往 oopDesc 的后面存,这就是 Hotspot 里对 C++ 的利用和扩展了。oopDesc 里面的对象头,紧接着后面就是一个 int 也就是 age,然后就是一个 String 引用也就是 name。顺序的话可能跟代码定义的不完全相同,大家不用在意。再往后,可能还会有 padding 填充。
为了方便获取后面的各种字段,oopDesc 提供了 field_addr,obj_field_addr 等方法。它们的实现就是拿当前对象的指针,然后往后面加 offset 算:
有了这些基础知识之后,再去理解 JVM 源代码就会简单很多。下面我们看一下 Hotspot 中bytecodeInterpreter里 new 这个指令的实现。bytecodeInterpreter是一个解释型的实现,并不是实际上跑的实现。不过用来理解VM工作过程是没问题的,《深入理解Java虚拟机》中也是用它做的例子。这就是new的实现:
首先可以看到它的主逻辑是,先判断用不用 TLAB 也就是线程本地存储。如果使用线程本地存储的话,就不用去全局堆上建对象了,也就不需要线程间的同步了。首先它拿到了一个 Klass 的实现,也就是要先拿到类的有关信息。然后cast成了,InstanceKlass 这个 Klass 的子类。再往后它算了下整个对象的大小,然后去 TLAB 里分配了对应的大小,得到了 result 这个内存指针。然后把 result 这个指针 cast 成 oopDesc * 类型设置上默认的 markword 以及 metadata 信息,这个对象在 Hotspot 层面就已经创建完成了。
回到最开始的反编译结果,我们会看到后面其实还有一个重要指令就是 invokespecial:
它用来调用对象的init,也就是Java里的constructor。constructor里面就是Java程序员可以控制的了。完成这一步之后,整个Java对象才算是在Java层面可用了。
#后端#