写在文章开头
近期和朋友聊天时聊到一些比较有趣的面试题?
1.指针压缩了解过吗?它解决什么问题? 2. 为什么设置JVM内存时不建议设置超过32G?它和指针压缩有什么关系?
针对上述两道面试题,笔者就以此文展开深入讲解:
你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。
指针压缩详解
Java对象内存布局
我们都知道Hotspot
虚拟机在中的java
对象的整体布局为:
- 对象头
- 实例数据
- 对齐填充
其中对象头由Mark world
、类型指针及数组长度组成。这其中类型指针就是本文的主角,它指向了当前实例元数据的指针,通过这个指针,虚拟就可以确认当前对象是属于哪一个类的实例。 我们以64位操作系统为例,在未开启指针压缩的情况下,类型指针占用8字节,一旦开启指针压缩,类型指针就会由原来的8字节变为4字节。
对此,这里我们不妨键入下面这条命令看看,JDK8
版本对于指针压缩的设定:
bash
java -xx:+PrintCommandLineFlags -version
从输出结果来看,JDK8
在默认情况下已经开启了普通对象压缩和类型指针压缩:
bash
-XX:InitialHeapSize=262499904 -XX:MaxHeapSize=4199998464 -XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers # 类型指针压缩
-XX:+UseCompressedOops # 普通对象压缩
-XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
对此,我们使用JOL查看一下Object的内存布局,对应示例代码如下:
java
public static void main(String[] args) {
// 打印对象大小
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
从输出结果来看,默认情况下,JDK8
版本的类型指针仅仅在OFFSET
为8的位置占用了4个字节的内存空间:
bash
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
# mark world
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
# 类型指针
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
# 对齐填充
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
而关闭指针压缩(-XX:-UseCompressedClassPointers)
后,类型指针占用字节数为8位。
bash
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
# mark world
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
# 类型指针
8 4 (object header) 00 1c 34 1c (00000000 00011100 00110100 00011100) (473177088)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
为什么需要指针压缩
先来说说第一个原因,默认情况下类型指针为8个字节,相较于指针压缩后的对象,它无缘无故就比后者多了4个字节,这意味着64位对象需要占用更多的堆空间,无意间加快的GC的发生,导致更频繁的GC
。
我们都知道CPU
在进行工作时都需要从内存中加载数据到缓存行中,所以为了保证有限的缓存行可以缓存尽可能多的数据,通过压缩指针的字节数,减小对象的体积,从而提升CPU
缓存的对象数,进而提升CPU缓存命中率
,提高程序的执行效率。
指针压缩引发的寻址问题和解决方案
类型指针占用内存空间由8byte
改为4byte
,就意味原本类型指针可以记录18446T
的内存地址(2^64)
变为4G(2^32)
,因为一个指针压缩导致64位操作系统的寻址范围大大减少,很明显这是得不偿失的。
所以设计者们就提出让压缩后的指针1个bit
表示8个bit
的内存空间,即假设我们的对象类型指针数据为200,这就意味着当前类型指针实际的地址起始地址为200*8
即1600
位置。
提出等比记录
的概念之后,又一个问题诞生了,假设我们的其中一个对象的地址为3567
,因为1个bit表示8bit的数据,可得:
bash
3567/8=445.875
即说明当前对象首地址在445bit
和446bit
之间的一个位置。
很明显无论如何计算都无法精确定位到这个对象的位置,所以为了解决等比记录后无法精确定位对象地址的问题,设计者们就用到了8位对齐的做法。 由上可知,我们的压缩后的指针记录地址都是8倍的地址值,这也就意味着只要我们的地址能够被8整除,那么元数据地址就可以被精准定位,所以上述3567
字节进行8位对齐后大小为3568
,这样以来,压缩指针只需定位从446bit(3568/8)
开始即可精确记录对象地址信息了。
指针压缩的局限性
由上可知,指针压缩后最大寻址范围是32G
,所以一旦我们设置堆内存超过32G(-Xms32g -Xmx32g)
,那么指针压缩就会自动时效。这也是为什么很多Java第三方工具都建议单个运行实例的内存设置不要超过32GB
的根本原因。
小结
相信通过本文对于java对象内存布局的剖析打印以及图文梳理,读者可以非常清晰了解了指针压缩的作用、使用场景还有局限性。如果你觉得文章对你有帮助也非常希望你帮忙点赞、转发。
我是sharkchili ,CSDN Java 领域博客专家 ,开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程 、JVM 、MySQL数据库个人专栏导航。
参考
聊一聊JAVA指针压缩的实现原理(图文并茂,让你秒懂):blog.csdn.net/liujianyang...
JVM的指针压缩:zhuanlan.zhihu.com/p/491684586