JAVA对象头的指针压缩

JAVA对象头的指针压缩

文章目录

  • JAVA对象头的指针压缩
    • 对象在JVM中的内存布局
    • 对象的访问定位
    • 压缩实验
      • 实验步骤
        • 压缩策略组合
        • 压缩内容
        • 压缩后的影响
        • 指针压缩的实现
    • JVM内存关键大小

对象在JVM中的内存布局

在 Hotspot 虚拟机中,对象的内存布局主要由 3 部分组成:

  1. 对象头(Header):包含了对象运行时数据Mark Word,Klass Pointer、数组长度(数组对象才会有)。

    • Mark Word:包含对象运行时的信息,包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
    • 类型指针(Klass Pointer):即对象指向它的类型元数据的指针。通过直接指针访问对象才需要在对象上存储类型指针,通过句柄访问对象时不需要此指针。Hotspot采用的是直接指针访问对象的方式,所以这类虚拟机中的对象上存储了类型指针。
    • 数组长度:数组对象才会有。
      JAVA对象头的指针压缩_第1张图片
      JAVA对象头的指针压缩_第2张图片
  2. 实例数据(Instance Data):对象存储的真正有效数据,即当前类型的字段和父类继承的字段

  3. 对应填充(Padding):不一定存在,主要用于占位符。虚拟机中任何对象的大小都是8字节的整数倍,如果对象所占空间不是8字节的整数倍,会进行补充对齐。

对象的访问定位

在对象创建之后,JAVA程序会通过JAVA栈上的reference数据来操作堆上的具体对象。对象的访问方式主要由虚拟机自主实现,主流的有两种方式:

  • 通过句柄访问对象

    JAVA堆中会有一块内存作为句柄池,reference中存储的是对象的句柄地址,在句柄中包含了对象的实例数据和对象的类型数据的地址。

    优势:reference中存储的是稳定的句柄地址,在对象发生移动的时候(比如在垃圾回收的时候会移动对象),只改变了句柄中对象实例数据的地址,而reference不需要修改。

    // 访问图解

  • 通过直接直接访问对象

    JAVA堆中的对象实例数据中存储着对象类型数据的地址,reference中存储的是对象的地址。

    优势:访问速度快,在访问对象的时候比句柄方式少一次指针定位的时间开销,由于对象访问比较频繁,那这个节约的开销积累下来就很可观了。

    // 访问图解

压缩实验

实验步骤

  • 使用的是64位OS

  • 引入查看对象头布局的工具

<dependency>
    <groupId>org.openjdk.jolgroupId>
    <artifactId>jol-coreartifactId>
    <version>0.17version>
dependency>
package org.donny;

import org.openjdk.jol.info.ClassLayout;

/**
 * @author [email protected]
 * @description 借助openjdk的jol工具, 查看对象内存的占用情况
 * oop(ordinary object pointer)--对象指针
 * -XX:+UseCompressedOops 默认开启
 * -XX:+UseCompressedClassPointers  默认开启对象头里面的类型指针压缩
 * @date 2023/6/5
 */
public class TestObjectHeader {
    public static void main(String[] args) {

        System.out.println("============Object对象===========");
        ClassLayout layout = ClassLayout.parseInstance(new Object());
        System.out.println(layout.toPrintable());

        System.out.println("============int类型数组对象===========");
        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());

        System.out.println("============复合类型对象===========");
        ClassLayout layout2 = ClassLayout.parseInstance(new CompositeObjectsTest());
        System.out.println(layout2.toPrintable());
    }

    public static class CompositeObjectsTest {
        //8B mark word
        //4B Klass Pointer   如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B
        int id;        //4B
        String name;   //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
        byte b;        //1B
        Object o;      //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
    }
}

JAVA对象头的指针压缩_第3张图片

压缩策略组合

VM的配置项

  • UseCompressedOops:压缩当前对象实例数据中的 Klass Pointer 指针
  • UseCompressedClassPointers:压缩当前对象的对象头中 Klass Pointer 指针

对象指针压缩+对象头的类型指针压缩

JDK1.8之后默认开启这两个压缩

VM options: -XX:+UseCompressedOops -XX:+UseCompressedClassPointers

============Object对象===========
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

============int类型对象===========
[I object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf800016d
 12   4        (array length)            0
 16   0    int [I.<elements>             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

============复合类型对象===========
org.donny.TestObjectHeader$CompositeObjectsTest object internals:
OFF  SZ               TYPE DESCRIPTION                 VALUE
  0   8                    (object header: mark)       0x0000000000000005 (biasable; age: 0)
  8   4                    (object header: class)      0xf800f161
 12   4                int CompositeObjectsTest.id     0
 16   1               byte CompositeObjectsTest.b      0
 17   3                    (alignment/padding gap)     
 20   4   java.lang.String CompositeObjectsTest.name   null
 24   4   java.lang.Object CompositeObjectsTest.o      null
 28   4                    (object alignment gap)      
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

对象指针不压缩+对象头的类型指针压缩

VM options:-XX:-UseCompressedOops -XX:+UseCompressedClassPointers

============Object对象===========
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8        (object header: class)    0x0000012a6a481c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

============int类型对象===========
[I object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8        (object header: class)    0x0000012a6a480b68
 16   4        (array length)            0
 20   4        (alignment/padding gap)   
 24   0    int [I.<elements>             N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

============复合类型对象===========
org.donny.TestObjectHeader$CompositeObjectsTest object internals:
OFF  SZ               TYPE DESCRIPTION                 VALUE
  0   8                    (object header: mark)       0x0000000000000005 (biasable; age: 0)
  8   8                    (object header: class)      0x0000012a6aae3210
 16   4                int CompositeObjectsTest.id     0
 20   1               byte CompositeObjectsTest.b      0
 21   3                    (alignment/padding gap)     
 24   8   java.lang.String CompositeObjectsTest.name   null
 32   8   java.lang.Object CompositeObjectsTest.o      null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

对象指针压缩+对象头的类型指针不压缩

VM options: -XX:+UseCompressedOops -XX:-UseCompressedClassPointers

============Object对象===========
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8        (object header: class)    0x000002d380801c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

============int类型对象===========
[I object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8        (object header: class)    0x000002d380800b68
 16   4        (array length)            0
 20   4        (alignment/padding gap)   
 24   0    int [I.<elements>             N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

============复合类型对象===========
org.donny.TestObjectHeader$CompositeObjectsTest object internals:
OFF  SZ               TYPE DESCRIPTION                 VALUE
  0   8                    (object header: mark)       0x0000000000000005 (biasable; age: 0)
  8   8                    (object header: class)      0x000002d380e63210
 16   4                int CompositeObjectsTest.id     0
 20   1               byte CompositeObjectsTest.b      0
 21   3                    (alignment/padding gap)     
 24   4   java.lang.String CompositeObjectsTest.name   null
 28   4   java.lang.Object CompositeObjectsTest.o      null
Instance size: 32 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

对象指针不压缩+对象头的类型指针不压缩

VM options: -XX:-UseCompressedOops -XX:-UseCompressedClassPointers

============Object对象===========
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8        (object header: class)    0x00000234d96d1c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

============int类型对象===========
[I object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8        (object header: class)    0x00000234d96d0b68
 16   4        (array length)            0
 20   4        (alignment/padding gap)   
 24   0    int [I.<elements>             N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

============复合类型对象===========
org.donny.TestObjectHeader$CompositeObjectsTest object internals:
OFF  SZ               TYPE DESCRIPTION                 VALUE
  0   8                    (object header: mark)       0x0000000000000001 (non-biasable; age: 0)
  8   8                    (object header: class)      0x00000234d9d33210
 16   4                int CompositeObjectsTest.id     0
 20   1               byte CompositeObjectsTest.b      0
 21   3                    (alignment/padding gap)     
 24   8   java.lang.String CompositeObjectsTest.name   null
 32   8   java.lang.Object CompositeObjectsTest.o      null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

结果集解释

OFF代表offset
SZ代表size
VALUE代表的十六进制的表示值
alignment/padding gap代表不足8字节的整数倍,填充补齐项
类型 开启指针压缩 不开启指针压缩
Object 16字节 16字节
int数组 16字节 24字节
复合对象 32字节 40字节

压缩内容

可以发现通过指针压缩可以对以下数据进行压缩:

  1. 对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节
  2. 对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节
  3. 对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节

主要是对象头里面的kclass指针,即指向方法区的类信息的指针,由8字节变为4字节。 还有就是引用类型指针也由8字节变为4字节

压缩后的影响

压缩后的好处:

  1. 减缓GC的压力: 即每个对象的大小都变小了,就不需要那么频繁的GC了。
  2. 增加CPU缓存的对象指针数量,同时降低CPU缓存的命中率。即CPU缓存本身的大小就小的多,如果采用八字节,CPU能缓存的oop(普通对象指针)肯定比四字节少。

指针压缩的实现

地址总线的根数决定了最大可用内存空间容量。每一个字节(B)的内存空间被视为一个地址单元,整个内存可以看作很多个地址单元组成的数组,每个地址单元有唯一确定的地址来标定,来区分不同的地址单元。地址总线的数量会变化,数据不一定准确可以查询服务器硬件参数确认。

地址总线数目 内存上限
32位操作系统 32 2 32 2^{32} 232=4GB
64位操作系统 36或者46 2 36 2^{36} 236=64GB或者 2 46 2^{46} 246=64TB

一种理解:

当开启指针压缩后,KlassPointer的寻址极限是4 byte × 8 bit=32 bit,即KlassPointer可以存放2^32(=4G)个内存单元地址。

因为每个对象的长度一定是8的整数倍,所以KlassPointer每一数位存放的一定是8的整数倍的地址,即0/8/16/24/32/40/48/64……,也就是4G × 8 = 32G。当分配给JVM的内存空间大于32G时,KlassPointer将无法寻找大于32G的内存地址,因此设置的压缩指针将失效。

第二种理解:

JVM的实现方式是

不再保存所有引用,而是每隔8个字节保存一个引用。例如,原来保存每个引用0、1、2…,现在只保存0、8、16…。因此,指针压缩后,并不是所有引用都保存在堆中,而是以8个字节为间隔保存引用。

在实现上,堆中的引用其实还是按照0x0、0x1、0x2…进行存储。只不过当引用被存入64位的寄存器时,JVM将其左移3位(相当于末尾添加3个0),例如0x0、0x1、0x2…分别被转换为0x0、0x8、0x10。而当从寄存器读出时,JVM又可以右移3位,丢弃末尾的0。(oop在堆中是32位,在寄存器中是35位,2的35次方=32G。也就是说,使用32位,来达到35位oop所能引用的堆内存空间

JVM内存关键大小

  • 当堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址
  • 当堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址, 那这样的话内存占用较大,会增加GC压力等等

参考文章
https://www.cnblogs.com/xiaomaomao/p/17350075.html
https://blog.51cto.com/u_87851/6326662
https://artisan.blog.csdn.net/article/details/106958768
https://blog.csdn.net/lioncatch/article/details/105919666

你可能感兴趣的:(#,JVM,JAVA,java,jvm,开发语言)