Java虚拟机14:Java对象大小、对象内存布局及锁状态变化
一个对象占多少字节?
关于对象的大小,对于C/C++来说,都是有sizeof函数可以直接获取的,但是Java似乎没有这样的方法。不过还好,在JDK1.5之后引入了Instrumentation类,这个类提供了计算对象内存占用量的方法。至于具体Instrumentation类怎么用就不说了,可以参看这篇文章如何精确地测量java对象的大小。
不过有一点不同的是,这篇文章使用命令行传入JVM参数来指定代理,这里我通过Eclipse设置JVM参数:
后面的是我打的agent.jar的具体路径。剩下的就不说了,看一下测试代码:
public class JVMSizeofTest { @Test
public void testSize() {
System.out.println("Object对象的大小:" + JVMSizeof.sizeOf(new Object()) + "字节");
System.out.println("字符a的大小:" + JVMSizeof.sizeOf('a') + "字节");
System.out.println("整型1的大小:" + JVMSizeof.sizeOf(new Integer(1)) + "字节");
System.out.println("字符串aaaaa的大小:" + JVMSizeof.sizeOf(new String("aaaaa")) + "字节");
System.out.println("char型数组(长度为1)的大小:" + JVMSizeof.sizeOf(new char[1]) + "字节");
} }
运行结果为:
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对于普通对象和数组对象的大小计算方式有所不同,我画了一张图说明:
解释一下其中每个部分:
- Mark Word:存储对象运行时记录信息,占用内存大小与机器位数一样,即32位机占4字节,64位机占8字节
- 元数据指针:指向描述类型的Klass对象(Java类的C++对等体)的指针,Klass对象包含了实例对象所属类型的元数据,因此该字段被称为元数据指针,JVM在运行时将频繁使用这个指针定位到位于方法区内的类型信息。这个数据的大小稍后说
- 数组长度:数组对象特有,一个指向int型的引用类型,用于描述数组长度,这个数据的大小和元数据指针大小相同,同样稍后说
- 实例数据:实例数据就是8大基本数据类型byte、short、int、long、float、double、char、boolean(对象类型也是由这8大基本数据类型复合而成),每种数据类型占多少字节就不一一例举了
- 填充:不定,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对象的大小:
- 开启指针压缩时,8字节Mark Word + 4字节元数据指针 = 12字节,由于12字节不是8的倍数,因此填充4字节,对象Object占据16字节内存
- 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 = 16字节,由于16字节正好是8的倍数,因此不需要填充字节,对象Object占据16字节内存
接着是字符'a'的大小:
- 开启指针压缩时,8字节Mark Word + 4字节元数据指针 + 1字节char = 13字节,由于13字节不是8的倍数,因此填充3字节,字符'a'占据16字节内存
- 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 + 1字节char = 17字节,由于17字节不是8的倍数,因此填充7字节,字符'a'占据24字节内存
接着是整型1的大小:
- 开启指针压缩时,8字节Mark Word + 4字节元数据指针 + 4字节int = 16字节,由于16字节正好是8的倍数,因此不需要填充字节,整型1占据16字节内存
- 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 + 4字节int = 20字节,由于20字节正好是8的倍数,因此填充4字节,整型1占据24字节内存
接着是字符串"aaaaa"的大小,所有静态字段不需要管,只关注实例字段,String对象中实例字段有"char value[]"与"int hash",由此可知:
- 开启指针压缩时,8字节Mark Word + 4字节元数据指针 + 4字节引用 + 4字节int = 20字节,由于20字节不是8的倍数,因此填充4字节,字符串"aaaaa"占据24字节内存
- 关闭指针压缩时,8字节Mark Word + 8字节元数据指针 + 8字节引用 + 4字节int = 28字节,由于28字节不是8的倍数,因此填充4字节,字符串"aaaaa"占据32字节内存
最后是长度为1的char型数组的大小:
- 开启指针压缩时,8字节的Mark Word + 4字节的元数据指针 + 4字节的数组大小引用 + 1字节char = 17字节,由于17字节不是8的倍数,因此填充7字节,长度为1的char型数组占据24字节内存
- 关闭指针压缩时,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作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代码更低因此引入了偏向锁。偏向锁的获取过程为:
- 访问Mark Word中偏向锁的标识是否设置为1,所标志位是否为01----确认为可偏向状态
- 如果为可偏向状态,则测试线程id是否指向当前线程,如果是,执行(5),否则执行(3)
- 如果线程id并为指向当前线程,通过CAS操作竞争锁。如果竞争成功,则将Mark Word中的线程id设置为当前线程id,然后执行(5);如果竞争失败,执行(4)
- 如果CAS获取偏向锁失败,则表示有竞争。当达到全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁(因为偏向锁是假设没有竞争,但是这里出现了竞争,要对偏向锁进行升级),然后被阻塞在安全点的线程继续往下执行同步代码
- 执行同步代码
有获取就有释放,偏向锁的释放点在于上述的第(4)步,只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的释放过程为:
- 需要等待全局安全点(在这个时间点上没有字节码正在执行)
- 它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
- 偏向锁释放后恢复到未锁定(标识位为01)或轻量级锁(标识位为00)状态
轻量级锁
轻量级锁的加锁过程为:
- 在代码进入同步块的时候,如果同步对象锁状态为无锁状态,JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word,此时线程堆栈与对象头的状态如图所示
- 拷贝对象头中的Mark Word复制到锁记录中
- 拷贝成功后,JVM将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向Object Mark Word,如果更新成功,则执行步骤(4),否则执行步骤(5)
- 如果更新动作成功,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标识位设置为00,即表示此对象处于轻量级锁状态,此时线堆栈与对象头的状态如图所示
- 如果更新动作失败,JVM首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标识的状态值变为10,Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而当前线程变尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程
偏向锁、轻量级锁与重量级锁的对比
下面用一张表格来对比一下偏向锁、轻量级锁与重量级锁,网上看到的,我觉得写得非常好,为了加深记忆我自己又手打了一遍:
Java虚拟机14:Java对象大小、对象内存布局及锁状态变化的更多相关文章
- Java虚拟机18:Java对象大小、对象内存布局及锁状态变化
一个对象占多少字节? 关于对象的大小,对于C/C++来说,都是有sizeof函数可以直接获取的,但是Java似乎没有这样的方法.不过还好,在JDK1.5之后引入了Instrumentation类,这个 ...
- 深入理解java虚拟机【Java内存结构】
Java虚拟机规范规定的java虚拟机内存其实就是java虚拟机运行时数据区,其架构如下: 其中方法区和堆是由所有线程共享的数据区. Java虚拟机栈,本地方法栈和程序计数器是线程隔离的数据区. (1 ...
- 深入理解Java虚拟机之Java内存区域与内存溢出异常
Java内存区域与内存溢出异常 运行时数据区域 程序计数器 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域 Java虚拟机栈 Java虚拟机栈(Java ...
- 深入理解Java虚拟机之读书笔记一 自动内存管理机制
一.运行时数据区域 1.程序计数器是线程的私有空间,每个线程都有.针对线程执行的是Java代码还是Native代码有两种取值,Java代码时:虚拟机字节码指令的地址:Native代码时:计数值为Und ...
- 《深入理解Java虚拟机》之(一、内存区域)
一.java的体系构成: Java的技术体系主要由支撑java程序运行的虚拟机.提供各种开发领域接口支持的java api.java编程语言及许多第三方java框架(如Spring .Struts等) ...
- JAVA虚拟机体系结构JAVA虚拟机的生命周期
一个运行时的Java虚拟机实例的天职是:负责运行一个java程序.当启动一个Java程序时,一个虚拟机实例也就诞生了.当该程序关闭退出,这个虚拟机实例也就随之消亡.如果同一台计算机上同时运行三个Jav ...
- 深入理解 Java 虚拟机——走近 Java
1.1 - 概述 Java 总述:Java 不仅是一门编程语言,还是一个由一系列 计算机软件 和 规范 形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于 嵌入式 ...
- 深入理解Java虚拟机-走进Java
一.Java技术体系 从广义上讲, Clojure. JRuby. Groovy等运行于Java虚拟机上的语言及其相关的程序都属于Java技术体系中的一员. 如果仅从传统意义上来看, Sun官方所定义 ...
- 《深入理解Java虚拟机》-Java代码是如何运行的
问题一:Java与C++区别 1.Java需要运行时环境,包括Java虚拟机以及Java核心类库等. 2.C++无需额外的运行时,通常编译后的代码可以让机器直接读取,即机器码 问题一:Java为什么要 ...
随机推荐
- bootstrap快速入门笔记(一)
一,头部基本格式:<head lang="en"> <meta charset="UTF-8"> <meta name=" ...
- JS模式--装饰者模式
在Javascript中动态的给对象添加职责的方式称作装饰者模式. 下面我们通常遇到的例子: var a = function () { alert(1); };//改成: var a = funct ...
- Fullcalendar 日历控件的基本使用
1:Fullcalendar 日历控件的基本简介 Fullcalendar是一款十分强大的开源日历免费控件,提供了丰富的属性设置和方法调用. 官网地址:https://fullcalendar.io/ ...
- 针对Mac的DuckHunter攻击演示
0x00 HID 攻击 HID是Human Interface Device的缩写,也就是人机交互设备,说通俗一点,HID设备一般指的是键盘.鼠标等等这一类用于为计算机提供数据输入的设备. DuckH ...
- 进程间通信系列 之 socket套接字及其实例
进程间通信系列 之 概述与对比 http://blog.csdn.net/younger_china/article/details/15808685 进程间通信系列 之 共享内存及其实例 ...
- CSS3 02. 边框、边框圆角、边框阴影、边框图片、渐变、线性渐变、径向渐变、背景、过渡transition、2D转换
边框圆角 border-radius 每个角可以设置两个值,x值.y值 border-top-left-radius:水平半径 垂直半径 border-radius:水平半径/垂直半径 border- ...
- 【算法系列学习三】[kuangbin带你飞]专题二 搜索进阶 之 A-Eight 反向bfs打表和康拓展开
[kuangbin带你飞]专题二 搜索进阶 之 A-Eight 这是一道经典的八数码问题.首先,简单介绍一下八数码问题: 八数码问题也称为九宫问题.在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的 ...
- seajs模块标识命名和解析规则
模块标识采用路径形式,但要注意与路径的区别.require.require.async的第一个参数是模块标识.而seajs.use第一个参数为文件路径. use是全局的,require是局部的.模块标 ...
- Node.js编写CLI的实践
导语:通常而言,Node.js的应用场景有前后端分离.海量web页面渲染服务.命令行工具和桌面端应用等等.本篇文章选取CLI(Command Line Tools)这子领域,来谈谈Node.js编写C ...
- 【企业级框架整合】Springmvc+mybatis+restful+bootstrap框架整合
1. 使用阿里巴巴Druid连接池(高效.功能强大.可扩展性好的数据库连接池.监控数据库访问性能.支持Common-Logging.Log4j和JdkLog,监控数据库访问)2. 提供高并发JMS消息 ...