Java 对象头内存模型
image.png image.png image.png默认情况下,jdk1.8 在64位虚拟机默认开启指针压缩。
Java 对象头主要包括两部分,第一部分就是 Mark Word,这也是 Java 锁实现原理中重要的一环,另外一部分是 Klass Word。
在笔者64位虚拟机,Jdk1.8(开启了指针压缩)的环境下,任何一个对象,啥也不做,只要声明一个类,那么它的内存占用就至少是96bits,也就是至少12字节。
Mark Word
Mark Word 在64位虚拟机环境下占用 64bits 空间。整个Mark Word的分配有几种情况:
- 未锁定(Normal): 哈希码(identity_hashcode)占用31bits,分代年龄(age)占用4 bits,偏向模式(biased_lock)占用1 bits,锁标记(lock)占用2 bits,剩余26bits 未使用(也就是全为0)
- 可偏向(Biased): 线程id 占54bits,epoch 占2 bits,分代年龄(age)占用4 bits, 偏向模式(biased_lock)占用1 bits,锁标记(lock)占用2 bits,剩余 1bit 未使用。
- 轻量锁定(Lightweight Locked): 锁指针占用62bits,锁标记(lock)占用2 bits。
- 重量级锁定(Heavyweight Locked):锁指针占用62bits,锁标记(lock)占用2 bits。
- GC 标记:标记位占2bits,其余为空(也就是填充0)
新建一个NullObject对象,占用了多少内存
image.png我们发现结果显示:Instance size:16 bytes,结果就是16字节,与我们之前预测的12字节不一样,为什么会这样呢?我们看到上图中有3行 object header,每个占用4字节,所以头部就是12字节,这里和我们的计算是一致的,最后一行是虚拟机填充的4字节,那为什么虚拟机要填充4个字节呢?
内存对齐
CPU 并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。
假设一个32位平台的 CPU,那它就会以4字节为粒度去读取内存块。那为什么需要内存对齐呢?主要有两个原因:
- 平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况。
- 性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作。
所以,没有进行内存对齐就会导致CPU进行额外的读取操作,并且需要额外的计算。如果做了内存对齐,CPU可以直接从地址0开始读取,一次就读取到想要的数据,不需要进行额外读取操作和运算操作,节省了运行时间。我们用了空间换时间,这就是为什么我们需要内存对齐。
回到Java空对象填充了4个字节的问题,因为原字节头是12字节,64位机器下,内存对齐的话就是128位,也就是16字节,所以我们还需要填充4个字节。
非空对象占用内存计算
写一个普通类来验证下:
public class TestNotNull {
private NullObject nullObject=new NullObject();
private int a;
}
int类型是占用4个字节,NullObject对象占用16字节,对象头占12字节,还有一个很重要的情况 NullObject在当前这个类中是一个引用,所以不会存真正的对象,而只存引用地址,引用地址占4字节,所以总共就是12+4+4=20字节,内存对齐后就是24字节。我们来验证下是不是这个结果:
结果如下:
image.png
可以看到TestNotNull的类占用空间是24字节,其中头部占用12字节,变量a是int类型,占用4字节,变量nullObject是引用,占用了4字节,最后填充了4个字节,总共是24个字节,与我们之前的预测一致。但是,因为我们实例化了NullObject,这个对象一会存在于内存中,所以我们还需要加上这个对象的内存占用16字节,那总共就是24bytes+16bytes=40bytes。我们图中最后的统计打印结果也是40字节
equals() 与 hashCode() 关系
- 两个对象 equals() 返回 true 的时候,那它们的 hashCode() 值需要相等;
- 如果两个对象的 hashCode() 值相等,那它们 equals() 不一定是 true;(哈希冲突)
所以在这种情况下,如果要判断两个对象是否相等,除了要覆盖 equals() ,也要覆盖 hashCode(),否则就会发生意料之外的问题。
当然,如果对象不会放入散列表中使用,那么 equals() 与 hashCode() 其实也没啥关系。
网友评论