new Object到底占用多少内存空间?

482 阅读7分钟

前言

通过 JOL 工具,深入剖析对象头、实例数据以及内存对齐的具体细节,了解 JVM 是如何管理和优化内存的。使用 JOL,可以验证内存结构,直观地观察 JVM 参数(如对象指针压缩、类指针压缩等)对对象布局的影响。

一、工具介绍

JOL(Java Object Layout) 是一个由 OpenJDK提供的工具库,用于分析和展示 Java 对象的内存布局、对象头、对象大小以及各种内存偏移量等信息。它对于研究 Java 内存管理、理解对象布局和 JVM 内部机制非常有帮助。

JOL 的核心功能

  1. 对象布局分析 JOL 可以展示 Java 对象在内存中的布局细节,比如对象头、字段偏移量、实例大小等。它会根据不同的 JVM 配置(如是否启用压缩指针)给出对象的实际内存布局。
  2. 对象头分析 JOL能展示对象头的详细信息,包括Mark Word、类指针和数组长度等。对象头是 JVM 用来管理对象状态的重要部分,它包含锁信息、GC 状态和哈希码等内容。
  3. 压缩指针(Compressed Oops)支持 JOL 支持在开启和关闭指针压缩(Compressed Oops)的情况下,展示对象的内存布局差异。通过对比,可以看到指针压缩对对象大小的影响。
  4. 对象对齐与填充 JOL 能显示对象的内存对齐和填充情况。由于 JVM 的内存布局需要满足对齐要求(通常是 8 字节),所以某些对象可能会被填充以满足内存对齐规则。
  5. 数组布局 JOL 支持对数组对象的布局分析,可以查看数组对象头和每个数组元素在内存中的布局。

二、JOL使用

2.1添加JOL依赖

在Maven项目中使用以下方式添加 JOL 依赖:

<!-- 对象内存分布工具 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

2.2使用JOL进行对象内存分析

一个简单的测试类

package com.lazy.snail.jvm;
​
import org.openjdk.jol.info.ClassLayout;
​
/**
 * @ClassName ObjectTest
 * @Description TODO
 * @Author lazysnail
 * @Date 2024/11/12 16:12
 * @Version 1.0
 */
public class ObjectTest {
    public static void main(String[] args) {
        Object obj = new Object();
        String layout = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(layout);
    }
}

三、对象内存布局详解

以2.2输出结果作为分析样例:

输出结果:

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

3.1表头

  1. OFF:内存布局中的偏移量

  2. SZ:内存中占用的字节数

  3. TYPE:数据类型(普通字段:显示具体的数据类型;对象头信息:通常为空)

  4. DESCRIPTION:(普通字段:字段名称;对象头信息:mark表示标记字段、class表示类指针、gap表示内存填充)

  5. VALUE:

    • 对于对象头中的mark word,VALUE表示对象的标记状态,如锁状态、哈希码等。
    • 对于类指针,VALUE显示的是类元数据的地址。
    • 对于普通字段,VALUE显示字段的当前值。

3.2标记字(Mark Word)

在 64 位 JVM 中,mark word占 8 字节(64 位),在2.2的输出结果中,可以看档第一行数据便是对象头中的标记字,占用了8个字节。

mark word是对象头中一个高度复用的区域,它的内容会根据对象的状态变化而调整。它不仅仅是锁信息的存储地,还包含了对象的哈希值、GC 标记、分代年龄等重要信息。在 JVM 的内部实现中,mark word帮助实现了 Java 对象的高效锁管理和垃圾回收优化。

下表是不同状态下对象头中存储的信息:

1b553c9ec427920e8eb945be18ab3ed3

3.3类指针

类指针也是对象头的一部分,从2.2的输出结果中可以看出,类指针占用了4字节。

3.3.1类指针位置及作用:
  • 类指针指向的是该对象的类元数据。

  • 类指针是对象头中的一部分,它使 JVM 能够通过这个指针找到对象的类信息:

    1. 字段布局:了解对象有哪些字段以及每个字段的位置和类型。
    2. 方法表:包含了这个类定义的所有方法,用于方法调用。
    3. 继承信息:包含该类的父类、接口等信息,用于类型检查和多态支持。
3.3.2类指针压缩

在 64 位 JVM 中,为了节省内存,JVM 会启用类指针压缩(Compressed Class Pointers)。当类指针压缩开启时(通过 -XX:+UseCompressedClassPointers,通常在默认情况下开启),JVM 会将类指针压缩到 32 位以节省内存空间。这在对象数量较多的应用中可以显著减少内存消耗。

查看是否开启了类指针压缩:

#JVM运行时参数查看
java -XX:+PrintCommandLineFlags -version

结果:

-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4294967296 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
java version "17.0.11" 2024-04-16 LTS
Java(TM) SE Runtime Environment (build 17.0.11+7-LTS-207)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.11+7-LTS-207, mixed mode, sharing)

从结果中可以看到-XX:+UseCompressedClassPointers,确实是默认开启的。

禁用类指针压缩:

现在我们在运行2.2测试类时将类指针压缩关闭:

image-20241112234346607

运行结果:

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

可以从结果看出,当我们把类指针关闭压缩禁用之后,对象头中的类指针变成了8个字节。

3.4对齐

从2.2的运行结果中看到,"object alignment gap"存在四字节的补位对齐的情况。

这是由于HotSpot JVM 采用 8 字节对齐,本身obj实例的大小是12字节,不是8的倍数,需要补上4个字节的内存,使其占用空间变成8的倍数。

至于为什么默认采用8字节对齐,涉及到内存访问效率、CPU缓存行对齐、内存碎片化等等。

对齐策略能修改吗?能,但是真没干过,看一看效果:

运行测试类时,加上-XX:ObjectAlignmentInBytes=32,将补齐策略修改成32位补齐

image-20241113000542836

输出结果:

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)    0x00040fc0
 12  20        (object alignment gap)    
Instance size: 32 bytes
Space losses: 0 bytes internal + 20 bytes external = 20 bytes total

可以看出,为了满足32位的补齐策略,无端的耗费了20字节的内存。

四、总结

当执行Object obj = new Object();时,对象的内存布局主要包括以下几部分:

对象头

  • Mark Word:用于存储对象的标记信息,包括对象的哈希码、GC 状态、锁状态等。大小通常为 8 字节。
  • Class Pointer:指向对象所属类(Object 类)的指针,帮助 JVM 知道该对象的类信息。通常占 4 或 8 字节,取决于是否启用了类指针压缩。

实例数据Object 类没有实例字段,因此这一部分为空,但仍需要空间来满足对象对齐要求。

对齐填充:为了满足 8 字节对齐要求,JVM 会在对象末尾增加填充字节(如果需要)。

对象引用本身也占用空间,大小为 4 字节或 8 字节,具体取决于是否启用了 对象引用指针压缩