对象创建与内存分配
Java中创建对象并为其分配内存的过程如下:
Java语言层面执行new关键字
在Java中,通常使用new关键字来创建一个对象(还没有对象的可以试试^_^),对应在虚拟机中会触发一条new指令。
虚拟机处理new指令
当虚拟机触发一条new指令时,首先根据指令参数去常量池中检查是否有相关类的符号引用,进一步检查它是否已经被加载、解析和初始化,如果没有,则需要先加载类。对类加载相关的信息判断完成之后,才开始为对象分配内存。
为对象分配内存
根据堆内存的规整状态(堆内存是否规整又由垃圾收集器的算法规则决定)常用如下两种方式:
指针碰撞
适用于堆内存绝对规整,没有碎片,用过的内存在一边,未用过的内存在另一边的情况。两段内存之间放置一个边界指针,分配内存时仅需要把指针向空闲内存段移动对象大小的距离即可。
空闲列表
适用于堆内存无规则,用过的内存与空闲内存交错的情况。此时虚拟机维护一个记录内存使用状态的列表,分配内存时,寻找一块能容下对象大小的空闲片段,并更新列表记录。
说明:由于堆内存是线程共享区域,如果多线程同时为对象分配内存,则上述方法必然存在线程安全问题。通常可想到的方案是对内存分配动作进行同步加锁处理,但这种方式必然影响效率;另一种方案为采用TLAB(Thread Local Allocation Buffer)方式,即先为每个线程分配一块内存,然后哪个线程需要为对象分配内存时,就在各个线程的内存区域内部分配,这也可以保证各个线程间互不影响。当TLAB空间用完重新分配时,才需要同步处理。
将分配的内存空间初始化为零值
内存分配完成后,虚拟机有个初始化零值的过程,如果使用TLAB方式,此操作也可能提前到TLAB分配时进行。正因为如此,我们平时创建一个对象后,其字段有初始值,如int返回0,boolean返回false。
虚拟机对对象进行必要设置
主要是存储一些信息,如此对象属于哪个类、怎么样获取类的元数据、对象的hashcode、对象的GC分代年龄等信息,这些信息存放在对象的对象头中(对象在内存中的存储布局分为3个区域:对象头、实例数据、对齐填充)。
<init>
方法执行,完成对象的真正初始化上述5步之后,已经创建了一个对象了,但各个字段被置为零值,接下来会执行
<init>
方法,根据开发者的代码逻辑来初始化对象,这样才算真正完成了对象的创建。
对象访问方式
创建好Java对象之后,就需要使用对象。在虚拟机栈上只定义了一个指向堆内的引用,主流有两种方式去操作对象:
使用句柄
此种方案会在堆中创建一个句柄池,栈中的引用存储的是对象句柄的地址,因此栈中的引用指向堆中的句柄池中的某个句柄,而对象句柄则存储了对象实例数据和类型数据的信息(它们的具体地址),示意图(来自《深入理解Java虚拟机》)如下:
直接指针
此种方案就是栈中的引用存储的就是堆中对象的地址,直接指向堆中的对象,因此在堆中对象的存储布局中就要考虑如何存放类型数据等信息,示意图(来自《深入理解Java虚拟机》)如下:
说明:上述两种方式各有优缺点。
对于句柄方式,由于栈引用存储的是对象句柄地址,因此对象移动时(GC引发)只需要修改对应句柄指向的对象地址即可,栈中的引用不用修改;其缺点就是访问较慢,因为有两次指针定位。
对于直接指针方式,它的优点就是只有一次指针定位,访问速度快,当然句柄方式的优点也是其缺点。Sun的HotSpot虚拟机是使用直接指针的方式来访问对象的。
参考文献
《深入理解Java虚拟机:JVM高级特性与最佳实践》