Java对象的内存结构

概述

Java 对象的内存结构对于理解 Java 内存管理和性能优化至关重要。本文将详细介绍 Java 对象的内部结构,并提供查看这些信息的方法。
Java 对象由三个主要部分组成:

  • 对象头 (Header)
  • 实例数据 (Instance Data)
  • 填充数据 (Padding Data)
    在这里插入图片描述
    HotSpot关于对象头的相关说明:
    在这里插入图片描述
    图片来源:openjdk 8的文件markOop.hpp

1. 对象头 (Header)

对象头主要用于存储对象自身的运行时数据,如哈希码、分代年龄、锁状态等。
一般对象头分为三部分:

  • Mark Word:存储对象的哈希值、分代年龄、锁状态等。
  • Class Pointer:存储对象的类的元数据的指针。
  • Array Length:用于存储数组的长度,占4字节。

Mark Word

Mark Word 是对象头中最关键的部分,它包含了对象的哈希值、分代年龄、锁状态等。Mark Word 的具体内容会根据锁的状态改变而改变。

众所周知,计算机分为32位和64位,操作系统也分为32位和64位。32位计算机需要安装32位的操作系统,64位计算机需要安装64位系统。32位操作系统最大内存是4GB,因为32位二进制能够表示的最大数是2^32,即22102410241024。而64位操作系统能够访问的最大内存是2^64,即16EB1

1. 32位HotSpot虚拟机中的MarkWord

在这里插入图片描述

2. 64位HotSpot虚拟机中的MarkWord

在这里插入图片描述
考虑到现在基本都是在64位操作系统上安装了64位的JDK,JVM自然也是64位的。本文以64位HotSpot为例,介绍Mark Word结构,其他JVM中的Java对象内存结构可能有所差异,大家可以自行查阅相关资料。

Mark Word中各字段是什么含义呢?

  • hashCode:按原始内容计算的hashcode,重写过hashcode方法的计算结果不会存这里。
    如果对象没有重写hashcode方法,那么默认是调用os::random产生hashcode,可以通过System.identityHashCode获取;os::random产生hashcode的规则为:next_rand = (16807 seed) mod (2 ^ 31 - 1),因此可以使用31位存储;另外一旦生成了hashcode,JVM会将其记录在MarkWord中。
  • 分代年龄:在GC中,当survivor区中对象复制一次,年龄加1。如果到15之后会移动到老年代,并发GC的年龄阈值为6。
  • 是否偏向锁:对象是否存在偏向锁标记
  • 锁标志位:对象的锁标志状态。在并发的情况下,可以通过锁标志判断象是否被线程占用。01是初始状态,未加锁。随着锁级别的不同,对象头里会存储不同的内容。
    • 偏向锁存储的是当前占用此对象的线程ID(此处的线程ID是操作系统层面的线程唯一ID,与Java中的线程ID是不一致的,了解即可)。
    • 轻量级锁存储的是指向线程栈中锁记录的指针
    • 重量级锁存储的是指向互斥锁的指针
  • Epoch:偏向锁的时间戳
(是否偏向锁)锁标志位 2bit锁状态
0(代表无锁)01无锁态(new)
1(偏向锁)01偏向锁
-00轻量级锁(自旋锁、无锁、自适应自旋锁)
-10重量级锁
-11GC 标记

TODO:以上各种锁的应用及升级情形也是一个大工程,容我以后再补。^_^

可以看到64位虚拟机其实是浪费了一部分空间的,JVM支持通过-XX:+UseCompressedOops -XX:-UseCompressedClassPointers参数来进行指针压缩。

Class Pointer

Class Pointer,也叫Class Metadata Address,这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过它确定对象是哪个类的实例。该指针的位长度为JVM的一个Word大小,即 32位的JVM 为 32位,64位的JVM为 64位。
如果应用的对象过多,使用 64位的指针将浪费大量内存,统计而言,64位的 JVM将会比 32位的 JVM多耗费 50%的内存。为了节约内存可以使用选项 +UseCompressedOops开启指针压缩,其中,oop即 ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个 Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针 JVM不会优化,比如指向 PermGen的 Class对象指针(JDK8中指向元空间的 Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
在64位JVM虚拟机中Mark Word、Class Pointer这两部分都是64位的,所以也就是需要128位大小(16 bytes)。从JDK1.6 update14开始,在64位操作系统中,JVM开始支持指针压缩。JDK1.8默认开启了指针压缩。

参数作用
-XX:+UseCompressedOops开启对象指针压缩
-XX:-UseCompressedOops关闭对象指针压缩
-XX:+UseCompressedClassPointers开启Class Pointer指针压缩
-XX:-UseCompressedClassPointers关闭Class Pointer指针压缩

一般只有虚拟机内存达到32G以上,4个字节已经无法满足寻址需求时,才需要关闭指针压缩

64位HotSpot开启压缩的规则:

  • 4G以下,直接砍掉高32位
  • 4G-32G,默认开启指针压缩(Oops & Class Pointer)
  • 32G以上,压缩无效,使用64位

Array Length

用于存储数组的长度,这是数组类型的对象独有的,占用4字节。

指针压缩原理

在JVM里,对象都是以8字节对齐的,即对象的大小都是8的倍数,所以不管在32位还是64位的JVM里对象地址的末尾3位始终都是0。
例如:

  • 08 = 00001000
  • 16 = 00010000
  • 24 = 00011000
  • 32 = 00100000

既然JVM已经知道了这些对象的内存地址后三位始终是0,那么这些0就没必要在内存中继续存储。相反,我们可以利用这3位存储一些有意义的信息,这样我们就多出3位的寻址空间,也就是说如果我们继续使用32位来存储指针,只不过后三位被我们用来存放有意义的地址空间信息,当寻址的时候,JVM将这32位的对象引用左移3位(后三位补0)即可。我们原本32位的内存寻址空间一下变成了35位,可寻址的内存空间变为2 ^ 35,2 ^ 5 * 1024 * 1024 * 1024 = 32G,也就是说在32位系统JVM的内存可扩大到32G了,基本上可满足大部分应用的使用了。
现在电脑内存普遍在8GB、16GB,而16GB内存的地址空间范围是0000000000000000000000000000000000 - 1111111111111111111111111111111111。
假如创建对象时申请到一个内存地址,位置是8589934616(2 ^ 33 + 3 * 8),下面以这个地址演示指针压缩的原理。
指针压缩有两个过程,分别是编码和解码。

  • 编码:编码是指获得到内存地址后,右移三位,得到压缩后的内存地址。
    8589934616转换为二进制为1000000000000000000000000000011000,右移三位得到1000000000000000000000000000011,即1073741827,它就是压缩后的内存地址。
  • 解码:解码是在操作内存前,把原来地址左移三位,得到真实的内存地址。
    1073741827转换为二进制为1000000000000000000000000000011,左移三位得到1000000000000000000000000000011000,即8589934616,它就是真实的内存地址。
    在这里插入图片描述
    32位系统的最大内存地址是2 ^ 32,即4294967296。它小于上述演示地址8589934616,所以32位系统不能直接操作它,但借助一些辅助技术就可以直接进行操作了。
    其实上面涉及到JVM的一个参数:ObjectAlignmentInBytes,这个参数表示Java堆中的对象,需要按照几字节对齐,范围是[8-256],必须是2的n次方。在JDK1.8中该参数默认值是8,也就是Java默认是8字节对齐。此时,如果配置最大堆内存超过32GB(实际略小于32GB就会失效),那么指针压缩会失效。
    你可以使用-XX:ObjectAlignmentInBytes=16修改该配置为16,那么最大堆内存超过64GB时指针压缩才会失效。同理,配置为32时,超过128GB才会失效。

虽然增大ObjectAlignmentInBytes能扩大寻址范围,这同时也会增加对象之间的填充数据,浪费内存和缓存空间,从而导致压缩指针没有达到原本节省空间的效果。

-XX:ObjectAlignmentInBytes=16

# 查看JVM配置
java -XX:+PrintCommandLineFlags -version

# 查看ObjectAlignmentInBytes配置
java -XX:+PrintFlagsFinal -version | grep ObjectAlignmentInBytes
     intx ObjectAlignmentInBytes                    = 8                                   {lp64_product}
java version "1.8.0_421"
Java(TM) SE Runtime Environment (build 1.8.0_421-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)

指针压缩测试

写一个简单的User类,使用JOL(不熟悉的请参考后面的教程)对它进行测试,代码如下:

package org.hbin.jol;

import org.openjdk.jol.info.ClassLayout;

import java.util.Date;

/**
 * @author Haley
 * @version 1.0
 * 2024/8/22
 */
public class JOLTest {
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
    }

    static class User {
        long id;
        String name;
        int age;
        Date birthday;

        Date createTime;

        int[] arr;
    }
}

测试结果:

UseCompressedOopsUseCompressedClassPointers对象大小
++40 bytes
--64 bytes
+-48 bytes
-+64 bytes1

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 实例数据 (Instance Data)

实例数据部分存储了对象的实际数据,也就是对象属性的值。这部分的大小取决于对象的属性数量和类型。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
HotSpot虚拟机默认的分配策略是longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认值为true),那么子类中较窄的变量也可能会插入到父类变量的空隙之中。

3. 填充数据 (Padding Data)

为了方便虚拟机的寻址,JVM要求对象结构的内存大小是8字节的整数倍。如果对象的大小的字节数不是8的倍数,那么 JVM 会添加一些额外的字节达到8的倍数来满足对齐要求。
例如对象头12个字节,实例数据只有一个byte变量,则填充数据是3个字节,12+1+3=16,是8的倍数。

填充数据不是必须存在的,仅仅是为了字节对齐。
只有存在填充数据时才有,否则为0字节

查看 Java 对象的内存结构

使用反射和VisualVM、JConsole等工具

使用反射可以来获取对象的内部信息,如属性信息、方法信息等。
利用VisualVM、JConsole等工具也可以查看对象在JVM中占用的空间信息、回收信息等。

使用JOL

Java对象布局(Java Object Layout),也叫JOL,是一个用来分析 JVM 中对象内存布局的小工具。可以用于查看对象在内存中的占用情况,实例对象的引用情况等。

引入Maven依赖

当前最新版本为0.17,你也可以根据需要自行选择或查找最新版本。

<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

测试样例

package org.hbin.jol;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

/**
 * @author Haley
 * @version 1.0
 * 2024/8/22
 */
public class JOLTest {
    public static void main(String[] args) {
        System.out.println(VM.current().details());
        System.out.println(ClassLayout.parseInstance(new int[]{1, 2, 3}).toPrintable());
        System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
    }
}

输出信息:

# VM mode: 64 bits
# Compressed references (oops): 3-bit shift
# Compressed class pointers: 3-bit shift
# Object alignment: 8 bytes
#                       ref, bool, byte, char, shrt,  int,  flt,  lng,  dbl
# Field sizes:            4,    1,    1,    2,    2,    4,    4,    8,    8
# Array element sizes:    4,    1,    1,    2,    2,    4,    4,    8,    8
# Array base offsets:    16,   16,   16,   16,   16,   16,   16,   16,   16

[I object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf800016d
 12   4        (array length)            3
 16  12    int [I.<elements>             N/A
 28   4        (object alignment gap)    
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以切换指针压缩的开关,查看各对象在内存中占用的空间信息是否有变化。

参考


  1. 关闭UseCompressedOops而打开UseCompressedClassPointers的情况下会输出一个警告:Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops,这是老版本遗留的BUG。从 Java 15 Build 23 开始, UseCompressedClassPointers 已经不再依赖 UseCompressedOops 了,两者在大部分情况下已经独立开来。 ↩︎ ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值