来聊聊大厂面试常问的指针压缩

写在文章开头

近期和朋友聊天时聊到一些比较有趣的面试题?

1.指针压缩了解过吗?它解决什么问题? 2. 为什么设置JVM内存时不建议设置超过32G?它和指针压缩有什么关系?

针对上述两道面试题,笔者就以此文展开深入讲解:

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

指针压缩详解

Java对象内存布局

我们都知道Hotspot虚拟机在中的java对象的整体布局为:

  1. 对象头
  2. 实例数据
  3. 对齐填充

其中对象头由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*81600位置。

提出等比记录的概念之后,又一个问题诞生了,假设我们的其中一个对象的地址为3567,因为1个bit表示8bit的数据,可得:

bash 复制代码
3567/8=445.875

即说明当前对象首地址在445bit446bit之间的一个位置。

很明显无论如何计算都无法精确定位到这个对象的位置,所以为了解决等比记录后无法精确定位对象地址的问题,设计者们就用到了8位对齐的做法。 由上可知,我们的压缩后的指针记录地址都是8倍的地址值,这也就意味着只要我们的地址能够被8整除,那么元数据地址就可以被精准定位,所以上述3567字节进行8位对齐后大小为3568,这样以来,压缩指针只需定位从446bit(3568/8)开始即可精确记录对象地址信息了。

指针压缩的局限性

由上可知,指针压缩后最大寻址范围是32G,所以一旦我们设置堆内存超过32G(-Xms32g -Xmx32g),那么指针压缩就会自动时效。这也是为什么很多Java第三方工具都建议单个运行实例的内存设置不要超过32GB的根本原因。

小结

相信通过本文对于java对象内存布局的剖析打印以及图文梳理,读者可以非常清晰了解了指针压缩的作用、使用场景还有局限性。如果你觉得文章对你有帮助也非常希望你帮忙点赞、转发。

我是sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考

聊一聊JAVA指针压缩的实现原理(图文并茂,让你秒懂):blog.csdn.net/liujianyang...

JVM的指针压缩:zhuanlan.zhihu.com/p/491684586

相关推荐
Estar.Lee8 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书1 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang1 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net
Rverdoser3 小时前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
Tech Synapse3 小时前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴3 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
微信-since811924 小时前
[ruby on rails] 安装docker
后端·docker·ruby on rails
代码吐槽菌6 小时前
基于SSM的毕业论文管理系统【附源码】
java·开发语言·数据库·后端·ssm
豌豆花下猫6 小时前
Python 潮流周刊#78:async/await 是糟糕的设计(摘要)
后端·python·ai