JAVA对象头的指针压缩
文章目录
- JAVA对象头的指针压缩
- 对象在JVM中的内存布局
- 对象的访问定位
- 压缩实验
- 实验步骤
- 压缩策略组合
- 压缩内容
- 压缩后的影响
- 指针压缩的实现
- JVM内存关键大小
对象在JVM中的内存布局
在 Hotspot 虚拟机中,对象的内存布局主要由 3 部分组成:
-
对象头(Header):包含了对象运行时数据Mark Word,Klass Pointer、数组长度(数组对象才会有)。
- Mark Word:包含对象运行时的信息,包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 类型指针(Klass Pointer):即对象指向它的类型元数据的指针。通过直接指针访问对象才需要在对象上存储类型指针,通过句柄访问对象时不需要此指针。Hotspot采用的是直接指针访问对象的方式,所以这类虚拟机中的对象上存储了类型指针。
- 数组长度:数组对象才会有。
-
实例数据(Instance Data):对象存储的真正有效数据,即当前类型的字段和父类继承的字段
-
对应填充(Padding):不一定存在,主要用于占位符。虚拟机中任何对象的大小都是8字节的整数倍,如果对象所占空间不是8字节的整数倍,会进行补充对齐。
对象的访问定位
在对象创建之后,JAVA程序会通过JAVA栈上的reference数据来操作堆上的具体对象。对象的访问方式主要由虚拟机自主实现,主流的有两种方式:
-
通过句柄访问对象
JAVA堆中会有一块内存作为句柄池,reference中存储的是对象的句柄地址,在句柄中包含了对象的实例数据和对象的类型数据的地址。
优势:reference中存储的是稳定的句柄地址,在对象发生移动的时候(比如在垃圾回收的时候会移动对象),只改变了句柄中对象实例数据的地址,而reference不需要修改。
// 访问图解
-
通过直接直接访问对象
JAVA堆中的对象实例数据中存储着对象类型数据的地址,reference中存储的是对象的地址。
优势:访问速度快,在访问对象的时候比句柄方式少一次指针定位的时间开销,由于对象访问比较频繁,那这个节约的开销积累下来就很可观了。
// 访问图解
压缩实验
实验步骤
-
使用的是64位OS
-
引入查看对象头布局的工具
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
package org.donny;
import org.openjdk.jol.info.ClassLayout;
/**
* @author 1792998761@qq.com
* @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
}
}
压缩策略组合
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字节 |
压缩内容
可以发现通过指针压缩可以对以下数据进行压缩:
- 对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节
- 对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节
- 对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节
主要是对象头里面的kclass指针,即指向方法区的类信息的指针,由8字节变为4字节。 还有就是引用类型指针也由8字节变为4字节。
压缩后的影响
压缩后的好处:
- 减缓GC的压力: 即每个对象的大小都变小了,就不需要那么频繁的GC了。
- 增加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