理解JVM指针压缩

在使用ElasticSearch时,看到提示jvm内存不要超过32G,超过32G会导致jvm指针压缩失效,会降低ES的性能。心中不禁感觉到奇怪,内存这种宝贵的资源不是越多越好吗,随即找相关的内容看,狠狠的涨了一波知识。

java对象内存结构

可以知道jvm内存大小会影响指针压缩的生效,jvm使用内存占比最大的地方是用来存储对象的堆内存,所以要理解指针压缩就得先弄清楚java中对象的内存结构

java对象在内存中以二分法的方式存储,对象实例在堆中存储的结构如上图,总体分三个部分:对象头,实例数据,对象填充。

  1. 对象头中存储信息也分两个部分,首先是markOop区域,主要存储对象运行时的数据,对象的hashcode,GC的年龄标识,锁信息等。第二个区域是klassOop,存储的是对象类型指针,指向方法区的对象所属类的元数据信息。
    • 对象头中markOop占8B,类型指针占用内存大小和指针压缩参数有关。在不开启指针压缩参数(-XX:-UseCompressedOops)的情况下,32位系统和64位系统中分别占用4B和8B的内存大小。在开启指针压缩参数(-XX:+UseCompressedOops)的情况下,32位系统和64位系统中分别占用4B和4B的内存大小。
    • 如果对象是一个数组类型的话,对象头中还会有一个4B大小的空间记录数组长度。
  2. 第二个部分则存放着对象的实例数据。实列数据分为基础数据和引用数据
    • 基础数据
      • long|double占用8个字节。
      • int|float占用4个字节。
      • short|char占用2个字节。
      • byte|boolean占用1个字节。
    • 引用数据
      • 引用数据的内存分配模式和对象头中的类型指针的内存分配模式一样。在不开启指针压缩参数(-XX:-UseCompressedOops)的情况下,32位系统和64位系统中分别占用4B和8B的内存大小。在开启指针压缩参数(-XX:+UseCompressedOops)的情况下,32位系统和64位系统中分别占用4B和4B的内存大小。
  3. 最后一部分则是对象填充。
    • 对象填充是jvm优化性能的手段,填充的大小和机器位数以及配置的参数有关,优化原理会在下面内容中体现,这里就不详细描述。

为什么引用大小在32位机上占用4B,在64位机上占用8B?

java中,引用类型保存的是被引用对象的内存地址,上面内容中提到的类型指针和引用数据都是存储着类型或对象的内存地址。32位机中内存地址是由32个bit表示,也就是4个字节,而计算机中内存的基本地址是字节,所以32位机通过内存地址能索引的内存大小是2^32*1B=4G,64位机中的内存地址则是由64个bit表示,同理能索引的内存大小则是2^64*1B=17179869184G。

为什么32位机内存地址是由32个bit表示,为什么....?

看了上面的内容,你可能会有疑惑为什么32位机中内存地址是由32个bit表示,64位由64个bit表示。这个就和计算机中cpu和内存的交互有关:你可以认为32位机中cpu每次从内存中只读取32bit内容,每次也只写入32bit内容,64位机cpu每次只读取64bit内容,也只写入64bit内容。这只是一个比较粗浅的概括,但对于理解JVM指针压缩来讲,记住这一点就足够了,不然这篇文章可能要有数百个为什么小标题。当然,如果还想问为什么可以详细阅读一下计算机组成原理。

对象内存对齐

在上一节的java对象内存结构中我们知道有部分区域是对象填充,这节就来了解一下对象填充。 了解之前我们需要知道一个前置条件:Java对象之间的内存地址需要对齐至8N。8是个默认的大小,具体值可以通过-XX:ObjectAlignmentInBytes这个配置参数修改。

我们可以使用jol-core工具包打印对象内存的分布状态来感受一下

先定义一个对象

arduino 复制代码
public class Object1 {  
    private short l;  
    private String s;  
    public Object1(short l, String s){  
        this.l = l;  
        this.s = s;  
    }  
}

初始化对象并打印对象内存分布信息

上图打印结果的1区域是markOop区域,占8B。

2区域是klassOop区域,因为我的电脑是64位,也没有额外的JVM配置,指针压缩默认开启,所以占用4B空间。

3区域是short基本类型,占2B,但因为Java对象之间的内存地址需要对齐至8N,及下面的String对象的内存起始地址要是8的倍数,所以在offset14的位置有2B的padding gap,即填充地址。这样String对象的内存地址在offset 16位的地方开始,由于指针压缩的开启,占4B地址,又因为对齐规则的存在,在offset20位的地方填充了4B,这样整个对象占用的的大小是8*3=24B大小。


上面例子展示出了对象内存地址对齐和指针压缩的具体作用结果,为了对指针压缩有个更直观的感受,我关闭一下指针压缩再打印一下内存分布信息。设置指针压缩关闭参数 -XX:-UseCompressedOops 运行main方法

1区域是markOop区域,仍然占8B。 2区域是klassOop区域,由于关闭了指针压缩,占用8B。 3区域是short基本类型,占用2B,和开启指针压缩不同的是在offset16的位置填充了6B内存,这样以保证String对象内存offset从3*8的位置开始。 4区域是String对像,关闭指针压缩后占用8B,最后也没有填充地址,因为此时整个对象占用4*8=32B大小,属于8的倍数。

为什么需要对象内存对齐

  • 适配cpu的读取机制,提高cpu操作数据的速度:在标题1中的末尾有提到过64位机cpu每次只读取64bit内容,也只写入64bit内容。当我们8字节对齐后,cpu从内存中读取数据时,能确保能整数次的读取出完整数据,不需要后续对数据进行裁剪,或者是多次读取后拼接内容。
  • 减少内存碎片:内存对齐确保了每个对象都以固定的边界开始,这样可以使得内存分配更加规整和高效。
  • 简化GC过程:JVM垃圾收集器可以依赖于固定的地址边界来快速定位和处理对象。统一边界简化了JVM的垃圾收集算法。
  • 支持指针压缩:有了对齐条件后,指针就可以按照统一规则压缩和解压,详细原理下面会讲到。

指针压缩原理

在内存对齐中的代码示例我们可以知道java对象内存地址满足8N对齐,即对象的起始地址在二进制表示方法下最后3位数永远都是0,那么这三位数在存储的时候可以省略,只要在读取的时候将地址后面加上3个0就行。所以开启指针压缩后,虽然对象的内存地址用4B即32位表示,但是能索引的地址范围实际是(32bit+3bit)^2*1B = 32G,远远超过了32位系统能索引的地址范围。也解释了为什么JVM内存大小设置超过32G会导致指针压缩失效,实际生产中可能还没到32G就失效了,推荐30G左右。

指针压缩带来的好处

  • 减少内存占用:由于每个对象引用的大小从64位减少到32位,因此相应地减少了对象引用的内存占用。在具有大量引用的应用程序中,这可以显著减少堆内存的总体大小。
  • 提高缓存利用率:由于对象引用的大小减少,更多的引用可以被加载到CPU缓存中,这可以提高缓存命中率,从而提高程序的运行效率。
  • 减少垃圾收集开销:内存占用减少意味着垃圾收集器需要处理的数据量减少,这可以减少垃圾收集的时间和频率,从而提高应用程序的性能。
  • 提高数据吞吐量:压缩引用可以让更多的数据在相同的内存空间中存储和处理,这对于内存密集型的应用程序来说是一个显著的优势。
  • 扩展应用程序的可扩展性:在有限的内存资源下,指针压缩使得应用程序能够处理更多的数据,这对于内存受限的环境特别有用。
  • 提升内存分配效率:由于每个对象占用更少的内存,内存分配器可以更高效地分配和管理内存。
相关推荐
TCChzp9 小时前
synchronized全链路解析:从字节码到JVM内核的锁实现与升级策略
java·jvm
埃泽漫笔12 小时前
JVM 基础 - JVM 内存结构
jvm
典孝赢麻崩乐急13 小时前
Java学习---JVM(1)
java·jvm·学习
Devil枫14 小时前
Kotlin项目实战与总结
开发语言·jvm·kotlin
timing99414 小时前
SQLite3 中列(变量)的特殊属性
java·jvm·sqlite
Zhu_S W18 小时前
深入理解Java虚拟机:Java内存区域与内存溢出异常
java·开发语言·jvm
AskHarries18 小时前
深入探索Java虚拟机的神秘接口:JVMTI
java·jvm
清心歌1 天前
JVM字节码加载与存储中的细节
jvm
间彧2 天前
什么是JVM Young GC
java·jvm
顧棟2 天前
JVM本地内存的使用监控情况
jvm