聊一聊JAVA指针压缩的实现原理(图文并茂,让你秒懂)

前言

在网上大家很多都看到过这样一句话:“JVM内存最好不要超过32G”。
今天我们就来分析一下为什么?32G到底是怎么算出来的。

JAVA对象模型

我们先了解一下,一个JAVA对象的存储结构。在Hotspot虚拟机中,对象在内存中的存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
在这里插入图片描述

1、对象头(Header)

对象头,又包括三部分:MarkWord、元数据指针、数组长度。
下面图是64位系统下的对象头布局

MarkWord:用于存储对象运行时的数据,比如HashCode、锁状态标志、GC分代年龄等。这部分在64位操作系统下,占8字节(64bit),在32位操作系统下,占4字节(32bit)。
具体的作用可以参考我之前写的synchronized锁原理分析(一、从Java对象头看synchronized锁的状态)

指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
这部分就涉及到一个指针压缩的概念,在开启指针压缩的情况下,占4字节(32bit),未开启情况下,占8字节(64bit),现在JVM在1.6之后,在64位操作系统下都是默认开启的。

数组长度:这部分只有是数组对象才有,如果是非数组对象,就没这部分了,这部分占4字节(32bit)。
java对象头

2、实例数据(Instance Data)

用于存储对象中的各种类型的字段信息(包括从父类继承来的)。

3、对齐填充(Padding)

java对象的大小默认是按照8字节对齐,也就是说java对象的大小必须是8字节的倍数。如果算到最后不够8字节的话,那么就会进行对齐填充。
那么为什么非要进行8字节对齐呢?这样岂不是浪费了空间资源?

Scott oaks在书上给出的理由是:
其实在JVM中(不管是32位的还是64位的),对象已经按8字节边界对齐了;对于大部分处理器,这种对齐方案都是最优的。所以使用压缩的oop并不会损失什么。如果JVM
中的第一个对象保存到位置0,占用57字节,那下一个对象就要保存到位置64,浪费了7
字节,无法再分配。这种内存取舍是值得的(而且不管是否使用压缩的oop,都是这样),因为在8字节对齐的位置,对象可以更快地访问。
不过这也是为什么JVM没有尝试模仿36位引用(可以访问64GB的内存)的原因。在那种情况下,对象就要在16字节的边界上对齐,在堆中保存压缩指针所节约的成本,就被为对齐对象而浪费的内存抵消了。

也就说,8字节对齐,是为了效率的提升,以空间换时间的一种方案。当然你还可以16字节对齐。但是8字节是最优选择。

指针压缩原理

为什么对象指针,可以用8字节存也可以用4字节存?4字节存不会有什么问题吗?

1、不开启指针压缩

首先,我们来分析如果不开启指针压缩的情况下是,java对象是怎么存储的

在这里插入图片描述

采用8字节(64位)存储真实内存地址,比之前采用4字节(32位)压缩存储地址带来的问题:

  1. 增加了GC开销:64位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,
    从而加快了GC的发生,更频繁的进行GC。

  2. 降低CPU缓存命中率:64位对象引用增大了,CPU能缓存的oop将会更少,从而降低了CPU缓存的效率。

2、开启指针压缩

既然64位存储内存地址,会导致了这么多问题,那么我们可以不可以找一种方法,既使用之前的4字节(32位)存指针地址,又可以扩大内存的方法呢?

答案就是采用指针压缩技术!!

4个字节,32位,可以表示232 个地址,如果这个地址是真实内存地址的话,那么由于CPU寻址的最小单位是byte,也就是 232 byte = 4GB。

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

上面这个原理一定要明白,才能理解下面的指针压缩原理,不明白可以先看一下我的这篇文章。

32位CPU最多支持4G内存是怎么算出来的?(解惑篇)

看完上面的文章之后,继续往下进行。

4字节,8位最大表示4GB内存。那么Java是怎么做到 4个字节表示32GB呢?怎有扩大了8倍???

这就要使用到之前提到的Java的对齐填充机制了。
Java的8字节对齐填充,就像是内存的8bit为一组,变为1byte一样。
这里的压缩指针,不是真实的操作系统内存地址,而是Java进行8byte映射之后的地址,所以也相对于操作系统的指针有进行的8倍的扩容。

看下图:
JVM就将堆内存进行了块划分,以8字节为最小单位进行划分。
在这里插入图片描述
将java堆内存进行8字节划分

java对象的指针地址就可以不用存对象的真实的64位地址了,而是可以存一个映射地址编号。
这样4字节就可以表示出2^32个地址,而每一个地址对应的又是8byte的内存块。
所以,再乘以8以后,一换算,就可以表示出32GB的内存空间。

这里很巧妙的运用了java对齐填充的特性,通过映射的方式达到了内存扩充的效果。

想一想?这里运用的原理是不是和操作系统32位表示4GB内存的原理一毛一样!!!
我想着也是java做对齐填充的一重大原因吧!!

也就解释了为什么当内存大于32GB时,开启指针压缩的参数会失效!
所以也网上建议大家在64位系统系下,JAVA的堆内存设置最好不要超过32G,一旦超过32G后,指针压缩就会失效,然后带来GC的触发频次变高,而且造成空间浪费。

注意:
32G是个近似值,这个临界值跟JVM和平台有关,当我们线上真正启动服务的时候直接设置 -Xmx=32GB 的时候很可能导致 CompressedOop 失效,那我们怎么确定当前环境下最大内存设置多大才且最大限度的使用内存才能启动 CompressedOop 呢?我们可以通过增加JVM参数 -XX:+PrintFlagsFinal,验证UseCompressedOops的值,从而得知,到底是不是真的开启了压缩指针,还是压缩指针失效!

  • 36
    点赞
  • 101
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论
Java阻塞队列是一种线程安全的数据结构,它提供了同步的功能,用于在多线程环境中安全地进行数据交换和通信。其实现原理主要涉及以下几个方面。 首先,阻塞队列的实现会使用锁机制确保线程安全。Java中可以使用ReentrantLock或synchronized关键字来实现锁,在对队列进行操作时会对其进行加锁,保证同一时刻只有一个线程能够访问队列。 其次,阻塞队列内部会使用条件变量或信号量来实现线程间的协调与通信。当队列为空时,消费者线程需要等待直到队列有数据可取;当队列已满时,生产者线程需要等待直到队列有空位置可放入新数据。通过条件变量或信号量的等待和唤醒机制,实现了线程间的同步和互斥。 此外,阻塞队列通常还会使用一个循环数组来存储数据。循环数组在插入和删除元素时能够高效地利用已分配的内存空间,避免了频繁的扩容和内存拷贝。同时,循环数组的读写指针可以通过取模运算得到,实现环形循环。 最后,阻塞队列还会根据不同的需求提供不同的阻塞操作。例如,用于插入元素的put()方法在队列已满时会阻塞直到有空位置可用,用于获取元素的take()方法在队列为空时会阻塞直到有数据可取。这些阻塞操作的实现依赖于同步和协调机制,保证了线程安全和数据一致性。 总之,Java阻塞队列通过使用锁、条件变量或信号量、循环数组等机制,实现了线程安全和线程间的同步与通信。它是多线程编程中常用的工具,能够有效地管理数据的生产和消费,提高多线程程序的可靠性和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jianyang.liu

从来没收到过一分钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值