Java对象内存布局
本文转载自Java对象内存布局
导语
首先直接抛出问题
Unsafe.getInt(obj, fieldOffset)
中的fieldOffset
是什么, 类似还有compareAndSwapX(obj, fieldOffset, oldValue, newValue)
?- 如何实现原子读, 原子写的
Java
反射是怎么实现Java
synchronized
锁是如何实现
要解答这些问题, 需要了解Java对象内存布局
Java对象内存布局
主要分为对象头和实例数据2部分
对象头又分成Mark Word
和Class Metadata Pointer
2部分
实例数据就是对象里定义的Field列表, 顺序并非严格按照源码里声明的顺序(但有一定的规则), Unit的起始位置相对于对象的位置就是字段的偏移量, 字段的偏移量需要满足内存对齐的要求
普通对象的内存整体布局
+-------------+------------------+
| | Mark Word |
| Object Head +------------------+
| | Metadata Pointer |
+-------------+------------------+
| | Unit |
| Instance +------------------+
| | ... |
| Data +------------------+
| | Unit |
+-------------+------------------+
数组对象的内存布局
+-------------+------------------+
| | Mark Word |
| +------------------+
| Object Head | Metadata Pointer |
| +------------------+
| | array length |
+-------------+------------------+
| | Unit |
| Instance +------------------+
| | ... |
| Data +------------------+
| | Unit |
+-------------+------------------+
下面逐一介绍每部分的结构及作用
Mark Word
任何Java对象都有此部分信息及内存消耗, 这部分归JVM管理,JDK层面无API修改此部分数据
这里主要记录对象锁信息和GC标记, 32bit虚拟机与64位虚拟机给Mark Word
区域分配的空间分为是32bit和64bit
但为了最大效率使用这部分空间, Mark Word的结构是非固定的
比如在32bit虚拟机中, 对于无锁态类型的对象, 其中25位用来存储hashcode
;
而在偏向锁类型的对象中, 23bit用来记录当前获取锁的线程ID
32bit的JDK结构如下:
+--------+-----------------------+-------+----------+------------------+--------------+
| 锁状态 | 23bit | 2bit | 4bit | 1bit(是否偏向锁) | 2bit(锁标志) |
+--------+-----------------------+-------+----------+------------------+--------------+
| 无锁态 | 对象的Hascode | 分代年龄 | 0 | 01 |
+--------+-------------------------------+----------+------------------+--------------+
|轻量级锁| 指向栈中锁记录的指针 | 00 |
+--------+-------------------------------------------------------------+--------------+
|重量级锁| 指向互斥量(重量级锁)的指针 | 10 |
+--------+-------------------------------------------------------------+--------------+
| GC标记 | 空 | 11 |
+--------+-------------------------------------------------------------+--------------+
| 偏向锁 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
+--------|-----------------------+-------+----------+------------------+--------------+
Java中任何对象都可以用来做锁, synchronized
关键字底层实现原理跟Mark Word
相关
在JDK1.6之前, synchronized
实现的锁是重量级, 性能较差(锁状态切换,涉及OS的线程在用户态与系统态之间切换)
在1.6之后, 针对各种场景进行优化, 如偏向锁, 轻量级锁(自旋锁), synchronized
的性能也有了很大提升, 并且synchronized
的使用比Lock
要简单安全, 所以JDK推荐优先使用synchronized
; 并且由于synchronized
语义比较明确, 后续还有优化的空间
由于本文重点不在说明synchronized
的实现原理, 想了解更多可以参考zejian大神的这篇文章深入理解Java并发之synchronized实现原理, 附上一张大神绘制的图以表敬意
Class Metadata Pointer
主要是获取对象的一些元信息, 比如类名, 包名, 字段列表, 方法列表等等
Instance Data
这里就是每个对象实例数据, 具体点就是对象里每个字段的值或数组对象里每个元素的值; 既然有值就一定会有类型, 在Java里数据类型分为基本类型与引用类型
数据类型
对于基本类型, 每种类型的占用空间大小如下, 单位B
+------+---------+-------+------+-----+-------+------+--------+
| byte | boolean | short | char | int | float | long | double |
+------+---------+-------+------+-----+-------+------+--------+
| 1 | 1 | 2 | 2 | 4 | 4 | 8 | 8 |
+------+---------+-------+------+-----+-------+------+--------+
而对于Reference类型, 一般跟OS的位数相同, 在64bit的操作系统上, 就是64位长度, 也就8B, 同理在32bit的虚拟机里就是4B; 这样对于有些从32位虚拟机移植过来的程序, 可能内存开销增加了50%以上;
所以Java
提供了一个启动参数用来设置Reference
的大小, 也就是内存地址压缩, 默认是开启压缩的; 注意: 地址压缩只是针对64位虚拟机的引用类型的优化
开启参数
-XX:+UseCompressedOops
关闭参数
-XX:-UseCompressedOops
差别就在那个+和-
内存对齐
内存对齐是提升程序性能的关键, 具体原因可以参考文章后面的附录, 也可以自行检索
简单的理解就是: 如果变量的内存地址是类型长度的整数倍, CPU只需一次访问即可; 否则就要多次访问并把每次结果进行拼接才能获得最终值
看看最上面的对象结构里的实例数据部分, 我按照自己的理解画成了一个个Unit
每个Unit
里面可放一个或多个Field
, 同一个Unit
里的类型可以不同, 但是长度必须相同, 比如byte
和boolean
, short
和char
可以放在一起
对齐规则:
上一数据结束位置 % 类型长度 == 0
需要补齐的大小
类型长度 - 上一数据结束位置 % 类型长度
比如对于一个long
类型的字段来说, 如果当前的偏移量是12, 那么 12 % 8 != 0
, 不对齐, 需要padding=8-12*8=4bit
;
具体可以见下面的例子
字段偏移量
每个对象在内存都有一个内存地址, 通过内存地址+类型, 我们就可以取出对象的值; 对于对象里的字段, 也是相似操作 地址的值一般来讲也是比较长的, 如果每个对象的字段地址都是用真实的地址值, 也比较浪费内存; 所以Java里采用了字段偏移量来实现, 可以理解为相对于对象起始位置的距离, 要获取真实地址只需要
FieldAddress = ObjectAddress + objectFieldOffset
由于Java里字段又分为类字段(静态的, 跟类相关)和实例字段(非静态, 跟对象相关), 对于静态字段
StaticFieldAddress = ClassAddress + staticFieldOffset
我们可以通过Unsafe.objectFieldOffset(Field)
来获取一个对象的字段偏移量, 通过Unsafe.staticFieldOffset(Field)
来获取一个类的字段偏移量
字段偏移量的值可以通过以下数学归纳法计算:
如果是第一个字段
fieldOffset1 = ObjectHeaderLength
在64bit, 地址压缩的情况下, 对象头长度是12byte(8byte mark word + 4byte metadata pointer);
同理, 地址不压缩对象头长度是16byte(8byte mark word + 8byte metadata pointer)
如果是非第一个字段
fieldOffset1 = 上一数据结束位置 + padding = 上一个字段的偏移量 + 上一个字段的长度 + padding
如果fieldOffset1
可以对齐field的类型长度, 则field的偏移量地址=fieldOffset1
如果fieldOffset1
不能对齐field的类型长度, 则field的偏移量地址=fieldOffset1+padding
判断是否对齐及padding
的计算参见上一节内存对齐
最终的计算结果跟3个因素相关
- 内存对象字段排序, 这个在下一节重点说明
- 补齐(
Padding
) - 地址压缩
字段的排序规则
- 排序规则的目的是尽可能减少
Padding
- 先基本类型, 再引用类型; 先长后短, 长度相同就按声明顺序
- 基本类型先大后小(8,4,2,1), 但有例外
Java类型的长度无非是1byte,2byte,4byte,8byte
这4种, 实例的数据也就是这4值的各种组合; 先长后短还是先短后长并不能减少Padding
的浪费, 但是先长后短可以减少内存碎片化, 或者说是连续的内存地址空间, 因为Padding
的部分都在对象的尾部(理解可能有误)
在于64bit压缩情况下, 对象头是12byte
, 如果对象有3个Field
, 分别是int, long, boolean
;
如果严格按照先大后小, 则应该是long, int, boolean
; 按照上一节偏移量的计算:
offset1 = 12
而long
是8byte
, 12%8!=0
, 需要padding=8-12%8=4byte
; 整个内存布局大概类似如下:
+----------------------+---------------+------------+-----------+---------------+---------------+
| Object Head 12 bit | padding 4 bit | long 8 bit | int 4 bit | boolean 1 bit | padding 3 bit |
0 12 16 24 28 29 32
累计padding=4+3=7bit
, 仔细分析, 在这种场景下, 还有更好的排列可以节省掉第一个padding
我们可以把后面的int放到空缺的4bit里, 反正这4bit不用白不用, 即如下布局
+----------------------+-----------+------------+---------------+---------------+
| Object Head 12 bit | int 4 bit | long 8 bit | boolean 1 bit | padding 3 bit |
0 12 16 24 25 28
这就是规则3的例外情况, 对于这空缺的4bit, 优先拿4bit类型的数据来填充, 次之2bit, 次之1bit; 如果都没有, 那就只能浪费了
对于64bit地址压缩的JVM, 内存对象字段大概是如下布局:
+----------------------------+
| 4 byte |
+----------------------------+
| Object Header 12 byte |
| Mark Word 8 byte |
| Metadata Pointer 4 byte |
+----------------------------+
| int or float 4 byte |
+----------------------------+
| long or double 8 byte |
+----------------------------+
| int or float 4 byte |
+----------------------------+
| char or short 2 byte |
+----------------------------+
| byte or boolean 1 byte |
+----------------------------+
| Reference 4 byte |
+----------------------------+
对于64bit非地址压缩的JVM, 内存对象字段大概是如下布局:
+----------------------------+
| 8 byte |
+----------------------------+
| Object Header 16 byte |
| Mark Word 8 byte |
| Metadata Pointer 8 byte |
+----------------------------+
| long or double 8 byte |
+----------------------------+
| int or float 4 byte |
+----------------------------+
| char or short 2 byte |
+----------------------------+
| byte or boolean 1 byte |
+----------------------------+
| Reference 8 byte |
+----------------------------+
以下通过代码来对上面的规则做一个证明
public class MemoryLayout {
private String name;
private int age;
private boolean sex;
private byte young;
public static void main(String[] args) {
long ageOffset = getFieldOffset("age");
long sexOffset = getFieldOffset("sex");
long youngOffset = getFieldOffset("young");
long nameOffset = getFieldOffset("name");
System.out.println("--> " + ageOffset);
System.out.println("--> " + sexOffset);
System.out.println("--> " + youngOffset);
System.out.println("--> " + nameOffset);
}
public static long getFieldOffset(String fieldName) {
try {
Field field = MemoryLayout.class.getDeclaredField(fieldName);
return UnsafeKit.getUnsafe().objectFieldOffset(field);
} catch (Exception e) {
throw new RuntimeException("not exist field [" + fieldName + "] in MemoryLayout");
}
}
}
我的JVM环境是64bit, 默认地址压缩, 程序的输出结果是:
--> 12
--> 16
--> 17
--> 20
- 数值分别表示每个字段的偏移量
- 每个字段的偏移量跟源码里的声明顺序并不一致, 按先基本类型, 再引用类型, 先大后小的规则, 字段的顺序是: age, sex, young, name
- age是第一个字段, 所以偏移量就是对象头大小, 在64bit地址压缩的JVM里就是12
- sex的计算过程: 上一个字段的偏移量 + 上一个字段的长度
=12+4=16
, boolean类型1bit,16%1==0
, 内存对齐, 所以结果就是16 - young的计算过程: 上一个字段的偏移量 + 上一个字段的长度=16+1=17, byte类型1bit,
17%1==0
, 内存对齐, 所以结果就是17 - name的计算过程: 上一个字段的偏移量 + 上一个字段的长度=17+1=18, 引用类型4bit,
18%4!=0
, 不对齐需要padding4-18%4=2bit
, 所以结果是18+2=20
总结
- Java所有对象都是有个
ObjectHead
, 由JVM维护; 主要用于锁及GC相关的 - 为了提升Java读写效率, Java所有字段在内存中需要内存对齐
- 为了减少Padding消耗, Java对对象的字段进行了一定规则的重排序
- 通过Java地址+字段的偏移量, 就可以操作内存里的数据; 整个Unsafe类就是基于这个来实现原子读写
- Java反射就是用Unsafe实现, 来直接操作数据
- 整个Java并发包都是基于Unsafe来实现并发安全, 包括但不限于: AQS, CAS, Lock, 信号量, Condition
参考
- java对象在内存中的结构(HotSpot虚拟机)
- 深入理解Java并发之synchronized实现原理
- Java直接内存对齐(Memory Alignment)
- 轻松搞定内存对齐
- 可能是把Java内存区域讲的最清楚的一篇文章
- Java对象内存布局
Java对象内存布局的更多相关文章
- 图文详解Java对象内存布局
作为一名Java程序员,我们在日常工作中使用这款面向对象的编程语言时,做的最频繁的操作大概就是去创建一个个的对象了.对象的创建方式虽然有很多,可以通过new.反射.clone.反序列化等不同方式来创建 ...
- 附 Java对象内存布局
注意:本篇博客,主要参考自<深入理解Java虚拟机(第二版)> 1.对象在内存中存储的布局分为三块 对象头 存储对象自身的运行时数据:Mark Word(在32bit和64bit虚拟机上长 ...
- Java单个对象内存布局.md
我们在如何获取一个Java对象所占内存大小的文章中写了一个获取Java对象所占内存大小的工具类(ObjectSizeFetcher),那么接下来,我们使用这个工具类来看一下Java中各种类型的对象所占 ...
- Java虚拟机14:Java对象大小、对象内存布局及锁状态变化
一个对象占多少字节? 关于对象的大小,对于C/C++来说,都是有sizeof函数可以直接获取的,但是Java似乎没有这样的方法.不过还好,在JDK1.5之后引入了Instrumentation类,这个 ...
- Java虚拟机18:Java对象大小、对象内存布局及锁状态变化
一个对象占多少字节? 关于对象的大小,对于C/C++来说,都是有sizeof函数可以直接获取的,但是Java似乎没有这样的方法.不过还好,在JDK1.5之后引入了Instrumentation类,这个 ...
- Ehcache计算Java对象内存大小
在EHCache中,可以设置maxBytesLocalHeap.maxBytesLocalOffHeap.maxBytesLocalDisk值,以控制Cache占用的内存.磁盘的大小(注:这里Off ...
- JAVA 对象内存结构
JAVA对象内存结构 HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header).实例数据(Instance Data)和对齐填充(Padding). 对象头 markWo ...
- [Java基础] Java对象内存结构
转载地址:http://www.importnew.com/1305.html 原文于2008年11月13日 发表, 2008年12月18日更新:这里还有一篇关于Java的Sizeof运算符的实用库的 ...
- java对象内存大小评估
Java对象的内存布局:对象头(Header).实例数据(Instance Data)和对齐填充(Padding).无论是32位还是64位的HotSpot,使用的都是8字节对齐.也就是说每个java对 ...
随机推荐
- 利用Javascript制作网页特效(时间特效)
在网页中经常可以看到各种各样的动态时间显示,在网页中合理地使用时间可以增加网页的时效感. 显示当前时间 getHours().getMinutes().getSeconds()分别获得当前小时数.当前 ...
- Centos6.5添加163软件yum源
将yum源设置为163yum,可以提升软件包安装和更新的速度,同时避免一些常见软件版本无法找到.具体设置方法如下: 1,进入yum源配置目录cd /etc/yum.repos.d 2,备份系统自带的y ...
- linux开发各种I/O操作简析,以及select、poll、epoll机制的对比
作者:良知犹存 转载授权以及围观:欢迎添加微信公众号:羽林君 IO 概念区分 四个相关概念: 同步(Synchronous) 异步( Asynchronous) 阻塞( Blocking ) 非阻塞( ...
- F - F(最小生成树)
题意:连通各点最短距离,最小生成树. You are assigned to design network connections between certain points in a wide a ...
- Luogu T16048 会议选址
本题idea版权来自CSDN博客Steve_Junior的医院设置2. 并没有什么用的链接 题目背景 \(A\)国的国情十分独特.它总共有\(n\)个城市,由\(n-1\)条道路连接.国内的城市当然是 ...
- HDU - 3281 dp
题意: 给你b个球,m个楼层,你需要找到一个楼层数k,使得从小于k这个楼层上面扔下去球,而球不会碎.求在最糟糕的情况下你最多要尝试多少次 题解: dp[i][j]表示你有b个球,楼层总数为m,你找到那 ...
- iOS网页调试
iOS上安装Chrome 打开Chrome://inspect,选择开始收集日志 新选项卡中访问目标站点 切换回日志收集页面,即可看到日志信息 https://blog.chromium.org/20 ...
- PowerShell随笔1---背景
既然是随笔,那就想到什么说什么,既会分享主题知识,也会分享一些其他技巧和个人学习方法,供交流. 我一般学习一个东西,我都会问几个问题: 这东西是什么? 这东西有什么用,为什么会出现,出现是为了解决什么 ...
- 国产网络测试仪MiniSMB - 如何3秒内创建出16,000条UDP/TCP端口号递增流
国产网络测试仪MiniSMB(www.minismb.com)是复刻smartbits的IP网络性能测试工具,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此以太 ...
- python之字符串split和rsplit的方法
1.描述 split()方法通过指定分隔符对字符串进行切片,如果参数num有指定值,则分隔num+1个子字符串,默认分隔符为所有空字符,包括空格.换行(\n).制表符(\t)等 rstrip()方法通 ...