对象的创建
1.1 对象创建的6种方式
使用new关键字、Class的newInstance()方法、Constructor类的newInstance()方法、clone()方法、反序列化、第三方库Objenesis。
每种创建对象方式的实际操作如下。
- 使用new关键字—调用无参或有参构造器创建。
- 使用Class的newInstance()方法—调用无参构造器创建,且需要是public的构造器。
- 使用Constructor类的newInstance()方法——调用无参或有参、不同权限修饰构造器创建,实用性更广。
- 使用clone()方法——不调用任何参构造器,且对象需要实现Cloneable接口并实现其定义的clone()方法,且默认为浅复制。
- 使用反序列化——从指定的文件或网络中,获取二进制流,反序列化为内存中的对象。
- 第三方库Objenesis——利用了asm字节码技术,动态生成Constructor对象。
1.2 new对象创建的字节码解读
各个指令的含义如下。
new:首先检查该类是否被加载。如果没有加载,则进行类的加载过程;如果已经加载,则在堆中分配内存。对象所需的内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。这个指令完毕后,将指向实例对象的引用变量压入虚拟机栈栈顶。
dup:在栈顶复制该引用变量,这时的栈顶有两个指向堆内实例对象的引用变量。
invokespecial:调用对象实例方法,通过栈顶的引用变量调用<init>方法。<init>是对象初始化时执行的方法,而<clinit>是类初始化时执行的方法。
从上面的四个步骤中可以看出,需要从栈顶弹出两个实例对象的引用。这就是为什么会在new指令下面有一个dup指令。其实对于每一个new指令来说,一般编译器都会在其下面生成一个dup指令,这是因为实例的初始化方法(<init>方法)肯定需要用到一次,然后第二个留给业务程序使用,例如给变量赋值、抛出异常等。如果我们不用,那编译器也会生成dup指令,在初始化方法调用完成后再从栈顶pop出来。
2 对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头(Object Header):这部分包含了对象的一些元数据和信息,包括但不限于:
-
- Mark Word:也称为对象标记(object mark word),用于存储对象自身的运行时数据,如哈希码(hash code)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在32位和64位的虚拟机中,Mark Word的长度分别对应为32位和64位。
-
- Klass Pointer:也被称为类型指针(type pointer),它指向对象所属的类元数据(class metadata)的首地址。虚拟机通过这个指针来确定对象属于哪个类的实例。
- 其他字段:可能还包括对象所属的年代、锁状态标志、偏向锁(thread ID)以及偏向时间等。
- 实例数据(Instance Data):这部分包含实际的属性和数据成员,即类的属性数据信息,包括父类的属性信息。
- 对齐填充(Padding):这部分主要是为了确保对象起始地址是8字节的整数倍而设置的,其目的是为了字节对齐。
3 对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
3.1 句柄访问
如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图所示。
句柄访问的缺点包括:
- 性能损失:由于句柄访问需要额外的间接寻址操作,因此会引入性能损失。每次访问对象数据时都需要先通过句柄找到实际数据的地址,增加了访问对象的时间开销。
- 内存开销:句柄访问机制会增加额外的内存开销,因为除了存储对象数据的内存空间外,还需要额外的空间来存储句柄。这会导致系统整体的内存占用增加。
- 额外的指针解引用:使用句柄访问机制时,访问对象数据需要经过额外的指针解引用过程,这会增加 CPU 的负担,降低程序的执行效率。
- 存在单点故障:句柄访问会引入句柄池,如果句柄池出现问题,可能导致整个应用无法正常工作,存在单点故障的风险。
3.2 直接指针访问
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如图所示。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
直接指针访问的缺点包括:
- 内存碎片化:直接指针访问可能导致内存碎片化问题。当对象被频繁创建和销毁时,可能会在 Java 堆中留下大量碎片,影响内存的连续性,从而增加垃圾回收的成本。
- 难以实现对象移动:直接指针访问会使对象的地址固定,难以实现对象的移动。在进行垃圾回收或对象整理时,如果对象需要移动,会涉及到更多的内存复制操作,增加系统开销。
- 安全性问题:直接指针访问可能存在安全性问题,因为直接暴露对象的地址给程序员,有可能被恶意利用进行非法操作,如越界访问等。