跳至主要內容

Java对象内存布局和对象头

Jin大约 8 分钟

Java对象内存布局和对象头

1、对象的存储位置

Object o = new Object();

Object相当于一个Object类的模板,类加载的时候放在方法区,存放类的信息

o:在方法体中,随着方法的调用,存储在栈中栈帧信息中,

tips:栈帧包括局部变量表,操作数栈,动态链接,返回地址

new Object():创建的对象信息放在堆空间中

2、对象在堆内存中的存储布局

Hotspot术语表官网:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

底层源码理论证明:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/89fb452b3688/src/share/vm/oops/oop.hpp

了解到了对象的存储位置,这时我们要知道对象在堆中存放哪些信息?

对象在堆内存中存储布局可以划分为三个:对象头,实例数据,填充属性

  1. 对象头:包括**Markword和类型指针**

    • hashcode,自己的hash值,记录在内存中什么地方
    • 给对象可以加锁,标记对象有没有加锁
    • 进行垃圾回收时,要对对象进行标记是否为垃圾对象还有它的垃圾回收次数
    • 这些基本信息在对象头中markword都记录着,比如GC标记信息,GC次数,同步锁标记,偏向锁持有者,hashcode值
  2. 实例数据:可以认为是对象中的一些数据,比如String id;

  3. 填充属性:当创建的对象占用内存大小不为8个字节的倍数,会对对象的内存占用补充为8的倍数

memory
memory

普通对象与数组对象:

object
object

2.1、对象头

2.1.1、对象标记Mark Word

对象标记MarkWord,默认存储 (哈希值(HashCode )、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID即、偏向时间戳epoch)等信息

这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。

它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。

Mark Word的内存布局如下:

64
64

GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15,因为GC年龄占4位,最大就是1111=15

// GC最大分代年龄,假如我们手动设置最大分代年龄:-XX:MaxTenuringThreshold=16,JVM会报错,JVM启动失败
public class MaxTenuringThreshold {
    public static void main(String[] args) {
        System.out.println("==========");
    }
}
  • IDEA配置VM:-XX:MaxTenuringThreshold=16,超出最大值15
image-20220805095706702
image-20220805095706702
  • 打印结果:
MaxTenuringThreshold of 16 is invalid; must be between 0 and 15
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

image-20220804100226477
image-20220804100226477
image-20220804100332808
image-20220804100332808
  • 在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节
image-20220804100633315
image-20220804100633315
  • 默认存储对象的HashCode、分代年龄和锁标志位等信息。
  • 这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。
  • 它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。

2.1.2、类元信息(又叫类型指针)

image-20220805093535843
image-20220805093535843
  • 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

2.1.3、对象头多大

  • 在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。

2.2、实例数据

存放类的属性(Field)数据信息,包括父类的属性信息, 如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

2.3、对齐填充

  • 虚拟机要求对象起始地址必须是8字节的整数倍。
  • 填充数据不是必须存在的,仅仅是为了字节对齐
  • 这部分内存按8字节补充对齐。

3、对象头的MarkWord

3.1、32位

看一下即可,不用学了,以64位为准

image-20220807003741004
image-20220807003741004

3.2、64位(重要)

image-20220807003919508

image-20220807004004216
image-20220807004004216
image-20220807004022288
image-20220807004022288

oop.hpp

image-20220807004143668
image-20220807004143668

markOop.hpp

  • hash: 保存对象的哈希码
  • age: 保存对象的分代年龄
  • biased_lock: 偏向锁标识位
  • lock: 锁状态标识位
  • JavaThread* :保存持有偏向锁的线程ID
  • epoch: 保存偏向时间戳
image-20220807004212198
image-20220807004212198
  • markword(64位)分布图,
  • 对象布局、GC回收和后面的锁升级就是
  • 对象标记MarkWord里面标志位的变化
image-20220807004239464
image-20220807004239464

4、分析对象在JVM的大小和分布

**JOL官网:**http://openjdk.java.net/projects/code-tools/jol/

4.1、POM

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.16</version>
            <scope>provided</scope>
        </dependency>

4.2、VM的细节详细情况

    @Test
    public void vm() {
        //VM的细节详细情况
        System.out.println(VM.current().details());
        //所有的对象分配的字节都是8的整数倍。
        System.out.println(VM.current().objectAlignment());
    }

打印结果

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

8

4.3、对象内存布局大小

    @Test
    public void demo() {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
  • 打印结果
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
image-20220807011025345
image-20220807011025345
  • OFFSET 偏移量,也就是到这个字段位置所占用的byte数
  • SIZE 后面类型的字节大小
  • TYPE 是Class中定义的类型
  • DESCRIPTION DESCRIPTION是类型的描述
  • VALUE VALUE是TYPE在内存中的值

4.4、类型指针压缩

loss due to the next object alignment:就是对齐填充花费的内存

但是为什么类型指针只有4个字节呢?上文不是说好了MarkWord+类型指针8+816字节吗?

控制台执行命令:java -XX:+PrintCommandLineFlags -version,查看JVM运行时参数:

其中发现了一个参数:-XX:+UseCompressedClassPointers,说明JVM默认开启了类型指针压缩!!!

PS D:\Projects\Idea_Projects\Jin> java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=535368512 -XX:MaxHeapSize=8565896192 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
 
java version "1.8.0_271"
Java(TM) SE Runtime Environment (build 1.8.0_271-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)

4.5、关闭类型指针压缩

手动关闭压缩再看看 :-XX:-UseCompressedClassPointers

image-20220807012627233
image-20220807012627233
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)    0x0000013cc5601c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
image-20220807012747091
image-20220807012747091

4.6、带有实例数据的对象演示

@Test
public void demo2() {
    Customer c1 = new Customer();//16 bytes
    System.out.println(ClassLayout.parseInstance(c1).toPrintable());
}

打印结果:

com.jin.juc.ob.Customer object internals:
OFF  SZ      TYPE DESCRIPTION               VALUE
  0   8           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4           (object header: class)    0xf80274e0
 12   4       int Customer.id               0
 16   1   boolean Customer.flag             false
 17   1   boolean Customer.flag2            false
 18   6           (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total

看下带有实例数据的对象的内存布局:

  • 对象头:8+4=12字节
  • 实例数据:实际情况实际占用,这里占用了int(4)+boolean(1)+boolean(1)=6字节
  • 对齐填充:6字节
  • 发现压缩的类型指针的4字节最后又通过对齐填充补回来了,这种情况类型指针压缩好似又有点鸡肋

关闭指针压缩-XX:-UseCompressedClassPointers

com.jin.juc.ob.Customer object internals:
OFF  SZ      TYPE DESCRIPTION               VALUE
  0   8           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8           (object header: class)    0x0000023726aadbd0
 16   4       int Customer.id               0
 20   1   boolean Customer.flag             false
 21   1   boolean Customer.flag2            false
 22   2           (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

再来看下带有实例数据的对象的内存布局:

  • 对象头:8+8=16字节
  • 示例数据:实际情况实际占用,这里占用了int(4)+boolean(1)+boolean(1)=6字节
  • 对齐填充:2字节

4.7、GC年龄采用4位bit存储,最大为15,

例如:MaxTenuringThreshold参数默认值就是15

    @Test
    public void demoGC() {

    }

配置: -XX:MaxTenuringThreshold=16

image-20220807014437081
image-20220807014437081

打印结果:

MaxTenuringThreshold of 16 is invalid; must be between 0 and 15
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
贡献者: Jin