Java虚拟机14:Java对象大小、对象内存布局及锁状态变化

2023-11-13

一个对象占多少字节?

关于对象的大小,对于C/C++来说,都是有sizeof函数可以直接获取的,但是Java似乎没有这样的方法。不过还好,在JDK1.5之后引入了Instrumentation类,这个类提供了计算对象内存占用量的方法。至于具体Instrumentation类怎么用就不说了,可以参看这篇文章如何精确地测量java对象的大小

不过有一点不同的是,这篇文章使用命令行传入JVM参数来指定代理,这里我通过Eclipse设置JVM参数:

后面的是我打的agent.jar的具体路径。剩下的就不说了,看一下测试代码:

 1 public class JVMSizeofTest {
 2 
 3     @Test
 4     public void testSize() {
 5         System.out.println("Object对象的大小:" + JVMSizeof.sizeOf(new Object()) + "字节");
 6         System.out.println("字符a的大小:" + JVMSizeof.sizeOf('a') + "字节");
 7         System.out.println("整型1的大小:" + JVMSizeof.sizeOf(new Integer(1)) + "字节");
 8         System.out.println("字符串aaaaa的大小:" + JVMSizeof.sizeOf(new String("aaaaa")) + "字节");
 9         System.out.println("char型数组(长度为1)的大小:" + JVMSizeof.sizeOf(new char[1]) + "字节");
10     }
11     
12 }

运行结果为:

Object对象的大小:16字节
字符a的大小:16字节
整型1的大小:16字节
字符串aaaaa的大小:24字节
char型数组(长度为1)的大小:24字节

接着,代码不变,加入一条虚拟机参数"-XX:-UseCompressedOops",再运行一遍测试类,运行结果为:

Object对象的大小:16字节
字符a的大小:24字节
整型1的大小:24字节
字符串aaaaa的大小:32字节
char型数组(长度为1)的大小:32字节

后文来详细解释一下原因。

 

Java对象大小计算方法

JVM对于普通对象和数组对象的大小计算方式有所不同,我画了一张图说明:

解释一下其中每个部分:

  1. Mark Word:存储对象运行时记录信息,占用内存大小与机器位数一样,即32位机占4字节,64位机占8字节
  2. 元数据指针:指向描述类型的Klass对象(Java类的C++对等体)的指针,Klass对象包含了实例对象所属类型的元数据,因此该字段被称为元数据指针,JVM在运行时将频繁使用这个指针定位到位于方法区内的类型信息。这个数据的大小稍后说
  3. 数组长度:数组对象特有,一个指向int型的引用类型,用于描述数组长度,这个数据的大小和元数据指针大小相同,同样稍后说
  4. 实例数据:实例数据就是8大基本数据类型byte、short、int、long、float、double、char、boolean(对象类型也是由这8大基本数据类型复合而成),每种数据类型占多少字节就不一一例举了
  5. 填充:不定,HotSpot的对齐方式为8字节对齐,即一个对象必须为8字节的整数倍,因此如果最后前面的数据大小为17则填充7,前面的数据大小为18则填充6,以此类推

为了保证效率,Java编译期在编译Java对象的时候,通过字段类型对Java对象的字段会进行排序,具体顺序如下表所示:

了解这个是很有用的,我们可以通过在字段时间通过填充长整型变量的方式把热点变量隔离在不同的缓存行中,减少伪同步,在多核CPU中极大地提升效率,这个以后有机会写文章专门讲解。

最后再说说元数据指针的大小。元数据指针是一个引用类型,因此正常来说64位机元数据指针应当为8字节,32位机元数据指针应当为4字节,但是HotSpot中有一项优化是对元数据类型指针进行压缩存储,使用JVM参数:

  • -XX:+UseCompressedOops开启压缩
  • -XX:-UseCompressedOops关闭压缩

HotSpot默认是前者,即开启元数据指针压缩,当开启压缩的时候,64位机上的元数据指针将占据4个字节的大小。换句话说就是当开启压缩的时候,64位机上的引用将占据4个字节,否则是正常的8字节

 

Java对象内存大小计算

有了上面的理论基础,我们就可以分析JVMSizeofTest类的执行结果及为什么加入了"-XX:-UseCompressedOops"这条参数后同一个对象的大小会有差异了。

首先是Object对象的大小:

  1. 开启指针压缩时,8字节Mark Word + 4字节元数据指针 = 12字节,由于12字节不是8的倍数,因此填充4字节,对象Object占据16字节内存
  2. 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 = 16字节,由于16字节正好是8的倍数,因此不需要填充字节,对象Object占据16字节内存

接着是字符'a'的大小:

  1. 开启指针压缩时,8字节Mark Word + 4字节元数据指针 + 1字节char = 13字节,由于13字节不是8的倍数,因此填充3字节,字符'a'占据16字节内存
  2. 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 + 1字节char = 17字节,由于17字节不是8的倍数,因此填充7字节,字符'a'占据24字节内存

接着是整型1的大小:

  1. 开启指针压缩时,8字节Mark Word + 4字节元数据指针 + 4字节int = 16字节,由于16字节正好是8的倍数,因此不需要填充字节,整型1占据16字节内存
  2. 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 + 4字节int = 20字节,由于20字节正好是8的倍数,因此填充4字节,整型1占据24字节内存

接着是字符串"aaaaa"的大小,所有静态字段不需要管,只关注实例字段,String对象中实例字段有"char value[]"与"int hash",由此可知:

  1. 开启指针压缩时,8字节Mark Word + 4字节元数据指针 + 4字节引用 + 4字节int = 20字节,由于20字节不是8的倍数,因此填充4字节,字符串"aaaaa"占据24字节内存
  2. 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 + 8字节引用 + 4字节int = 28字节,由于28字节不是8的倍数,因此填充4字节,字符串"aaaaa"占据32字节内存

最后是长度为1的char型数组的大小:

  1. 开启指针压缩时,8字节的Mark Word + 4字节的元数据指针 + 4字节的数组大小引用 + 1字节char = 17字节,由于17字节不是8的倍数,因此填充7字节,长度为1的char型数组占据24字节内存
  2. 关闭指针压缩时,8字节的Mark Word + 8字节的元数据指针 + 8字节的数组大小引用 + 1字节char = 25字节,由于25字节不是8的倍数,因此填充7字节,长度为1的char型数组占据32字节内存

 

Mark Word

Mark Word前面已经看到过了,它是Java对象头中很重要的一部分。Mark Word存储的是对象自身的运行数据,如哈希码(HashCode)、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等等。

不过由于对象需要存储的运行时数据很多,其实已经超出了32位、64位Bitmap结构所能记录的限度,但是对象头是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息。例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标识位,1Bit固定位0。在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下图所示:

这里要特别关注的是锁状态,后文将对锁状态及锁状态的变化进行研究。

 

锁的升级

如上图所示,锁的状态共有四种:无锁态、偏向锁、轻量级锁和重量级锁,其中偏向锁和轻量级锁是JDK1.6开始为了减少获得锁和释放锁带来的性能消耗而引入的。

四种锁的状态会随着竞争情况逐渐升级,锁可以升级但是不能降级,意味着偏向锁可以升级为轻量级锁但是轻量级锁不能降级为偏向锁,目的是为了提高获得锁和释放锁的效率。用一张图表示这种关系:

 

偏向锁

HotSpot作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代码更低因此引入了偏向锁。偏向锁的获取过程为:

  1. 访问Mark Word中偏向锁的标识是否设置为1,所标志位是否为01----确认为可偏向状态
  2. 如果为可偏向状态,则测试线程id是否指向当前线程,如果是,执行(5),否则执行(3)
  3. 如果线程id并为指向当前线程,通过CAS操作竞争锁。如果竞争成功,则将Mark Word中的线程id设置为当前线程id,然后执行(5);如果竞争失败,执行(4)
  4. 如果CAS获取偏向锁失败,则表示有竞争。当达到全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁(因为偏向锁是假设没有竞争,但是这里出现了竞争,要对偏向锁进行升级),然后被阻塞在安全点的线程继续往下执行同步代码
  5. 执行同步代码

有获取就有释放,偏向锁的释放点在于上述的第(4)步,只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的释放过程为:

  1. 需要等待全局安全点(在这个时间点上没有字节码正在执行)
  2. 它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 偏向锁释放后恢复到未锁定(标识位为01)或轻量级锁(标识位为00)状态

 

轻量级锁

轻量级锁的加锁过程为:

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态,JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word,此时线程堆栈与对象头的状态如图所示
  2. 拷贝对象头中的Mark Word复制到锁记录中
  3. 拷贝成功后,JVM将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向Object Mark Word,如果更新成功,则执行步骤(4),否则执行步骤(5)
  4. 如果更新动作成功,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标识位设置为00,即表示此对象处于轻量级锁状态,此时线堆栈与对象头的状态如图所示
  5. 如果更新动作失败,JVM首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标识的状态值变为10,Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而当前线程变尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程

 

偏向锁、轻量级锁与重量级锁的对比

下面用一张表格来对比一下偏向锁、轻量级锁与重量级锁,网上看到的,我觉得写得非常好,为了加深记忆我自己又手打了一遍:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java虚拟机14:Java对象大小、对象内存布局及锁状态变化 的相关文章

  • 如何为最终用户方便地启动Java GUI程序

    用户想要从以下位置启动 Java GUI 应用程序Windows 以及一些额外的 JVM 参数 例如 javaw Djava util logging config file logging properties jar MyGUI jar
  • Java Swing:从 JOptionPane 获取文本值

    我想创建一个用于 POS 系统的新窗口 用户输入的是客户拥有的金额 并且窗口必须显示兑换金额 我是新来的JOptionPane功能 我一直在使用JAVAFX并且它是不同的 这是我的代码 public static void main Str
  • Java中反射是如何实现的?

    Java 7 语言规范很早就指出 本规范没有详细描述反射 我只是想知道 反射在Java中是如何实现的 我不是问它是如何使用的 我知道可能没有我正在寻找的具体答案 但任何信息将不胜感激 我在 Stackoverflow 上发现了这个 关于 C
  • 如何在 Play java 中创建数据库线程池并使用该池进行数据库查询

    我目前正在使用 play java 并使用默认线程池进行数据库查询 但了解使用数据库线程池进行数据库查询可以使我的系统更加高效 目前我的代码是 import play libs Akka import scala concurrent Ex
  • Final字段的线程安全

    假设我有一个 JavaBeanUser这是从另一个线程更新的 如下所示 public class A private final User user public A User user this user user public void
  • JavaMail 只获取新邮件

    我想知道是否有一种方法可以在javamail中只获取新消息 例如 在初始加载时 获取收件箱中的所有消息并存储它们 然后 每当应用程序再次加载时 仅获取新消息 而不是再次重新加载它们 javamail 可以做到这一点吗 它是如何工作的 一些背
  • Mockito when().thenReturn 不必要地调用该方法

    我正在研究继承的代码 我编写了一个应该捕获 NullPointerException 的测试 因为它试图从 null 对象调用方法 Test expected NullPointerException class public void c
  • Spring @RequestMapping 带有可选参数

    我的控制器在请求映射中存在可选参数的问题 请查看下面的控制器 GetMapping produces MediaType APPLICATION JSON VALUE public ResponseEntity
  • 如何在PreferenceActivity中添加工具栏

    我已经使用首选项创建了应用程序设置 但我注意到 我的 PreferenceActivity 中没有工具栏 如何将工具栏添加到我的 PreferenceActivity 中 My code 我的 pref xml
  • 十进制到八进制的转换[重复]

    这个问题在这里已经有答案了 可能的重复 十进制转换错误 https stackoverflow com questions 13142977 decimal conversion error 我正在为一个类编写一个程序 并且在计算如何将八进
  • 如何为俚语和表情符号构建正则表达式 (regex)

    我需要构建一个正则表达式来匹配俚语 即 lol lmao imo 等 和表情符号 即 P 等 我按照以下示例进行操作http www coderanch com t 497238 java java Regular Expression D
  • 在两个活动之间传输数据[重复]

    这个问题在这里已经有答案了 我正在尝试在两个不同的活动之间发送和接收数据 我在这个网站上看到了一些其他问题 但没有任何问题涉及保留头等舱的状态 例如 如果我想从 A 类发送一个整数 X 到 B 类 然后对整数 X 进行一些操作 然后将其发送
  • 使用Caliper时如何指定命令行?

    我发现 Google 的微型基准测试项目 Caliper 非常有趣 但文档仍然 除了一些示例 完全不存在 我有两种不同的情况 需要影响 JVM Caliper 启动的命令行 我需要设置一些固定 最好在几个固定值之间交替 D 参数 我需要指定
  • 如何在控制器、服务和存储库模式中使用 DTO

    我正在遵循控制器 服务和存储库模式 我只是想知道 DTO 在哪里出现 控制器应该只接收 DTO 吗 我的理解是您不希望外界了解底层域模型 从领域模型到 DTO 的转换应该发生在控制器层还是服务层 在今天使用 Spring MVC 和交互式
  • 在 Mac 上正确运行基于 SWT 的跨平台 jar

    我一直致力于一个基于 SWT 的项目 该项目旨在部署为 Java Web Start 从而可以在多个平台上使用 到目前为止 我已经成功解决了由于 SWT 依赖的系统特定库而出现的导出问题 请参阅相关thread https stackove
  • Eclipse Java 远程调试器通过 VPN 速度极慢

    我有时被迫离开办公室工作 这意味着我需要通过 VPN 进入我的实验室 我注意到在这种情况下使用 Eclipse 进行远程调试速度非常慢 速度慢到调试器需要 5 7 分钟才能连接到远程 jvm 连接后 每次单步执行断点 行可能需要 20 30
  • 如何从终端运行处理应用程序

    我目前正在使用加工 http processing org对于一个小项目 但是我不喜欢它附带的文本编辑器 我使用 vim 编写所有代码 我找到了 pde 文件的位置 并且我一直在从 vim 中编辑它们 然后重新打开它们并运行它们 重新加载脚
  • 静态变量的线程安全

    class ABC implements Runnable private static int a private static int b public void run 我有一个如上所述的 Java 类 我有这个类的多个线程 在里面r
  • java.lang.IllegalStateException:驱动程序可执行文件的路径必须由 webdriver.chrome.driver 系统属性设置 - Similiar 不回答

    尝试学习 Selenium 我打开了类似的问题 但似乎没有任何帮助 我的代码 package seleniumPractice import org openqa selenium WebDriver import org openqa s
  • Spring Boot @ConfigurationProperties 不从环境中检索属性

    我正在使用 Spring Boot 1 2 1 并尝试创建一个 ConfigurationProperties带有验证的bean 如下所示 package com sampleapp import java net URL import j

随机推荐

  • 程序员必备的画图工具

    作者 CUGGZ 来源 前端充电宝 XMind 是一个跨平台的思维导图软件 具有多种结构样式 除了普通的思维导图 还包括树形图 逻辑图 鱼骨图 时间轴 树状表格等等 不同的结构样式可以自由组合混用 同时支持一键更换结构样式 最近经常有小伙伴
  • 编译安装 Nginx 提示:/configure: error: C compiler cc is not found

    问题产生背景 反向代理服务器需要增加探活功能 需要对前置nginx 进行重新编译安装第三方模块 发现在编译安装配置时候一直过不去 百度查询过很多解决办法 基本都是没有安装好编译环境之类的说法 但是在确定编译环境所涉及的包全部都安装以后 还是
  • idea必备开发插件.

    1 lombok 支持lombok的各种注解 从此不用写getter setter这些 可以把注解还原为原本的java代码 非常方便 https plugins jetbrains com plugin 6317 lombok plugin
  • 2022 RoboCom 世界机器人开发者大赛-本科组(省赛)-RC-u5 树与二分图

    2022 RoboCom 世界机器人开发者大赛 本科组 省赛 RC u5 树与二分图 文章目录 2022 RoboCom 世界机器人开发者大赛 本科组 省赛 RC u5 树与二分图 题目描述 输入格式 输出格式 输入样例 输出样例 思路 A
  • 感知机分类学习

    感知机 perceptron 是一种二类分类的线性分类模型 也就是说 使用于将数据分成两类的 并且数据要线性可分的情况 线性可分是指存在一个超平面能够将空间分成两部分 每一部分为一类 感知机的目的就在于找这样的一个超平面 假设输入数据形式为
  • pandas入门

    pandas is a fast powerful flexible and easy to use open source data analysis and manipulation tool 一 读取文本文件中的数据 导入pandas
  • Python之创建多级菜单

    方法一 usr bin env python coding utf 8 Time 2021 11 25 19 09 Author Argonaut FileName 创建多级菜单 py 功能 可进可退的功能菜单 while True pri
  • 将C盘和桌面所在的E盘合并分区后,出现的路径问题解决方案

    问题一 开机时出现警告 由于启动计算机时出现页面配置问题 Windows在您的计算机上创建了一个临时页面文件 所有的磁盘驱动器的总页面大小可能稍大于您所指定的大小 解决方案 照着做就行 问题二 Windows 10系统开机显示 位置不可用C
  • shell脚本整段注释

    摘自 http zhidao baidu com link url XmCCZmfluRe6n8TjPRKJTx4GGOUPSGX1VNBm euqGdpKGpveTESxC0HL90UBNT5nZCvmvfq2oIJdP3JO5EoPSq
  • STM32关于PVD低电压能检测的知识

    在实际工程运用中需要对突发情况作出及时的相应 通常都需要考虑当系统电压下降或断电时 需要对控制系统加以保护 这时候就需要在程序中加入系统电压监测 PVD 供电电压降低到某一个电压值时 需要系统进入保护状态 执行紧急关闭任务 对系统数据进行保
  • DDR基础知识点汇总

    文章目录 文档推荐 DDR颗粒的电路图来源 DDR3 SDRAM电路结构高清图 DDR4 SDRAM电路结构高清图 DDR3 1866控制器 PHY 颗粒之间的带宽关系 channel gt DIMM gt rank gt chip gt
  • docker镜像中配置文件的修改

    docker镜像中配置文件的修改 需要修改docker里面的配置文件时 因为docker镜像里面没有vim 下载也比较麻烦 可以使用 docker cp docker镜像名 想要修改的文件的路径 想要复制到的路径 将镜像中的文件复制到镜像外
  • ARM芯片开发(S5PV210芯片)——定时器、看门狗、RTC

    1 计数器 计数器就是每隔一段固定的时间计数值就加一 于是我们可以根据计数值来计算时间 经过的时间 计数值x计数时间间隔 2 定时器 2 1 定时器介绍 定时器具有计时的功能 类似于我们手机自带的倒计时功能 比如我们先给定时器设置计时一小时
  • 从瀑布到敏捷——漫画解读软件开发模式变迁史

    网址 https www tapd cn forum view 36971 从文章中可知 1 瀑布模型 将客户隔绝在外并按顺序逐一完成的模式 从时间上来说 只有等上一交付件完成了 下一阶段才能开始是一种浪费 特点 文档驱动 单道生产 2 敏
  • JVM--基础--21--对象的内存布局

    JVM 基础 21 对象的内存布局 1 普通对象实例与数组对象实例的数据结构图 2 在HotSpot虚拟机中 对象在内存中存储的布局如下 2 1 对象头 Header 2 1 1 markword 用于存储对象自身的运行时数据 如哈希码 H
  • 递增二叉树-网易游戏

    递增二叉树 网易游戏 题目描述 给定一个二叉树 每个节点有一个正整数权值 若一棵二叉树 每一层的节点权值和都严格小于下一层的结点权值和 责成这棵二叉树为递增二叉树 现在给你一棵二叉树 你需要判断其是不是一棵递增二叉树 输入描述 输入的第一行
  • Redis的数据结构之bitmap

    背景 项目开发过程中 我们经常会使用boolean类型来存储数据 例如记录用户每天签到 签到了是1 没签则为0 如果我们需要统计一年内的签到数 如果采用String来存储 需要每个用户都要记录 365次 当用户数量非常大时 需要的存储空间非
  • Docker基础入门:镜像、容器导入导出与私有仓库搭建

    Docker基础入门 镜像导入导出与私有仓库搭建 一 Docker镜像 容器的导入和导出 1 1 Docker镜像的导出 1 2 Docker镜像的载入 1 3 Docker容器的导出 1 4 Docker容器的导入 二 镜像和容器导出和导
  • MPEG-1中I、B、P帧的基本编码原理

    在上篇文章中 我们对MPEG 1有了一个轮廓性的介绍 知道视像序列中的图像类型有三种 分别为I帧 P帧 和B帧 但是我们并没有更深入的去了解 编码这三种类型的图像数据时所采用的不同方式 只知道它们都是把图像分为以16x16像素的宏块 8x8
  • Java虚拟机14:Java对象大小、对象内存布局及锁状态变化

    一个对象占多少字节 关于对象的大小 对于C C 来说 都是有sizeof函数可以直接获取的 但是Java似乎没有这样的方法 不过还好 在JDK1 5之后引入了Instrumentation类 这个类提供了计算对象内存占用量的方法 至于具体I