jvm 对象大小和指针压缩

1 对象的内存布局

下图是java对象的结构,数组对象才会有数组长度
在这里插入图片描述

2 计算对象大小

2.1 8字节对齐

java 的对象默认都是8字节对齐的,即对象的大小都是8的整数倍,所以大小不够8的整数倍的会被补到8的整数倍,例:如果对象大小为30byte那么jvm会自动补2byte,补的2byte就叫对齐填充。

2.1.1为什么要8字节对齐

读取对象效率更高。假如一个对象大小为30byte,如果我们一个字节去读,需要读30次,才能将对象读取完成,假如是8字节对齐,我们只需要读4次,就能将对象读取完成。这其实是一种以空间换时间的方案。

2.2 指针压缩

在堆中,32位的对象引用(指针)占4个字节,而64位的对象引用占8个字节。64位JVM在支持更大堆的同时,由于对象引用变大却带来了性能问题:

  1. 增加了GC开销:64位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了GC的发生,更频繁的进行GC。
    不开启指针压缩,对象指针用8byte表示,开启指针压缩,对象用4byte表示。
  2. 降低CPU缓存命中率:64位对象引用增大了,CPU能缓存的oop将会更少,从而降低了CPU缓存的效率。

为了能够保持32位的性能,oop必须保留32位,即对象引用占4byte。用32位oop来引用更大的堆内存,那么就需要用到指针压缩。
开启指针压缩命令: -XX:+UseCompressedOops
关闭指针压缩命令:-XX:-UseCompressedOops

2.2.1 指针压缩原理

4字节,32位,可以表示232个地址,如果这个地址是真实内存地址的话,那么由于CPU寻址的最小单位是byte,也就是 232 byte = 4GB。
如果内存地址是指向 bit的话,32位的最大寻址范围其实是 512MB,但是由于内存里,将8bit为一组划分,所以内存地址就其实是指向的8bit为一组的byte地址,所以32位可以表示的容量就扩充了8倍,就变成了4GB。

那么jvm如何用4byte表示32G呢?
上面已经提到Java的8字节对齐填充,就像是内存的8bit为一组,变为1byte一样。
将java堆内存进行8字节划分,java对象的指针地址就可以不用存对象的真实的64位地址了,而是可以存一个映射地址编号。这样4字节就可以表示出2^32个地址,而每一个地址对应的又是8byte的内存块。所以,再乘以8以后,一换算,就可以表示出32GB的内存空间。这也是为什么当内存大于32GB时,开启指针压缩的参数会失效!

所以存储的时候后三位抹0,使用的时候后三位补0;
java 的所有的对象都是8字节对齐,8字节对齐的规律就是对象指针后三位永远为0。

2.2.2 为什么不16byte对齐

1.cpu执行gc算法处理32g已经是极限了。
2.费空间。

2.3 对象各部分大小

对象的大小都是8字节对齐的,即对象大小都是8的整数倍。
对象头:
    Mark Word:8字节
    类型指针:
        不开指针压缩:8字节
        开启指针压缩:4字节
    数组长度:
        如果不是数组对象:0字节
        数组对象: 4字节
        所以数组最多存放(2的32次方)-1个元素
    实例数据:
        byte:1B
        short:2B
        int:4B
        long:8B
        float:4B
        double:8B
        boolean:1B
        char:2B
        引用类型:
            开启指针压缩 :4B
            关闭指针压缩 :8B
    对齐填充:补足8的整数倍。如果一个对象占30B ,JVM底层会补2B(对齐填充),凑成32字节,达到8字节对齐。

3 实例计算对象大小

java中可以通过jol-core打印对象大小,引入

<dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.10</version>
 </dependency>

通过ClassLayout.parseInstance(obj).toPrintable()打印对象大小。

3.1 没有实例数据的对象

public class OopTest1 {
    
    
    public static void main(String[] args) {
    
    
        OopTest1 oop = new OopTest1();

        System.out.println(ClassLayout.parseInstance(oop).toPrintable());
    }
}

以上代码开启指针压缩和关闭指针压缩的执行结果是16B
关闭指针压缩: 8b(Mark word)+ 8 (类型指针)+ 0(数组长度)+0(实例数据)+0(对齐填充)=16b
开启指针压缩: 8b(Mark word)+ 4 (类型指针)+ 0(数组长度)+0(实例数据)+4(对齐填充)=16b

3.2 有实例数据对象

public class OopTest3 {
    
    
    public int a =1;
    public int b =2;
    public int c =3;
    public static void main(String[] args) {
    
    
        OopTest3 oop = new OopTest3();

        System.out.println(ClassLayout.parseInstance(oop).toPrintable());
    }

}

关闭指针压缩: 8b(Mark word)+ 8 (类型指针)+ 0(数组长度)+43(实例数据)+4(对齐填充)=32b
开启指针压缩: 8b(Mark word)+ 4 (类型指针)+ 0(数组长度)+4
3(实例数据)+0(对齐填充)=24b

public class OopTest4 {
    
    
    public int a =1;
    public int b =2;
    public int c =3;
    public OopTest3 oopTest3 = new OopTest3();

    public static void main(String[] args) {
    
    
        OopTest4 oop = new OopTest4();

        System.out.println(ClassLayout.parseInstance(oop).toPrintable());
    }

}

关闭指针压缩: 8b(Mark word)+ 8 (类型指针)+ 0(数组长度)+43int+81引用类型(实例数据)+4(对齐填充)=40b
开启指针压缩: 8b(Mark word)+ 4 (类型指针)+ 0(数组长度)+43int+41引用类型(实例数据)+0(对齐填充)=32b

3.3 数组对象

public class OopTest2 {
    
    
    public static void main(String[] args) {
    
    
//        OopTest2 oopTest2 = new OopTest2();
        int[] arrs = {
    
    1,2,3};
        System.out.println(ClassLayout.parseInstance(arrs).toPrintable());
    }
}

开启指针压缩:8b(Mark word)+ 4 (类型指针)+ 4(数组长度)+43(实例数据)+4(对齐填充)=32b
关闭指针压缩:8b(Mark word)+ 8 (类型指针)+ 4(数组长度)+4(对齐填充)+4
3(实例数据)+4(对齐填充)=40b
数组的特殊性,关闭指针压缩会有两段填充
参考链接:
https://blog.csdn.net/liujianyangbj/article/details/108049482
https://www.cnblogs.com/liang1101/p/12727754.html

猜你喜欢

转载自blog.csdn.net/qq_36706941/article/details/112412830