文章目录
先抛出一个问题:一个空对象内存大小是多少?看完这篇文章或许会有收获
对象内存布局
对象头(Header)
对象头包含Mark Word、类型指针和数组长度。
Mark Word
Mark Word:用于存储程序运行时的标志位,如锁状态、GC分代年龄和哈希码等。在64位系统下,这部分占8字节;在32位系统下,占4字节
以64位系统为例,Mark Word结构图如下:
HashCode类似于对象的ID,通过Hash算法生成,常用equals()比较对象是否相等;
分代年龄是指该对象经历了多少次垃圾回收,默认情况下,一个对象在新生代中经历15次垃圾回收(分代年龄>15),仍然存活的话,便会进入老年代
锁标志位是JVM用来识别该对象是否被上锁,以及锁的级别(JVM根据锁膨胀过程会有偏向锁,轻量级锁和重量级锁三个等级);
Class Pointer(类型指针)
类型指针指向实例对象对应的类信息的内存地址,其占用的内存大小有两种情况:
当开启了指针压缩(64位系统默认开启),内存大小是4字节,不开启指针压缩,内存大小是8字节
指针压缩的对象:
- 类型指针
- 对象的实例数据
- 数组对象
数组长度
这部分有数组对象特有,其他对象不存在这部分。
实例数据(Instance Data)
64位系统下,
boolean 和 byte 1字节
char 和 short 2字节
int 和 float 4字节
double 和 long 8字节
对象引用reference 8字节
这部分是实例数据的大小。比如
public class User{
private Integer id;
private String userName;
private Hobby hobby;
private Object obj;
}
可以推断出 该对象的实例数据部分占的内存大小为4+8+8+8=28字节(不开启指针压缩)或者 4+4+4+4==16字节(开启指针压缩)
对齐填充(Padding)
对齐填充的目的是保证对象的大小是8字节的整数倍。不是必须的,如果对象大小已经是8字节的整数倍了,就不需要对齐填充了
CPU在进行内存访问时,一次寻址的指针大小是8字节
假设没有对齐填充,数据在内存的存放情况如下
如果想要访问类型为long的数据,因为CPU每次的寻址大小是8字节,则CPU必须两次读取内存,第一次CPU访问0x00-0x07,第二次访问0x08-0x0F,结合两次的结果 才能访问到long的数据
假如采用对齐填充的话,在char和long之间填充一个空字节,数据在内存的存放情况如下
如果这个时候想要访问long数据的话,只需要一次CPU访问,即读取0x08-0x0F。
对齐填充存在的意义就是为了提高CPU访问数据的效率,这是一种以空间换时间的做法;正如我们所见,虽然访问效率提高了(减少了内存访问次数),但是在0x07处产生了空间浪费。
JOL工具包分析对象内存布局
引入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>RELEASE</version>
</dependency>
public class User {
private Integer id;
private String name;
private Object obj;
private List<Integer> list;
public static void printf(User p) {
// 查看对象的整体结构信息
System.out.println(ClassLayout.parseInstance(p).toPrintable());
}
public static void main(String[] args) {
User user=new User();
System.out.println(user.hashCode());
User.printf(user);
}
}
输出结果如下:
495053715
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.mybatisplus.demo.demo.jvm.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
① 0 4 (object header) 01 93 eb 81 (00000001 10010011 11101011 10000001) (-2115267839)
① 4 4 (object header) 1d 00 00 00 (00011101 00000000 00000000 00000000) (29)
② 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
③ 12 4 java.lang.Integer User.id null
③ 16 4 java.lang.String User.name null
③ 20 4 java.lang.Object User.obj null
③ 24 4 java.util.List User.list null
④ 28 4 (loss due to the next object alignment)
⑤ Instance size: 32 bytes
⑥ Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以通过vm args : -XX:+PrintFlagsFinal 查看到UseCompressedOops 值为true。印证了前面说的64位操作系统默认开启指针压缩
对上面的输出格式做介绍:
标记①、②是对象头的部分,标记③是实例数据部分,标记④是对齐填充,标记⑤ 代表对象的大小,标记⑥ 代表因对齐填充导致内存消耗的大小(若不需要对齐填充,也就没有④和⑥了)
其中① 代表了Mark Word,占用8个字节,其中第一个字节是00000001,结合前面Mark Word的结构发现其是无锁结构的Mark Word。这里对第一个字节拆分下
再来分析下hashcode,其值是495053715,对应16进制是1d81eb93,对应二进制是 00011101 10000001 11101011 10010011
但是发现①处设置的值是93eb811d,刚好是hashcode的倒序存放,将二进制倒置10010011 11101011 10000001 00011101 刚好跟上面吻合。其实这是大端存储的存储方式。
大端存储: 高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。
小端存储: 高位字节存储在内存的高地址端,低位字节存储在内存的低地址端。这是我们平常的存放逻辑
举例:65306 对应 FF1A,如果采用小端存储就是FF1A,如果采用大端存储就是1AFF
因为Mark Word结构说这种hashCode是31位的,因此我认为对应原始二进制数据( 00011101 10000001 11101011 10010011)的最高位0
余下的3个字节,合24位没有使用。
②处是类型指针,指向对象类元数据的内存地址,由于使用了指针压缩,内存大小大小由8字节->4字节
③处是实例数据,跟我们之前分析的一致,使用了指针压缩,这几部分占用大小都是4字节(int 基本数据类型 占4个字节)。
对前面①、②、③处进行内存大小计算:(4+4)+4+(4+4+4+4)=28字节
由于规定对象大小是8字节的整数倍,需要填充4字节,凑够32字节,并且刚好按顺序①①、②③、③③处 都能凑成8字节的整数倍,因此对齐填充在最后一个③后
所以⑤处对象的大小是32字节,⑥处对齐填充的大小是4字节。
手动关闭指针压缩,看下内存大小变换
VM Options: -XX:-UseCompressedOops
495053715
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.mybatisplus.demo.demo.jvm.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 93 eb 81 (00000001 10010011 11101011 10000001) (-2115267839)
4 4 (object header) 1d 00 00 00 (00011101 00000000 00000000 00000000) (29)
8 4 (object header) 80 f0 66 0e (10000000 11110000 01100110 00001110) (241627264)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
① 16 4 int User.id 0
20 4 (alignment/padding gap)
24 8 java.lang.String User.name null
32 8 java.lang.Object User.obj null
40 8 java.util.List User.list null
Instance size: 48 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
可以发现 几个引用类型的内存大小均变成了8字节。通过前面的分析,对齐填充自然而然地在①后
问题: 如果一个空对象,内存大小是多少呢?
空对象的话,没有实例数据。
对象头部分:Mark Word 占8字节,类型指针(指针压缩):占4字节;类型指针(不启用指针压缩):占8字节,
对齐填充:为了保证对象的内存大小是8 的整数倍,需要填充4字节(指针压缩);不启用指针压缩,不需要填充字节
因此一个空对象大小是16字节。