为什么JVM开启指针压缩后支持的最大堆内存是32G?

如果配置最大堆内存超过 32 GB(当 JVM 是 8 字节对齐),那么压缩指针会失效。 但是,这个 32 GB 是和字节对齐大小相关的,也就是-XX:ObjectAlignmentInBytes配置的大小(默认为8字节,也就是 Java 默认是 8 字节对齐)。-XX:ObjectAlignmentInBytes可以设置为 8 的整数倍,最大 128。也就是如果配置-XX:ObjectAlignmentInBytes为 24,那么配置最大堆内存超过 96 GB 压缩指针才会失效。

前言

作为Java程序员,当我们在命令行提交一个Java任务时,一般我们会显性的设置一些参数,最常见的如指定堆内存大小,程序参数等,来满足你程序的运行需求。

但是,在设置JVM内存大小时,我建议你最好不要超过32G这个阈值,因为一旦超过,你的原本的内存占用可能会瞬间剧增。

为什么?

想搞清楚这个问题,今天就来跟你聊聊下面这两个参数:

-XX:+UseCompressedClassPointers //压缩类指针
-XX:+UseCompressedOops  //压缩普通对象指针
-XX:+UseCompressedClassPointers:表示是否启用类指针压缩,因为对于任何一个jvm中的对象而言,其内部都有一个指向自己对应类(属于哪个class)的指针(Java习惯叫引用),在64位的Java虚拟机中,默认是启动压缩的;
-XX:+UseCompressedOops:表示是否使用普通对象指针压缩,Oops是Ordinary object pointers的缩写,就是任何指向一个在堆中的对象(非简单类型)的指针,默认也是启动压缩的。

这两个参数都是jvm默认的,一般我们不会去过多关注,很多同学甚至都没有见过,但是面试时,很多时候就会问。

01. 为什么要压缩指针

我们知道,计算机的操作系统有32位和64位之分,这个位其实指的是CPU在内存中的寻址能力。

32位就是32bit,任何一个bit要么是0要么就是1,因此32bit通过排列组合能够带来的不同数值就是:2^32=4G个对象,而因为在普通内存中,对象的大小最小以1byte来计算,所以32位操作系统能够寻址的内存空间最大也就4G,多了也是浪费。

同理,64位的操作系统的寻址空间就是:2^64=16777216TB,吓人不,估计在地球毁灭前都用不完。虽然可以支持这么大的寻址空间,但目前还没有哪个硬件厂商能造出这么大的内存。

那对应到我们的Java虚拟机上,其内存寻址是什么样的呢?

跟操作系统一样,我们先看下一个常用的JVM的规格:

可以看到这是一个JDK版本为1.8的64位虚拟机,所谓虚拟机,其实可以把它理解为一个专门运行Java指令的操作系统。

上面说到,所谓64位指的就是向内存对象寻址的能力,而如何找到目标对象,靠的就是指针(Java叫引用),既然是指针,就有大小。

多大呢?应该是64bit,即8byte。

在验证这个指针大小之前,我们先来看下一个对象的在内存中结构是什么样的?

  1. Markword:对象头,用来标记对象状态的,比如hascode、锁状态信息等;
  2. Class pointer:类型指针,用来指向方法区(method area)中该对象属于哪个类,比如A对象指向A.class;
  3. Instance data:实例数据,指的是对象中的成员变量,如果是简单类型,则直接存对象本身,如果是非简单类型,则保存对象的指针(引用);
  4. Padding:内存对齐,对于64位Java虚拟机来说,对象为8字节(64bit)对齐,如果对象大小不满8字节的倍数,则通过该部分进行补齐,比如对象时间大小只有12Byte,则需要补齐到16Byte,这样做最大的好处在于寻址效率高(知道为什么数组可以用下标直接读取数据吗?就是因为每个对象大小一样且挨在一起)。

接下来,看这么个例子:

public class A {
    private Object object = new Object();

    public static void main(String[] args) {
        //打印出对象的内存布局
        System.out.println(ClassLayout.parseInstance(new A()).toPrintable());
    }
}
注意,这里除了依赖JDK本身外,还需要依赖一个第3方 jar包,用来专门查看对象的内存布局情况。
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.13</version>
</dependency>

用最简单的方式运行一下:

可以看到将A对象打印出来,一共分为下面3个部分,内存总占用为16byte。

1代表的Markword部分,由两个4byte组成;

2代表class pointer部分,居然只占用了4byte,不是说好了64位虚拟机需要占用8byte吗?

3代表对象指针,同样居然也只有4byte,而不是8byte;

难道上面的推测是错的吗?当然不是。

原因在于,JVM在启动时默认开启了指针压缩功能

我们来看一下,当我们在运行一个Java程序时,在什么参数都不指定的情况下,JVM默认开启了哪些参数设置。

查看默认情况下JVM开启的参数

用这个命令就可以看到,JVM自动给你设置了5个额外的参数,包括最小heap大小,和最大heap大小,GC方式,以及2个指针压缩的选项。

就是这2个默认的指针压缩配置,导致了上面我们打印一个对象时,其成员变量在内存布局中的指针只有4byte。

我们来验证一下:

1,先关闭掉类压缩指针,关闭参数为:-XX:-UseCompressedClassPointers,意思是不启用类指针压缩:

关闭类指针压缩

可以看到,其内存布局由之前的3部分,变成了现在的4部分,第2部分的类指针由原来的4byte变成了现在的8byte,因为我把类指针压缩给关闭了。

那为什么多出来个第4部分呢,原因也很简单,上面说了,在64位JVM中任何一个对象的大小必须满足8byte对齐,即8byte的整数倍,如果不满足就用要额外的空间来填充,上面3部分加起来只有20byte,不满足8的倍数,因此需要额外再加4byte到24byte,刚好为8的3倍。

2,再来验证下普通对象指针,把压缩功能给关闭了(-XX:-UseCompressedOops),会是个什么情况:

再关闭普通对象指针压缩

可以看到,第3部分的普通对象指针已经从原来的4byte变成了8byte,而因为这3部分的大小加起来刚好是8byte的3倍,因此不需要额外的padding,也就没有了刚才第4部分。

这2步就证明了之前的推论

至于为什么要这么做,原因也很简单,因为计算机的内存资源向来都是硬件资源中最昂贵的资源之一,如果有手段将原本占用内存太大的部分进行有效合理的减少,那当然是一件值得庆幸的事情。

02. 指针压缩支持的内存范围

在文章一开始部分就提到,指针的大小直接决定了CPU能够识别的内存范围,那么JVM既然将原本8byte的指针压缩到了4byte,那么其能够识别的内存是不是也对应缩小了呢?

来做个试验,这次,我们不刻意关闭指针压缩功能(2个都不关闭),只控制JVM的内存大小,先看下面这个:

当指定堆内存为31G时

可以看到,我在命令行中把堆的最大和最小内存都限制在了31G,因此这个JVM在任何时候都会维持31G的堆内存,而这个时候可以看到,无论是类指针还是对象指针都是被压缩的4byte。

好,接着我把内存再次加大试试:

当指定堆内存为32G时

看到没,神奇的事情发生了,我仅仅只是把内存从31G增加到32G,没有添加任何不压缩指针的参数,但是从结果中却可以看到,无论是类指针还是对象指针都由原来的4byte变成了8byte,即不再对任何指针进行压缩。

有同学可能会怀疑,是不是这个时候JVM把压缩指针的功能关闭了啊,那我们把这两个指针压缩参数显示地打开,再验证一下:

指定堆内存为32G,且显示打开指针压缩

你会发现,即便把压缩配置打开,也没有用,也就是说,当JVM的堆内存一旦超过32G,其指针压缩功能则自动失效

为什么呢?

我们来算一下,既然指针压缩到了4byte,也就是32bit,同样按照之前算的,用排列组合的方式可以识别2^32个对象,也就是4G个对象,刚才同样也说了,在Java中,非简单对象都是必须以8byte对齐。

因此,其能够识别的最大内存就是4G*8byte=32GB

这也是为什么很多Java服务在运行中,官方都建议单个运行实例的内存设置不要超过32GB的根本原因。典型的如Elasticsearch,很多资料都说设置JVM大小不要超过32G,但是很少有提到为什么。


作者:Hashcon
链接:https://www.zhihu.com/question/365436606/answer/2317862899
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值