一、对象的创建
当Java虚拟机遇到一条new字节码指令时,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否已经被加载,如果没有,就必须先将就该类加载到内存中,具体过程见:类的加载过程。
在类加载检查通过后,虚拟机就会为这个对象分配内存,内存分配的方式主要有两种:
- 指针碰撞:如果JVM堆内存是绝对规整的,所有已经使用的内存都放在一边,空闲的内存都放在另一边,中间用一个指针指向分界点,那么只需要将指针往后移动新对象的大小同等距离即可。
- 空闲列表:如果堆内存不是规整的,那么就无法使用“指针碰撞”的方式来为新对象分配内存,虚拟机就需要维护一张列表,用于记录堆内存上哪一块空间是空闲的,在这个列表中选取一块足够大的内存空间为新对象分配内存,并更新列表。
具体使用哪种内存分配方式是由使用的垃圾收集器是否具有空间压缩整理的能力决定的,使用Serial
、ParNew
这种带压缩整理过程的垃圾收集器时,采用的分配算法是指针碰撞
。而如果使用CMS
这类基于标记-清除
算法的垃圾收集器时,将采用空闲列表
来分配内存
由于对象的创建在虚拟机中是非常频繁的行为,在并发情况下并不是线程安全的,可能出现为A对象分配内存空间,指针还没来得及修改,对象B又使用了同一块内存空间。解决这个问题又两种可选方案:
- 采用同步的方式分配内存空间,在Java虚拟机中采用CAS加上失败重试的方式来保证更新操作的原子性。
- 另一种方案是将内存分配动作按照线程划分在不同的空间中进行,即每个线程再Java堆中预先分配一块小内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程需要分配内存,就在哪个线程的本地线程分配缓冲区中分配,只用当本地线程分配缓冲中的空间不够为新对象分配空间了,才需要采用同步的方式分配空间。虚拟机是否使用本地线程分配缓冲,可以通过
-XX:+/-UseTLAB
参数来设置。
在内存分配完成后,虚拟机还需要对象的对象头进行必要的设置,例如GC分代年龄、这个对象属于哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到调用hashCode()方法才计算)等。
执行完上面的操作后,虚拟机还需要执行Class文件中的方法,也就是我们在代码中写的构造函数。
二、对象的内存布局
在HotSpot虚拟机中,对象在堆内存中的存储结构可以分为三个部分:
- 对象头(Head)
对象头包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁;偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32个比特和64个比特,称为“Mark Word”。
对象头的另一部分是类型指针(Klass Word),即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是属于哪个类的实例,如果对象是一个Java数组,那么对象头中还必须有一块用于记录数组长度。
- 实例数据(Instance Data)
实例数据是对象真正储存的有效信息,是我们在代码中定义的各种类型的字段内容,包括从父类继承下来的。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle)和字段在源码中定义的顺序的影响。HotSpot虚拟机默认的分配顺序是:longs/doubles
、ints
、shorts/chars
、bytes/booleans
、oops(Ordinary Object Pointers, OOPs)
,父类定义的字段会出现子类前面。
- 对齐填充(Padding)
对齐填充不是必须的部分,也没有什么特别的含义,仅仅是为了将对象的大小凑到8字节的倍数。