从理论到实践,刨根问底探索Java对象内存布局

2024-06-04 00:38

本文主要是介绍从理论到实践,刨根问底探索Java对象内存布局,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

从理论到实践,刨根问底探索Java对象内存布局

所谓对象的内存布局,就是对象在分配到内存中后的存储格式。

对象在内存中的布局一共包含三部分:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

img

第一部分:对象头

首先来看一下对象头的结构

Java对象头分为两部分:

  1. Mark Word:对象自身运行时数据。
  2. Klass Pointer:类型指针,即对象指向它的类元数据的指针。

1、Mark Word

为啥叫Mark Word呢?我理解因为这部分是用来标记对象运行时的数据和状态,比如对象的HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。而Word呢,是因为这段信息是用一个Word(字)长度来保存的。在32位系统中,一个字是32bit,也就是4字节。64位系统中,一个字是64bit,也就是8字节。

对于这部分的描述,从markOop.hpp源码的注释中即可得知。

下面就以32位的虚拟机为例,来探寻一下对象头的Mark Word部分是什么样的数据结构。

先划重点,锁状态很重要

这里要注意两点:

  1. 对象头的数据格式和对象的锁状态紧密相关。在不同的锁状态下,对象头的结构都不一样。其目的是为了尽量在极小的空间内存储尽量多的信息。
  2. 锁状态的标志位是固定的,无论是32位还是64位的虚拟机,对象头中最后两位就是锁的状态标志。

既然锁的状态很重要,那么就先看一下下锁标志对应的状态含义:

lock状态
01无锁
00轻量级锁(locked)
10重量级锁(monitor,inflated lock)
11GC标记(marked)
01-无锁状态下的Mark Word结构

无锁状态下,涉及到两种情况:

  1. 稀松平常的无锁状态
  2. 偏向锁

是否存在偏向锁,我们也是用1位长的标识来判断。

当不存在偏向锁时,1-25位是对象的HashCode,之后的4位是对象GC的分代年龄,之后的1位是偏向锁的标志,此时该标志为0。

当存在偏向锁时,1-23为是持有偏向锁的线程的ID,之后的2位是偏向时间戳,然后4位依旧是对象GC的分代年龄,再之后的1位是偏向锁的标志,此时该标志为1。

00-轻量级锁状态下的Mark Word结构

轻量级锁状态时,对象头的前30位保存指向持有锁的线程的栈帧中锁记录的指针。

此时,获取了该对象偏向锁的线程,会在线程的栈帧上建立锁记录的空间,并通过CAS的方式将对象头的信息复制到锁记录的位置,并将对象头替换成指向锁记录的指针。

10-重量级锁状态下的Mark Word结构

当有两个及以上的线程竞争同一个锁,则轻量级锁就会升级成重量级锁。此时对象头的前30位保存的是指向重量级锁Monitor的指针。

关于Monitor,这里可以做个简单的理解:Java的重量级锁,是通过一个Monitor对象来实现的。JVM通过Monitor对象中的_owner、_EntryList来维护是哪个线程持有这个对象的锁,以及后续的阻塞线程。源码在objectWaiter.hpp中可以深入了解。

11-GC标记

当最后两位为11时,代表被GC标记了,则对象头前面的30位信息为空。

Mark Word 小结

在这里根据上面的描述,画个图来展示一下Mark Word这部分数据在32位系统和62位系统里的布局,更直观清晰一些。

img

img

2、Klass Pointer(类型指针)

紧跟着Mark Word,是对象头的另一部分——类型指针。类型指针也是用一个字的长度(32位系统是4byte,64位系统是8byte)来保存的。这个指针会指向该对象对应的类元数据,说人话就是,JVM通过这个指针知道这个对象是哪个类的实例。

3、数组长度

如果这是一个普通的Java对象,则对象头中只有Mark Word和Klass Pointer两部分。当它是一个数组对象时,对象头中还需要一部分空间来保存数组的长度。有了数组长度,JVM才能够知道一个数组对象的大小。数组长度这部分也是用一个字的长度(32位系统是4byte,64位系统是8byte)来保存。

如果64位的JVM开启了+UseCompressedOops选项,则类型指针和数组长度这两个区域都会被压缩成32位。

第二部分:实例数据

这部分就是对象真正存储的有效信息,也就是类里定义的各种类型字段的内容。

第三部分:对齐填充

这部分没有实际的含义,仅仅是起到占位符的作用。因为JVM要求对象起始地址必须是8字节的整数倍,也就是一个对象的大小必须是8字节的整数倍,所以如果一个对象的实例数据不满足8字节的整数倍,则需要做一个对齐填充的操作,保证对象的大小是8字节的整数倍。

实践一下

接下来我们来跑几个demo看看真实的对象头布局,借助JOL(Java Object Layout),我们可以分析JVM中对象的布局。

环境:64位系统,JDK8。

1、引入JOL依赖

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

2、一个简单的类

public class OneObject {private int id;private String name;private double score;
}

3、打印对象内存布局

这里选择了五种情况:

  1. 刚刚new出来的新鲜对象
  2. 经历过gc后的对象
  3. 加锁时的对象(偏向锁)
  4. 多线程竞争锁时的对象(重量级锁)
  5. 算一下对象的hashCode

代码如下:

@Test
public void showObjectData() throws InterruptedException {OneObject object = new OneObject();log.info("初始化后的对象布局:{}", ClassLayout.parseInstance(object).toPrintable());System.gc();log.info("gc一次之后的对象布局:{}", ClassLayout.parseInstance(object).toPrintable());synchronized (object) {log.info("加锁时的对象布局:{}", ClassLayout.parseInstance(object).toPrintable());}for (int i = 0; i < 2; i++) {Thread thread = new Thread(()->{synchronized (object) {log.info("竞争锁时的对象布局:{}", ClassLayout.parseInstance(object).toPrintable());}});thread.start();}Thread.sleep(500);object.hashCode();log.info("计算完hashCode的对象布局:{}", ClassLayout.parseInstance(object).toPrintable());
}

4、输出结果

执行以上的代码,输出结果如下:

00:06:03.877 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 初始化后的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:OFFSET  SIZE               TYPE DESCRIPTION                               VALUE0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)12     4                int OneObject.id                              016     8             double OneObject.score                           0.024     4   java.lang.String OneObject.name                            null28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total00:06:03.890 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - gc一次之后的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:OFFSET  SIZE               TYPE DESCRIPTION                               VALUE0     4                    (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)12     4                int OneObject.id                              016     8             double OneObject.score                           0.024     4   java.lang.String OneObject.name                            null28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total00:06:03.891 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 加锁时的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:OFFSET  SIZE               TYPE DESCRIPTION                               VALUE0     4                    (object header)                           90 99 0b 6d (10010000 10011001 00001011 01101101) (1829476752)4     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)12     4                int OneObject.id                              016     8             double OneObject.score                           0.024     4   java.lang.String OneObject.name                            null28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total00:06:03.892 [Thread-1] INFO com.esparks.pandora.learning.vm.LearnObjectData - 竞争锁时的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:OFFSET  SIZE               TYPE DESCRIPTION                               VALUE0     4                    (object header)                           62 d9 00 20 (01100010 11011001 00000000 00100000) (536926562)4     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)12     4                int OneObject.id                              016     8             double OneObject.score                           0.024     4   java.lang.String OneObject.name                            null28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total00:06:03.893 [Thread-2] INFO com.esparks.pandora.learning.vm.LearnObjectData - 竞争锁时的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:OFFSET  SIZE               TYPE DESCRIPTION                               VALUE0     4                    (object header)                           62 d9 00 20 (01100010 11011001 00000000 00100000) (536926562)4     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)12     4                int OneObject.id                              016     8             double OneObject.score                           0.024     4   java.lang.String OneObject.name                            null28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total00:06:04.398 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 计算完hashCode的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:OFFSET  SIZE               TYPE DESCRIPTION                               VALUE0     4                    (object header)                           09 63 c1 aa (00001001 01100011 11000001 10101010) (-1430166775)4     4                    (object header)                           56 00 00 00 (01010110 00000000 00000000 00000000) (86)8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)12     4                int OneObject.id                              016     8             double OneObject.score                           0.024     4   java.lang.String OneObject.name                            null28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totalProcess finished with exit code 0
对象布局分析

这里我们先以第一种情况的对象布局来简单说明输出的格式,直接在图上标明啦。

img

这里要插一句,JOL在输出对象头的时候,是按照四个字节的长度从内存中获取对象的对象头数据。所以你会看到64位的Mark Word会拆成两行(两个4字节)打印出来。

几个问题

看到这里的时候,脑海里不禁冒出几个问题。

Q1:为什么Klass pointer只有四个字节呢?

因为JVM默认开启了+UseCompressedOops选项,所以Klass Pointer被压缩成了32位。如果在启动时配置了-UseCompressedOops选项,那么Klass Pointer就也是64位啦。

Q2:说好了Mark Word的最后两位是锁状态,这刚创建的对象,最后两位怎么就是00了呢?

这个就要和字节存储的大小端模式有关了。

举个例子,一个16进制的整数0x12345678,对应的二进制整数为:00010010 00110100 01010110 01111000(12 34 56 78),一共占用四个字节。那么在内存中应该如何存储这长度为4byte的字节序列的数据呢?

有两种方式:

  1. 按照内存地址的顺序,依次保存12 34 56 78这四个字节的数据。这种将字节序列的高序字节存储在内存的起始地址上的方式,叫大端模式。
  2. 按照内存地址的顺序,依次保存78 56 34 12这四个字节的数据。这种将字节序列的低序字节存储在内存的起始地址上的方式,叫小端模式。

img

而一般我们用的x86或者ARM的CPU,采用的都是小端模式来保存内存中的字节序列,所以和我们常见的顺序是反着的。因此,你看到的输出的前两行的对象头,实际上的值是这样的:

img

所以对象初始化后,就是无锁状态啦。

分析不同状态时的对象头

刚才已经就刚初始化的对象分析过一次内存布局了。而在锁状态不同的情况下,变化也只限于对象头中Mark Word的值变动,所以这里就快速的分析一下其余的四种状态时的Mark Word了。这里我也自动将输出的小端格式转换成正常的顺序来分析。也是通过实际情况来回顾验证一下刚才的理论知识啦,对照前面的图片中不同的颜色比对就可以了。

1.gc一次之后的Mark Word

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001001

红色的01代表无锁状态,蓝色的0代表无偏向锁,黄色的0001是GC分代年龄。因为GC过一次,该对象没有被回收,年龄加1。

2.加锁时的Mark Word

00000000 00000000 00000000 00000001 01101101 00001011 10011001 10010000

红色的00代表轻量级锁状态,绿色的一串是指向持有锁的线程的栈帧中锁记录的指针。

3.竞争锁时的Mark Word

00000000 00000000 00000000 00000001 00100000 00000000 11011001 01100010

红色的10代表重量级锁状态,绿色的一串是指向重量级锁Monitor的指针。

4.计算完hashCode的Mark Word

00000000 00000000 00000000 01010110 10101010 11000001 01100011 00001001

红色的01代表此时回归到无锁状态,,蓝色的0代表无偏向锁,黄色的0001是GC分代年龄为1,紫色这一串是刚才计算的hashCode,保存在了这里。所以只有在调用了hashCode()方法时,JVM才会把对象的hashCode保存到对象头中。

总结

好啦,总结一下。

本篇文章先是介绍了Java对象的内存布局(由对象头、实例数据、对齐填充三部分组成);之后详细的介绍了对象头的数据结构(Mark Word、Klass Pointer、数组长度),以及不同锁状态下(01无锁、00轻量级锁、10重量级锁、11GC标记),Mark Word中的数据格式以及代表的含义;最后通过JOL打印出对象的内存布局,进一步验证了前半部分枯燥的理论知识。

希望看到这里,能让你彻底的理解Java对象在内存中的完整样貌啦~

参考资料

  1. markOop.hpp源码,主要在注释中
  2. objectWaiter.hpp源码
  3. 《深入理解Java虚拟机》

这篇关于从理论到实践,刨根问底探索Java对象内存布局的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1028574

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2