99.9%的Java程序员都说不清的问题:JVM中的对象内存布局?
传智汇
传智播客旗下IT互联网精英社区
在 Java 程序中,我们拥有多种新建对象的方式。除了最为常见的new语句之外,我们还可以通过反射机制、Object.clone方法、反序列化以及Unsafe.allocateInstance 方法来新建对象。
其中,Object.clone 方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。
Unsafe.allocateInstance 方法则没有初始化实例字段,而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。
我们先来考察new语句,准备一个类,如下图所示:
本文不是专门介绍invoke系列指令的,我会在后面的文章中介绍invoke系列指令。
不过在这里我多说一嘴,字节码中的invokespecial指令通常用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
提到构造器,就不得不提到 Java 对构造器的诸多约束。首先,如果一个类没有定义任何构造器的话, Java 编译器会自动添加一个无参数的构造器。
我们刚才的TestNew类,他的字节码编译出来后,有下面的片段。
在JAVA源码中,我们没有定义构造器,但是生成出来的字节码,已经自动帮我们添加了一个无参数的构造器。他使用的invokespecial方法最终调用的是其父类Object类的构造器方法。
我将讲述JVM的构造器调用原则,那就是,如果子类的构造器需要调用父类的构造器。如果父类存在无参数构造器的话,该调用可以是隐式的。也就是说, Java 编译器会自动添加对父类构造器的调用。
但是,如果父类没有无参数构造器,那么子类的构造器则需要显式地调用父类带参数的构造器。
显式调用有两种,一是直接使用“super”关键字调用父类构造器,二是使用“this”关键字调用同一个类中的其他构造器。
无论是直接的显式调用,还是间接的显式调用,都需要作为构造器的第一条语句,以便优先初始化继承而来的父类字段。
可以不优先初始化继承来的父类字段吗?可以,如果你能使用字节码注入工具的话。
当我们调用一个构造器时,它将优先调用父类的构造器,直至 Object 类。这些构造器的调用者皆为同一对象,也就是通过 new 指令新建而来的对象。
事实上,我上面的陈述意味着:通过 new 指令新建出来的对象,它的内存其实涵盖了所有父类中的实例字段。
也就是说,虽然子类无法访问父类的私有实例字段,或者子类的实例字段隐藏了父类的同名实例字段,但是子类的实例还是会为这些父类实例字段分配内存的。
它的原理是什么?答案是内存对齐。
我们可以通过配置虚拟机的内存对齐选项来进一步提升寻址范围。但是,这同时也可能增加对象间填充,导致压缩指针没有达到原本节省空间的效果。
就算是关闭了压缩指针,Java 虚拟机还是会进行内存对齐。此外,内存对齐不仅存在于对象与对象之间,也存在于对象中的字段之间。
这是为什么呢?
CPU的缓存行机制大家应该有所耳闻,如果字段不是对齐的,那么就有可能出现跨缓存行的字段。
该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。
我们将在后期文章关于volatile关键词的本质分析的过程中,再次考察到CPU缓存的相关机制。
最后我要提一句的是,字段重排列技术,就是我刚才提到的,对象的字段之间存在的内存对齐。这指的是重新分配字段的先后顺序,以达到内存对齐的目的
(点击图片可查看)
-END-