你创建的对象真正占了多少内存?作为程序员基本每天都在 new 对象,那么 new 出来的对象真正占用了多少内存呢?你 new 出来的对象会不会导致 OOM ?不知道大家关注过没。
问题来源
之前写代码的时候遇到如下一个逻辑:有一个 10000 size 的 List,需要再创建一个 List,把数据都写进来,那么这个操作会新增多少内存占用呢?这个东西分析起来还挺有意思,那么就让我们以 ArrayList 为例来分析下 new 出来的对象所占用的内存吧。
首先我们要分析都先创建了那些对象?
List<String> list = Lists.newArrayListWithCapacity(10000);
我们通过上段代码创建了新 List,并执行了数据处理,可以看出来主要是创建了 10000 容量的 ArrayList。我们都知道 ArrayList 中存储的都是对象的引用,所以这部分操作增加的内存就是这个新的 ArrayList 需要分配的内存。
再看下 ArrayList 的源码会发下主要就下面两个私有字段,以及 AbstractList 中的 modCount 字段。
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
protected transient int modCount = 0;
也就是说 ArrayList 需要分配实例数据就是属性:size 跟 elementData 大小,由于 elementData 是数组,所以对 ArrayList 来说也只需要分配其引用即可。最核心的内存占用就是 Objecgt[] elementData 这个数组了,在初始化 ArrayList 时候如果指定其容量会执行如下代码,其实就直接初始化了一个相应大小的数组。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
对象分析
接着我们来分析如何计算一个对象的内存占用大小。
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头
HotSpot 虚拟机的对象头包括三部分信息:
- 第一部分 MarkWord,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit,官方称它为“MarkWord”
- 对象头的另外一部分是 KlassWord,类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
- 数组类型长度
MarkWord | KlassWord | Array Length | |
---|---|---|---|
32bit | 4 | 4 | 4 |
64bit | 8 | 8 | 4 |
64bit+comp | 8 | 4 | 4 |
实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
JVM 会根据如下顺序进行字段排序,为了是节省 padding 次数
- doubles (8) and longs (8)
- ints (4) and floats (4)
- shorts (2) and chars (2)
- booleans (1) and bytes (1)
- references (4/8(64bit未开启压缩))
- repeat for sub-class fields
可以看出来相同宽度的总是被分配在一起,在满足这个前提条件下,父类定义的变量会出现在子类之前,但如果开启了 CompactFields 参数,子类中较窄的变量也可能会插到父类变量空隙中。
对齐填充
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于 HotSpot JVM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说,就是对象的大小必须是 8 字节的整数倍。当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
实际计算
了解了这些信息以后我们就可以进行上面对象的计算了,我们是在 64bit 下并开启了指针压缩情况下进行计算。
首先计算 10000 长度的数组大小。
head | data | padding | total |
---|---|---|---|
8 + 4 + 4 | 4 * 10000 | 0 | 40016 |
再计算下 ArrayList 对象的大小
head | data | padding | total |
---|---|---|---|
8 + 4 + 0 | 4 + 4 + 4 | 0 | 24 |
那总的内存占用也就是 24 + 40016 = 40040。
简单工具
那么有没有简单的工具,我输入给他一个对象他就帮我计算出内存大小呢?有的,主要有两种方式一种是使用 Instrumentation,但是用起来还挺麻烦,有兴趣的同学可以自己研究下,这里主要介绍下 jol 的使用,jol 是OpenJDK 的一个工具包可以用来方便的计算对象或者类的内存占用大小,也提供 Maven GAV,可以自行去搜索引用。
那么下面使用下 jol 工具来验证下我们的计算结果。
执行如下代码:
List<String> list = Lists.newArrayListWithCapacity(10000);
System.out.println(ClassLayout.parseInstance(list).toPrintable());
Field field = ArrayList.class.getDeclaredField("elementData");
field.setAccessible(true);
Object array = field.get(list);
System.out.println(ClassLayout.parseInstance(array).toPrintable());
输出结果如下:
java.util.ArrayList object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 22 2f 00 f8 (00100010 00101111 00000000 11111000) (-134205662)
12 4 int AbstractList.modCount 0
16 4 int ArrayList.size 0
20 4 java.lang.Object[] ArrayList.elementData [null, null, .....]
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
[Ljava.lang.Object; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) f5 22 00 f8 (11110101 00100010 00000000 11111000) (-134208779)
12 4 (object header) 10 27 00 00 (00010000 00100111 00000000 00000000) (10000)
16 40000 java.lang.Object Object;.<elements> N/A
Instance size: 40016 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
跟我们之前的计算的结果一致,如果不开启指针压缩,-XX:-UseCompressedOops 那么结果如下:
java.util.ArrayList object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 50 a5 8e 21 (01010000 10100101 10001110 00100001) (562996560)
12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2)
16 4 int AbstractList.modCount 0
20 4 (alignment/padding gap)
24 4 int ArrayList.size 0
28 4 (alignment/padding gap)
32 8 java.lang.Object[] ArrayList.elementData [null, null, ......]
Instance size: 40 bytes
Space losses: 8 bytes internal + 0 bytes external = 8 bytes total
[Ljava.lang.Object; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) f0 d1 8b 21 (11110000 11010001 10001011 00100001) (562811376)
12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2)
16 4 (object header) 10 27 00 00 (00010000 00100111 00000000 00000000) (10000)
20 4 (alignment/padding gap)
24 80000 java.lang.Object Object;.<elements> N/A
Instance size: 80024 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
那么,你学会了如何计算一个对象的实际内存占用了吗?
网友评论