根据java虚拟机规范里面的描述:
java对象分为三部分:对象头(Object Header), 实例数据(instance data),对齐填充(padding)。
举例说明一个Hello对象的大小
// mark word 8个字节,
// 假设开启了指针压缩,那class pointer 4个字节,
// instance data只有一个long类型的a,long类型占8个字节,一共20个字节
//因为对象大小必须被8整除,所以padding对齐填充需要补充4个字节的大小。
public class Hello {
private long a;
}
复制代码
接下来我们一部分一部分介绍对象的组成。
1. mark word
这可能是整个对象中最为重要,也最复杂的组成部分了,对于mark word,甚至于请求头,很多同学都是在学锁(synchronized关键字)的时候认识到他的,所以我们也从锁的不同状态开始来介绍mark word。
状态 | biased_lock | lock |
---|---|---|
无锁 | 0 | 01 |
偏向锁 | 1 | 01 |
轻量级锁 | 0 | 00 |
重量级锁 | 0 | 10 |
64位虚拟机
|------------------------------------------------------------------------------------|
|unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:0| 01 | Nomal |
|------------------------------------------------------------------------------------|
|thread:54| epoch:2 |unused:1|age:4|biase_lock:1| 01 | Biased |
|------------------------------------------------------------------------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|------------------------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|------------------------------------------------------------------------------------|
| | 11 | Marked for GC |
|------------------------------------------------------------------------------------|
biased_lock:是否启用偏向锁
age:4位的Java对象年龄
identity_hashcode:31位的对象标识hashCode
thread:持有偏向锁的线程ID。
epoch:偏向锁的时间戳。
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
复制代码
我们会发现,在无锁状态下,Mark Word中存储着对象的identity hash code值。
-
当对象的hashCode()方法(非用户自定义)第一次被调用时,JVM会生成对应的identity hash code值,并将该值存储到Mark Word中。后续如果该对象的hashCode()方法再次被调用则不会再通过JVM进行计算得到,而是直接从Mark Word中获取。只有这样才能保证多次获取到的identity hash code的值是相同的。
-
以jdk8为例,JVM默认的计算identity hash code的方式得到的是一个随机数,因而我们必须要保证一个对象的identity hash code只能被底层JVM计算一次。
-
但当锁升级到轻量级锁之后,对象的hashcode,GC age都不见了,那是因为当升级至轻量级锁时,jvm会将对象的 mark word 复制一份到栈帧的Lock Record中。等线程释放该对象时,再重新复制给对象。
-
而重量级锁,ObjectMonitor类里也有字段可以记录非加锁状态下的mark word,其中也可以存储identity hash code的值,所以重量级锁也可以和identity hash code共存。
-
但对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象就不能再被设置偏向锁么。因为如果可以,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
- 所以当一个对象已经计算过identity hash code,它就无法进入偏向锁状态。
- 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁。
- 轻量级锁的实现中,会通过线程栈帧的锁记录存储Displaced Mark Word;重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。
2. class pointer
一个指向方法区中Class信息的指针,虚拟机通过这个指针确定该对象属于哪个类的实例。
在64位的JVM中,支持指针压缩功能:
- 未开启指针压缩时,类型指针占用8B (64bit)
- 开启指针压缩情况下,类型指针占用4B (32bit)
在不压缩的情况下:
- 相当于每个对象多4个字节,需要占用更多的堆空间,增加GC开销
- 64位对象变大了,CPU能缓存的oop将会更少,从而降低了CPU缓存的效率
指针压缩原理:
- 32位的JVM就如第一张图,4个字节的class pointer正好可以表示4*8=32位,最多也就可以表示2的32次方个地址,由于CPU寻址的最小单位是byte,所以2的32次个字节=4G。
- 但是当绝对多数服务器的内存都开始大于4G时,32的的JVM也就不够用了,于是来到了64位JVM的时代。
- 重新回到指针,64的JVM的指针大小是64位,也就是8个字节,如何才能压缩成4个字节呢。如果还是按照byte作为CPU的最小寻址单位去实现,那4个字节的指针,最多也就只能表示4G。
- 于是乎,前辈们利用了Java的对齐填充机制,总所周知,Java是8字节对齐填充,于是一位(如图寻址1)不再表示1个字节的地址,而是表示8个字节的地址,那样能够表示的内存空间就变成了2的32个次方8=4G8=32G,足够表示绝大多数服务器的内存大小。
- 所以当内存大于32GB时,开启指针压缩的参数会失效。
3. instance data
- 实例数据部分是对象真正存储的有效信息,也就是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。
- 这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
- HotSpot虚拟机 默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。