从理论到实践,刨根问底探索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

相关文章

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Spring Boot集成/输出/日志级别控制/持久化开发实践

《SpringBoot集成/输出/日志级别控制/持久化开发实践》SpringBoot默认集成Logback,支持灵活日志级别配置(INFO/DEBUG等),输出包含时间戳、级别、类名等信息,并可通过... 目录一、日志概述1.1、Spring Boot日志简介1.2、日志框架与默认配置1.3、日志的核心作用

破茧 JDBC:MyBatis 在 Spring Boot 中的轻量实践指南

《破茧JDBC:MyBatis在SpringBoot中的轻量实践指南》MyBatis是持久层框架,简化JDBC开发,通过接口+XML/注解实现数据访问,动态代理生成实现类,支持增删改查及参数... 目录一、什么是 MyBATis二、 MyBatis 入门2.1、创建项目2.2、配置数据库连接字符串2.3、入

Springboot项目启动失败提示找不到dao类的解决

《Springboot项目启动失败提示找不到dao类的解决》SpringBoot启动失败,因ProductServiceImpl未正确注入ProductDao,原因:Dao未注册为Bean,解决:在启... 目录错误描述原因解决方法总结***************************APPLICA编

深度解析Spring Security 中的 SecurityFilterChain核心功能

《深度解析SpringSecurity中的SecurityFilterChain核心功能》SecurityFilterChain通过组件化配置、类型安全路径匹配、多链协同三大特性,重构了Spri... 目录Spring Security 中的SecurityFilterChain深度解析一、Security

SpringBoot多环境配置数据读取方式

《SpringBoot多环境配置数据读取方式》SpringBoot通过环境隔离机制,支持properties/yaml/yml多格式配置,结合@Value、Environment和@Configura... 目录一、多环境配置的核心思路二、3种配置文件格式详解2.1 properties格式(传统格式)1.

Apache Ignite 与 Spring Boot 集成详细指南

《ApacheIgnite与SpringBoot集成详细指南》ApacheIgnite官方指南详解如何通过SpringBootStarter扩展实现自动配置,支持厚/轻客户端模式,简化Ign... 目录 一、背景:为什么需要这个集成? 二、两种集成方式(对应两种客户端模型) 三、方式一:自动配置 Thick

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致

Spring WebClient从入门到精通

《SpringWebClient从入门到精通》本文详解SpringWebClient非阻塞响应式特性及优势,涵盖核心API、实战应用与性能优化,对比RestTemplate,为微服务通信提供高效解决... 目录一、WebClient 概述1.1 为什么选择 WebClient?1.2 WebClient 与