• 在HotSpot中,对象在内存中存储的布局
    • 对象头Header
      • 对象自身的运行时数据
      • 类型指针
    • 实例数据Instance Data
    • 对齐填充Padding

Markdown

对象头Header

对象自身的运行时数据(Mark Word)

  • 包含内容
    • 哈希码(HashCode)
    • GC分代年龄
    • 锁状态标志
    • 线程持有的锁
    • 偏向线程ID
    • 偏向时间戳
  • 长度
    • 32位虚拟机
      • 32bit
    • 64位虚拟机
      • 64bit(未开启指针压缩)
      • 32bit(开启指针压缩)
  • 非固定的数据结构
    • 原因
      • 对象头信息是与对象自身定义的数据无关的额外存储成本
      • 考虑虚拟机的空间效率
    • 目的
      • 为了在极小的空间存储尽量多的信息
    • 根据对象的状态复用自己的存储空间
实例
  • 在32位HotSpot虚拟机中,如果对象处于未被锁定的状态下
    • 25bit->对象哈希码
    • 4bit->对象分代年龄
    • 2bit->锁标志位
    • 1bit->固定位0
  • 其它状态下(轻量级锁定、重量级锁定、GC标记、可偏向)下对象存储内容
存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

类型指针

对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  • 注意
    • 查找对象的元数据信息不一定要经过对象本身。
    • 如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据
      • 虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但无法从数组的元数据中确定数组的大小

实例数据Instance Data

对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

无论是从父类继承下来的,还是在子类中定义的,都需要记录。

  • 影响存储顺序的原因
    • 虚拟机分配策略参数(FieldsAllocationStyle)
    • 字段在Java远吗中定义的顺序

HotSpot虚拟机默认的分配策略

相同宽度的字段总是被分配到一起

  • longs/doubles
  • ints/floats
  • shorts/chars
  • bytes/boolean
  • references
  • oops(Ordinary Object Pointers)

在此前提下,在父类中定义的变量会出现在子类之前。

如果CompactFields参数值为true(默认为true),那么子类中较窄的变量也可能会插入到父类变量的空隙之中。代码参考下面的测试实例中CompactFields()

对齐填充

不是必然存在,无特别含义,仅仅是占位符

由于HotSpot VM的自动内存管理系统要求对象起始地址(对象的大小)必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象占用内存大小

普通对象占用内存情况

32位系统 64位系统(+UseCompressedOops) 64位系统(-UseCompressedOops)
Mark Word 4 bytes 8 bytes 8 bytes
Class Pointer 4 bytes 4 bytes 8 bytes
对象头 8 bytes 12 bytes 16 bytes

数组对象占用内存情况

32位系统 64位系统(+UseCompressedOops) 64位系统(-UseCompressedOops)
Mark Word 4 bytes 8 bytes 8 bytes
Class Pointer 4 bytes 4 bytes 8 bytes
Length 4bytes 4bytes 4bytes
对象头 12 bytes 16 bytes 20 bytes

实例数据

Type 32位系统 64位系统 (+UseCompressedOops) 64位系统 (-UseCompressedOops)
double 8bytes 8bytes 8bytes
long 8bytes 8bytes 8bytes
float 4bytes 4bytes 4bytes
int 4bytes 4bytes 4bytes
char 2bytes 2bytes 2bytes
short 2bytes 2bytes 2bytes
byte 1bytes 1bytes 1bytes
boolean 1bytes 1bytes 1bytes
oops(ordinary object pointers) 4 bytes 4 bytes 8 bytes

测试实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package com.super2bai.jvm;

import java.lang.reflect.Field;
import java.util.Collection;

import org.omg.PortableServer.ServantActivator;

import com.javamex.classmexer.MemoryUtil;
import com.javamex.classmexer.MemoryUtil.VisibilityFilter;

import sun.misc.Unsafe;

/**
* ① 将下载的 classmexer.jar 加入当前项目的classpath中<br>
* ② 启动Main是添加启动项:<br>
* -javaagent:/Users/2bai/Downloads/classmexer.jar<br>
*
* ③ JVM 参数: <br>
* -XX:+UseCompressedOops (默认启用)<br>
* -XX:+CompactFields (默认启用)<br>
* -XX:FieldsAllocationStyle=1 (默认为1)<br>
*
* JDK版本:1.8
*/
public class TheObjectMemory {

private static Unsafe UNSAFE;
// 获得Unsafe
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(Unsafe.class);
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws NoSuchFieldException {
// 测试对象内存布局
testObjectLayout();
// 测试对象内存占用
testMemoryUsage();
// 测试对齐填充位置
testPaddingLocation();
// 测试空隙插入
testCompactFields();
// 测试基本类型分配后再分配引用类型
testReferenceAllocate();
// 测试对象总大小,包含引用对象占用的内存
testDeepMemoryUsage();
// 测试数组对象占用内存大小
testObjectArrayMemoryUsage();
}

/**
* 测试类
*/
private static class ObjectA {
String str; // 4
int i1; // 4
byte b1; // 1
byte b2; // 1
int i2; // 4
Object obj; // 4
byte b3; // 1
}

/**
* 测试对象内存布局
*/
public static void testObjectLayout() {
System.out.println("****测试对象内存布局****");
/**
* Oop指针是4还是未压缩的8也可以通过unsafe.arrayIndexScale(Object[].class)来获得<br>
* 这个方法返回一个引用所占用的长度<br>
* 因为在JDK 8中"UseCompressedOops"选项是默认启用的,因此class pointer只占用了4个字节。<br>
*/
int objectRefSize = UNSAFE.arrayIndexScale(ObjectA[].class);
System.out.println("ObjectA, Oop指针, length=" + objectRefSize);

// 通过反射获得一个类的Field
Field[] fields = ObjectA.class.getDeclaredFields();

for (Field f : fields) {
// 通过Unsafe的objectFieldOffset()获得每个Field的offSet
// 可以看到确实是按照从长到短,引用排最后的方式在内存中排列的。
// 对Field按照offset排序,取得最小的offset,然后加上这个field的长度,再加上Padding对齐
System.out.println("ObjectA, " + f.getType() + " " + f.getName() + " offset: " + UNSAFE.objectFieldOffset(f));
}
/**
* 上面三步就可以获得一个对象的Shallow size。 可以进一步通过递归去计算所引用对象的大小,从而可以计算出一个对象所占用的实际大小。
* 对象头(8)+Oops(4)+i1(4)+i2(4)+b1(1)+b2(1)+b3(1)+padding(1)+str(4)+obj(4)=32
* 32正好为8的倍数,所以不需要额外填充
*/
}

/**
* 测试对象内存占用
*/
public static void testMemoryUsage() {
System.out.println("****测试对象内存占用****");
/**
* returns the number of bytes occupied by an object<br>
* not included any objects it refers to
*/
ObjectA a = new ObjectA();
System.out.println("ObjectA.class, memoryUsage : " + MemoryUtil.memoryUsageOf(a));
long noBytes1 = MemoryUtil.deepMemoryUsageOf(a, VisibilityFilter.ALL);
System.out.println("ObjectA.class, VisibilityFilter=All, deepMemoryUsageOf=" + noBytes1);
}

private static class ObjectB {
long a;
long b;
}

/**
* 测试对齐填充位置<br>
* 对象头(8)+Oops(4)+padding(4)+a(8)+b(8)<br>
* padding为什么不是在最后呢?<br>
* 是这样的,在64位系统中,CPU一次读操作可读取64bit(8 bytes)的数据。<br>
* 如果,你在对象头分配后就进行属性 long a字段的分配,<br>
* 也就是说从偏移量为12的地方分配8个字节,这将导致读取属性long a时需要执行两次读数据操作。<br>
* 因为第一次读取 到的数据中前4字节是对象头的内存,<br>
* 后4字节是属性long a的高4位(Java 是大端模式),<br>
* 低4位的数据则需要通过第二次读取 操作获得。
*/
public static void testPaddingLocation() throws NoSuchFieldException {
System.out.println("****测试对齐填充位置****");
ObjectB b = new ObjectB();
// memoryUsage : 32
System.out.println("ObjectB, memoryUsage : " + MemoryUtil.memoryUsageOf(b));
// a field offset : 16
System.out.println("ObjectB, long a offset : " + UNSAFE.objectFieldOffset(ObjectB.class.getDeclaredField("a")));
// b field offset : 24
System.out.println("ObjectB, long b offset : " + UNSAFE.objectFieldOffset(ObjectB.class.getDeclaredField("b")));
}

private static class ObjectC {
long a;
int b;
}

/**
* 测试空隙插入<br>
* 对象头(8)+Oops(4)+b(4)+a(8)<br>
* 在前面的理论中,我们说过基本变量类型在内存中的存放顺序是从大到小的<br>
* (顺序:longs/doubles、ints、shorts/chars、bytes/booleans)。<br>
* 所以,按理来说,属性int b应该被分配到了属性long a的后面。<br>
* 但是,从属性位置偏移量的结果来看,<br>
* 我们却发现属性int b被分配到了属性long a的前面,<br>
* 这是为什么?<br>
* 是这样的,因为JVM启用了'CompactFields'选项,<br>
* 该选项运行分配的非静态(non-static)字段被插入到前面字段的空隙中,以提供内存的利用率。<br>
* 从前面的实例中,我们已经知道,对象头占用了12个字节,<br>
* 并且再次之后分配的long类型字段不会紧跟在对象头后面分配,<br>
* 而是再新一个8字节偏移量位置处开始分配,因此对象头和属性long a直接存在了4字节的空隙,<br>
* 而这个4字节空隙的大小符合(即,大小足以用于)属性int b的内存分配。<br>
* 所以,属性int b就被插入到了对象头与属性long a之间了。<br>
*
* @throws NoSuchFieldException
* @throws SecurityException
*/
public static void testCompactFields() throws NoSuchFieldException, SecurityException {
System.out.println("****测试空隙插入****");
ObjectC obj = new ObjectC();
// memoryUsage : 24
System.out.println("ObjectC, memoryUsage : " + MemoryUtil.memoryUsageOf(obj));
// a field offset : 16
System.out.println("ObjectC, long a offset : " + UNSAFE.objectFieldOffset(ObjectC.class.getDeclaredField("a")));
// b field offset : 12
System.out.println("ObjectC, int b offset : " + UNSAFE.objectFieldOffset(ObjectC.class.getDeclaredField("b")));
}

private static class ObjectD {
int a;
long b;
String str;
}

/**
* 测试基本类型分配后再分配引用类型<br>
* 对象头(8)+Oops(4)+a(4)+b(8)+str(4)+padding(4)=32<br>
* 从属性 int a、long b,以及对象引用 str 的偏移量可以发现,对象引用是在基本变量分配完后才进行的分配的。<br>
* 这是通过JVM选项'FieldsAllocationStyle=1'决定的,FieldsAllocationStyle的值为1,<br>
* 说明:先放入基本变量类型<br>
* (顺序:longs/doubles、ints、shorts/chars、bytes/booleans),<br>
* 然后放入oops(普通对象引用指针)<br>
*
* @throws NoSuchFieldException
* @throws SecurityException
*/
public static void testReferenceAllocate() throws NoSuchFieldException, SecurityException {
System.out.println("****测试基本类型分配后再分配引用类型****");
ObjectD obj = new ObjectD();
// memoryUsage : 24
System.out.println("ObjectD, memoryUsage : " + MemoryUtil.memoryUsageOf(obj));

// a field offset : 12
System.out.println("ObjectD, int a offset : " + UNSAFE.objectFieldOffset(ObjectD.class.getDeclaredField("a")));

// str field offset : 16
System.out.println("ObjectD, long b offset : " + UNSAFE.objectFieldOffset(ObjectD.class.getDeclaredField("b")));

// str field offset : 24
System.out.println("ObjectD, String str offset : " + UNSAFE.objectFieldOffset(ObjectD.class.getDeclaredField("str")));
}

static class TheInnerObject {
int innerA;
}

/**
* memoryUsageOf方法仅计算了对象本身的大小,并未包含引用对象的内存大小<br>
* (注意,memoryUsageOf方法计算的是引用指针的对象,而非引用对象占用的内存大小)。<br>
* deepMemoryUsageOf方法则会将引用对象占用的内存大小也计算进来。<br>
*
* 注意,deepMemoryUsageOf(Object obj)默认只会包含non-public的引用对象的大小。<br>
* 如果你想将public引用对象的大小也计算在内,可通过deepMemoryUsageOf重载方法<br>
* deepMemoryUsageOf(Object obj, VisibilityFilter referenceFilter),<br>
* VisibilityFilter参数传入 'VisibilityFilter.ALL'来实现。<br>
*
*/
TheInnerObject innerObject = new TheInnerObject();

public static void testDeepMemoryUsage() {
System.out.println("****测试对象总大小,包含引用对象占用的内存****");
TheObjectMemory obj = new TheObjectMemory();

// TheObjectMemory memoryUsage : 16
System.out.println("TheObjectMemory memoryUsage : " + MemoryUtil.memoryUsageOf(obj));

// TheInnerObject memoryUsage : 16
TheInnerObject innerObj = new TheInnerObject();
System.out.println("TheInnerObject memoryUsage : " + MemoryUtil.memoryUsageOf(innerObj));

// TheObjectMemory deepMemoryUsageOf : 32
System.out.println("TheObjectMemory deepMemoryUsageOf : " + MemoryUtil.deepMemoryUsageOf(obj));
}

int a;
String str = "hello";

/**
* 测试数组对象占用内存大小
*
* PS:运行此方法需注释掉 225行的内容:TheInnerObject innerObject = new TheInnerObject();
* 否则内存大小会包含上面实例所占用的内存(16) 对象头(8)+Oops(4)+innerA(4)=16
*
* 数组对象自身占用的内存大小 = 对象头 + 数组长度 * 元素引用指针/基本数据类型大小 + 对齐填充
*
* ● 对象头:mark word(8 bytes) + class pointer(4 bytes) + length(4 bytes) = 16
* bytes 因为在JDK 8 中"UseCompressedOops"选项是默认启用的,因此class pointer只占用了4个字节。
*
* ● 实例数据:数组长度(1) * 对象引用指针(4 bytes) = 4 bytes
*
* ● 对齐填充:4 bytes
*
* 对象占用内存大小:对象头(16) + 实例数据(4) + 对齐填充(4) = 24
*
* deepMemoryUsageOf = array memoryUsage + array_length(数组长度) *
* item_deepMemoryUsage (元素占用 的全部内存)
*
* 注意,这里的数组是一个对象数组,因此memoryUsage中计算的是对象引用指针的大小。如果是一个基本数据类型的数组,如,
* int[],则,memoryUsage计算的就是基本数据类型的大小了。也就是说,如果是基本数据类型的数组的话,memoryUsage
* 的值是等于deepMemoryUsageOf的值的。
*
*/
public static void testObjectArrayMemoryUsage() {
System.out.println("****测试数组对象占用内存大小***");
TheObjectMemory[] objArray = new TheObjectMemory[1];
TheObjectMemory obj = new TheObjectMemory();
objArray[0] = obj;

// memoryUsage : 24
System.out.println("objArray memoryUsage : " + MemoryUtil.memoryUsageOf(objArray));

// deepMemoryUsageOf : 104
System.out.println("objArray deepMemoryUsageOf : " + MemoryUtil.deepMemoryUsageOf(objArray));

// obj memoryUsage : 24
System.out.println("obj memoryUsage : " + MemoryUtil.memoryUsageOf(obj));
// obj deepMemoryUsageOf : 80
System.out.println("obj deepMemoryUsageOf : " + MemoryUtil.deepMemoryUsageOf(obj));

// first item offset(数组第一个元素的内存地址偏移量) : 16
System.out.println("first item offset : " + UNSAFE.arrayBaseOffset(objArray.getClass()));
}
}

参考

JVM中 对象的内存布局 以及 实例分析

《深入理解Java虚拟机》

Classmexer agent

R大(RednaxelaFX)同事写的显示Java对象布局的工具