做java开发的同学一般都比较熟悉JVM,那么关于指针压缩这块内容是不是也了解呢,不熟悉的小伙伴往下看吧。

首先说明,本文涉及的JDK版本是1.8,JVM虚拟机是64位的HotSpot实现为准。

java对象结构

了解指针压缩前,需要先搞懂java的实例对象在JVM虚拟机中内存结构是什么样的。

java对象由三部分构成:

  1. 对象头

对象头里也有三部分构成。

  • Markword

存储对象的hashCode、垃圾回收对象的年龄以及锁信息等。

  • 类型指针

对象指向的类信息地址即元数据指针,比如User对象指针指向User.class的JVM内存地址。注意:jdk1.8以后元数据是存在Metaspace里的,jdk1.8之前是在方法区里

  • 数组长度

只有对象是数组的情况下,才有这部分数据,若对象不是数组,则没有这部分,不分配空间。

  1. 对象体

对象里的非静态属性占用的空间(包括父类的所有属性,不区分修饰类型),不包括方法,注意:是非静态属性,属于对象的属性,静态属性是属于类的不在对象上分配空间。如果属性是基本数据类型,则直接存对象本身,如果是引用类型,则存的是对象的指针。

  1. 对齐填充

如果对象头+对象体大小不是8字节的倍数,则通过该部分进行补齐,比如对象头+对象体大小只有30字节,则需要补齐到32字节,这里的对齐填充就是2字节。默认情况下,JVM中对象是以8字节对齐的,若对象头加上对象体是8的倍数时,则不存在字节对齐,否则会填充补齐到8的倍数。

对象结构如下图所示(默认情况下)。

压缩指针的由来

计算机操作系统分32位和64位,这里的位在计算机里是用0和1来表示的,用32个(或64个)二进制0和1的组合来表示内存地址。以32位为例,在普通的内存中,对象的大小最小是以1字节来计算的,通过0和1的排列组合,能够表示寻址的内存空间最大就是2^32个,换算成内存空间就是2 ^ 32 / 1024 / 1024 / 1024 = 4G,也就是说32位的操作系统最大能寻址的内存空间只有4G,同理,64位的操作系统(查阅资料显示其实没有用到64位,最多只用到了48位,这个可自行查阅资料,反正肯定比32位大的多)2 ^ 48 / 1024 / 1024 / 1024 / 1024 = 256TB,这样内存就足够大了,但是目前还没有厂商能生产出这么大的内存。

4G对于现在的java应用系统来说,内存已经算小的了,那我们就会想到使用64位的系统,这样内存就可以更大了,但是当我们准备将32位系统切换到64位系统,起初我们可能会期望系统性能会立马得到提升,但现实情况可能并不是这样的,为什么呢?

(1)32位系统对象指针是4字节,64位系统对象指针是8字节(1位表示1bit,8个bit表示1字节),这样64位系统中的对象引用占用的内存空间是32位系统中的两倍大小,因此间接的导致了在64位系统中更多的内存消耗以及更频繁的GC发生,GC占用的CPU时间越多,那么我们的应用程序占用CPU的时间就越少,响应会变慢,吞吐量会降低。

(2)对象的引用变大了,那么CPU可缓存的对象相对就少了,降低了CPU缓存命中率,增加了对内存的访问,CPU对CPU缓存的访问速度可比对内存的访问速度快太多了,所以大量的对内存访问,会降低CPU的执行效率,增加了执行时间,从而影响性能。

既然32位系统内存不够,64位内存够但又影响性能,那有没有折中方案来解决这两个问题呢,于是聪明的JVM开发者想到了利用压缩指针,在64位的操作系统中利用32位的对象指针引用获得超过4G的内存寻址空间。

如何压缩指针

由于在JVM里,对象都是以8字节对齐的即对象的大小都是8的倍数,所以不管用32位还是64位的二进制表示,末尾3位始终都是0。既然JVM已经知道了这些对象的内存地址后三位始终是0,那么这些无意义的0就没必要在堆中继续存储。相反,我们可以利用存储0的这3位bit存储一些有意义的信息,这样我们就多出3位bit的寻址空间,也就是说如果我们继续使用32位来存储指针,只不过后三位原本用来存储0的bit现在被我们用来存放有意义的地址空间信息,当寻址的时候,JVM将这32位的对象引用左移3位即可(后三位补0)。我们原本32位的内存寻址空间一下变成了35位,可寻址的内存空间变为2 ^ 35 / 1024 / 1024 / 1024 = 32G,也就是说在64位系统JVM的内存可扩大到32G了,基本上可满足大部分应用的使用了。

所以在64位系统下,通过压缩指针我们可以继续使用32位来处理(引用指针由8字节可降低到4字节),存储的时候右移3位,寻址的时候左移3位,如下图所示。



这样一来,JVM虽然额外的执行了一些位运算但是极大的提高了寻址空间,并且将对象引用占用内存大小降低了一半,节省了大量空间,况且这些位运算对于CPU来说是非常容易且轻量的操作,可谓是一举两得。

如何进一步扩大寻址空间

前边提到我们在Java虚拟机堆中对象起始地址均需要对其至8的倍数,不过这个数值我们可以通过JVM参数-XX:ObjectAlignmentInBytes 来改变(默认值为8)。当然这个数值的必须是2的次幂,数值范围需要在8 - 256之间。

正是因为对象地址对齐至8的倍数,才会多出3位bit让我们存储额外的地址信息,进而将4G的寻址空间提升至32G。

同样的道理,如果我们将ObjectAlignmentInBytes的数值设置为16呢?

对象地址均对齐至16的倍数,那么就会多出4位bit让我们存储额外的地址信息。寻址空间变为2 ^ 36 / 1024 / 1024 / 1024 = 64G。

通过以上规律,我们就能知道,在64位系统中开启压缩指针的情况,寻址范围的计算公式:4G * ObjectAlignmentInBytes = 寻址范围。

但是并不建议大家贸然这样做,因为增大了ObjectAlignmentInBytes虽然能扩大寻址范围,但是这同时也可能增加了对象之间的字节填充,导致压缩指针没有达到原本节省空间的效果。

JVM压缩指针参数

可以通过以下命令查看java命令默认的启动参数:

java -XX:+PrintCommandLineFlags -version



通过下面这个命令,可以看到所有JVM参数的默认值。

java -XX:+PrintFlagsFinal -version

关于压缩指针的两个参数:

UseCompressedClassPointers:压缩类指针

UseCompressedOops:压缩普通对象指针

Oops是Ordinary object pointers的缩写,这两个参数默认是开启的,即-XX:+UseCompressedClassPointers,-XX:+UseCompressedOops,也可手动设置,如下所示

-XX:+UseCompressedClassPointers //开启压缩类指针
-XX:-UseCompressedClassPointers //关闭压缩类指针
-XX:+UseCompressedOops //开启压缩普通对象指针
-XX:-UseCompressedOops //关闭压缩普通对象指针

32位HotSpot VM是不支持UseCompressedOops参数的,只有64位HotSpot VM才支持。

Oracle JDK从6 update 23开始在64位系统上会默认开启压缩指针。

实例

使用 JOL 工具分析 Java 对象

maven依赖

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>

常用类及方法

查看对象内部信息: ClassLayout.parseInstance(obj).toPrintable()

查看对象外部信息:GraphLayout.parseInstance(obj).toPrintable()

查看对象占用空间总大小:GraphLayout.parseInstance(obj).totalSize()

查看类内部信息:ClassLayout.parseClass(Object.class).toPrintable()

JOL使用

public class ObjectLayOut1 {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便面");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
System.out.println(ClassLayout.parseInstance(goods).toPrintable());
} } @Setter
class Goods {
private byte b;
private char type;
private short age;
private int no;
private float weight;
private double price;
private long id;
private boolean flag;
private String goodsName;
private LocalDateTime produceTime;
}

输出:

com.star95.study.jvm.Goods object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 int Goods.no 123456
16 8 double Goods.price 1.5
24 8 long Goods.id 111
32 4 float Goods.weight 0.065
36 2 char Goods.type A
38 2 short Goods.age 10
40 1 byte Goods.b 1
41 1 boolean Goods.flag true
42 2 (alignment/padding gap)
44 4 java.lang.String Goods.goodsName (object)
48 4 java.time.LocalDateTime Goods.produceTime (object)
52 4 (object alignment gap)
Instance size: 56 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total

OFF:表示偏移量

SZ:占用字节大小

TYPE DESCRIPTION:类型描述

VALUE:取值,基本类型存的就是实际值,引用类型存的是指针

压缩指针参数组合

以以上代码配合以下jvm参数设置,看压缩的不同结果

一、-XX:+UseCompressedOops -XX:+UseCompressedClassPointers

类指针压缩和普通对象压缩都开启,这个也是jdk1.8默认的,不加这两个参数,压缩也是生效的。



输出:



这里类指针和引用类型的属性都压缩到以4字节保存了,对象占用56个字节,对象大小计算公式:

对象大小=对象头+实例数据+(填充数据)

二、-XX:+UseCompressedOops -XX:-UseCompressedClassPointers

开启普通对象压缩,关闭类指针压缩。

输出:



类指针占8个字节,普通对象指针是4个字节,对象占用56个字节,这里我们也可以看到保存到的顺序改变了,这就是重排序,JVM会根据类型及对齐字节大小合理优化。

三、-XX:-UseCompressedOops -XX:-UseCompressedClassPointers

类指针和普通对象指针压缩都关闭。

输出:



类指针和普通对象指针都占8字节,压缩均关闭的情况下,我们看到对象大小变成了64字节。

四、-XX:-UseCompressedOops -XX:+UseCompressedClassPointers

关闭普通对象压缩,开启类指针压缩。

输出:



我们看到,类指针占8字节,对象指针占8字节,底部还有一个警告,意思是说类指针压缩依赖普通对象压缩,这种相当于关闭了类指针压缩和普通对象压缩。

  • 开启 UseCompressedOops 时,UseCompressedClassPointers 会默认自动开启,可手工设置UseCompressedClassPointers的开启或关闭。
  • 关闭 UseCompressedOops 时,UseCompressedClassPointers 也会默认自动关闭,无法设置UseCompressedClassPointers,设置了也不会生效。

内存大小对压缩的影响

结合以上代码例子,我们通过设置jvm堆内存大小,看看是否影响类指针压缩和普通对象压缩。

设置jvm参数:

-Xmx31g -Xms31g -XX:+UseCompressedOops -XX:+UseCompressedClassPointers





类指针和对象指针都被压缩到4字节了。

接着我们把堆内存加到32g试试。

-Xmx32g -Xms32g -XX:+UseCompressedOops -XX:+UseCompressedClassPointers



堆内存加到32g,虽然类指针压缩和对象压缩都开启了,但是输出的还是各占8字节,说明压缩失效了,而且最后JVM还会发出警告,意思是64位的虚拟机,对开启压缩来说堆内存太大了。

为什么呢?

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

因此,其能够识别的最大内存就是4G*8byte=32GB,所以内存过大,压缩自动失效了。

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

总结

  • 通过指针压缩,利用对齐填充特性,通过映射方式达到了内存地址扩展的效果
  • 指针压缩能够节省内存空间,同时提高了程序的寻址效率
  • 堆内存设置最好不要超过32GB,这时指针压缩将会失效,造成空间的浪费
  • 指针压缩不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段指针(包括引用类型的数组指针)
  • -XX:ObjectAlignmentInBytes,默认是 8,也就是 8 字节对齐

JVM之指针压缩的更多相关文章

  1. 「每日五分钟,玩转JVM」:指针压缩

    64位JVM和32位JVM 最初的时候,JVM是32位的,但是随着64位系统的兴起,JVM也迎来了从32位到64位的转换,32位的JVM对比64位的内存容量比较有限,但是我们使用64位虚拟机的同时,也 ...

  2. 码农会锁,synchronized 对象头结构(mark-word、Klass Pointer)、指针压缩、锁竞争,源码解毒、深度分析!

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 感觉什么都不会,从哪开始呀! 这是最近我总能被问到的问题,也确实是.一个初入编程职场 ...

  3. 为什么JVM要用到压缩指针?Java对象要求8字节的整数倍?

    前言 前两天在一个帖子中看到一道面试题: 堆内存超过32G时,为什么压缩指针失效? 之前没有了解过这方面的知识,于是开始google起来,但当我翻看了不下一页的帖子,我都仍然没有搞懂,因为好多答案给我 ...

  4. 阿里面试官:小伙子,你给我说一下JVM对象创建与内存分配机制吧

    内存分配机制   逐步分析 类加载检查: 虚拟机遇到一条new指令(new关键字.对象的克隆.对象的序列化等)时,会先去检查这个指令的参数在常量池中定位到一个类的符号引用,并且这个符号引用代表的类是否 ...

  5. JVM的艺术-对象创建与内存分配机制深度剖析

    JVM的艺术-对象创建与内存分配机制深度剖析 引言 本章将介绍jvm的对象创建与内存分配.彻底带你了解jvm的创建过程以及内存分配的原理和区域,以及包含的内容. 对象的创建 类加载的过程 固定的类加载 ...

  6. JVM专题1: 类和类加载机制

    合集目录 JVM专题1: 类和类加载机制 Java对象的结构 在HotSpot虚拟机中, 对象在内存中存储的布局可以分为3块区域 对象头Header 实例数据Instance Data 对齐填充Pad ...

  7. 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用

    欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...

  8. Java8 jvm参数

    jmap输出 [tomcat@n01 ~]$ /opt/java/jdk1..0_101/bin/jmap -heap Attaching to process ID , please wait... ...

  9. Java性能优化权威指南-读书笔记(二)-JVM性能调优-概述

    概述:JVM性能调优没有一个非常固定的设置,比如堆大小设置多少,老年代设置多少.而是要根据实际的应用程序的系统需求,实际的活跃内存等确定.正文: JVM调优工作流程 整个调优过程是不断重复的一个迭代, ...

  10. Twitter 工程师谈 JVM 调优

    一. 调优需要关注的几个方面 内存调优 CPU 使用调优 锁竞争调优 I/O 调优 二. Twitter 最大的敌人:延迟 导致延迟的几个原因? 最大影响因素是 GC 其他的有:锁和线程调度.I/O. ...

随机推荐

  1. pysimplegui之系统托盘图标创建

    在 PySimpleGUI(tkinter 版本)上运行时,系统托盘图标为 PNG 和 GIF 格式.PNG.GIF 和 ICO 格式适用于 Wx 和 Qt 端口. 指定"图标"时 ...

  2. Linux(二)文件权限和压缩

    1 搜索查找类 1.1 查找定位文件 find <搜索范围.路径> <选项> find将从指定目录下递归地遍历其各个子目录,将满足条件的文件显示在终端. 选项说明 -name: ...

  3. Arch Linux配置Java学习环境

    1. JDK JDK8:主流版本 $ sudo pacman -S jdk8-openjdk JDK11:将会是下一个主流版本 $ sudo pacman -S jdk11-openjdk JDK19 ...

  4. 20130625-关于mac配置android cocos2dx

    1.下载cocos2dx  ndk  eclipse http://developer.android.com/tools/sdk/ndk/index.html 2.cocos2dx文件中找到crea ...

  5. [C++核心编程] 4.7 多态

    文章目录 4.7 多态 4.7.1 多态的基本概念 4.7.2 多态案例一-计算器类 4.7.3 纯虚函数和抽象类 4.7.4 多态案例二-制作饮品 4.7.5 虚析构和纯虚析构 4.7.6 多态案例 ...

  6. 沁恒 CH32V208(三): CH32V208 Ubuntu22.04 Makefile VSCode环境配置

    目录 沁恒 CH32V208(一): CH32V208WBU6 评估板上手报告和Win10环境配置 沁恒 CH32V208(二): CH32V208的储存结构, 启动模式和时钟 沁恒 CH32V208 ...

  7. pytest的几种执行方式

    1 pytest xxxx 2 python -m pytest xxxx python -m pytest --html=./report/rep2.html test_env_pytest_ini ...

  8. 2022-01-04:一个无序数组长度为n,所有数字都不一样,并且值都在[0...n-1]范围上。 返回让这个无序数组变成有序数组的最小交换次数。 来自小红书。

    2022-01-04:一个无序数组长度为n,所有数字都不一样,并且值都在[0-n-1]范围上. 返回让这个无序数组变成有序数组的最小交换次数. 来自小红书. 答案2022-01-04: 下标循环怼. ...

  9. 2021-08-20:打砖块。有一个 m x n 的二元网格,其中 1 表示砖块,0 表示空白。砖块 稳定(不会掉落)的前提是:1.一块砖直接连接到网格的顶部,或者,2.至少有一块相邻(4 个方向之一

    2021-08-20:打砖块.有一个 m x n 的二元网格,其中 1 表示砖块,0 表示空白.砖块 稳定(不会掉落)的前提是:1.一块砖直接连接到网格的顶部,或者,2.至少有一块相邻(4 个方向之一 ...

  10. Vue选日期滚动条自动定位到选定的日期位置

    html 这里的关键点就是   :id="'scroll'+index" 以及 :scroll-into-view="intoIndex" <view c ...