一、对象的内存布局
我们平时在开发中,评估内存使用的时候,很容易只算数据本身的占用内存,而忽略了对象头的内存,但是在很多情况下,对象头的内存甚至可能会超过数据本身,所以这块数据不可忽略。
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1.1 HotSpot虚拟机对象的对象头
-
第一类是用于存储对象自身的运行时数据,如哈希码,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别占用4个字节和8个字节(未开启压缩指针)
-
第二类是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。在开启指针压缩的状况下占 4 字节,未开启状况下占 8 字节。(64位操作)
-
如果是数组,那在对象头中还必须有一块用于记录数组长度的数据,占4个字节
1.2 对齐填充
对齐填充仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍)(书里是这样写的,但是实际测试发现并不是,也可能是12字节),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
那么为何非要进行 8 字节对齐呢?这样岂不是浪费了空间资源?
因为CPU访问内存还是一个较慢的操作,所以计算机并非逐个字节读取,而是以2、4、8的倍数字节块读取内存,它们会要求这些数据的首地址的值是某个数是k(通常是4或8)的倍数 ,这就是所谓的内存对齐。大白话就是,各种数据类型都要一定的规则进行排列,而不是一个接一个的排放,这就是对齐。对齐是在效率和空间上做的权衡。
如果不对齐,在读取某个对象的时候,就会需要多读不属于它的部分,如下图,如果没有做内存对齐,因为CPU只能读取4或8的倍数,在每次读取Object2的时候,都必须把Object1和Object3都读进来
二、在实际操作中看一个对象有多大
首先引入maven依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
2.1 Integer 占多大
import org.openjdk.jol.info.ClassLayout;
/**
* @Author: ZhaoLei
* @Create date: 2022/12/28
* @Description:
*/
public class TestInteger {
public static void main(String[] args) {
Integer i1 = 1;
ClassLayout classLayout_i1 = ClassLayout.parseInstance(i1);
System.out.println(classLayout_i1.toPrintable());
}
}
java.lang.Integer 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) bf 22 00 f8 (10111111 00100010 00000000 11111000) (-134208833)
12 4 int Integer.value 1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到Integer,刚好是12字节的对象头 + 4字节的实例数据,占用16字节
2.2 Short 占多大
我们都知道,基本数据类型short,在内存中是16位,也就是2字节存储,但是它被包装成Short后,会占用多大呢?
import org.openjdk.jol.info.ClassLayout;
/**
* @Author: ZhaoLei
* @Create date: 2022/12/28
* @Description:
*/
public class TestShort {
public static void main(String[] args) {
Short s1 = 1;
ClassLayout classLayout_s1 = ClassLayout.parseInstance(s1);
System.out.println(classLayout_s1.toPrintable());
}
}
java.lang.Short 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) 78 22 00 f8 (01111000 00100010 00000000 11111000) (-134208904)
12 2 short Short.value 1
14 2 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total
可以看到,short被包装后,占用了16个字节,其中有2个字节的对齐填充。
看了上面两个示例,我们发现一个包装类会比基本数据类型占的空间大的多,对象头 + 对齐填充比实际数据占的空间都要多。所以能用基本数据类型的情况下,就不用包装类!!!
2.3 String 占多大
import org.openjdk.jol.info.ClassLayout;
/**
* @Author: ZhaoLei
* @Create date: 2022/12/28
* @Description:
*/
public class TestString {
public static void main(String[] args) {
String str = "abc";
ClassLayout classLayout_str = ClassLayout.parseInstance(str);
System.out.println(classLayout_str.toPrintable());
}
}
java.lang.String 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) da 02 00 f8 (11011010 00000010 00000000 11111000) (-134216998)
12 4 char[] String.value [a, b, c]
16 4 int String.hash 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到String对象本身占了24个字节,但是String这里的实例数据是指向了一个char型数组,所以我们还需要加上char型数组所占的空间
import org.openjdk.jol.info.ClassLayout;
/**
* @Author: ZhaoLei
* @Create date: 2022/12/28
* @Description:
*/
public class TestCharArray {
public static void main(String[] args) {
char[] chars = new char[] {'a', 'b', 'c'};
ClassLayout classLayout_chars = ClassLayout.parseInstance(chars);
System.out.println(classLayout_chars.toPrintable());
}
}
[C 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) 41 00 00 f8 (01000001 00000000 00000000 11111000) (-134217663)
12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
16 6 char [C.<elements> N/A
22 2 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total
可以看到,char型数组占了24个字节,char数组的大小还要取决于元素个数
综上,String “abc” 会在内存中占用48个字节,这里是基于JDK 1.8测试的,String的底层是char型数组,一个char占2个字节,如果是JDK 11及以上,String的底层实现是byte数组了,一个byte占一个字节,存储可能会不同
2.4 HashMap占多大
HashMap是一个集合,我们关注集合对象本身这个对象占的空间,其实意义不大,要在其中填充进去数以后,才有意义
准备工作:我们先把50w个cuid加载到HashMap,然后用 JvisualVM 的堆dump进行分析,JvisualVM一般在$JAVA_HOME的bin目录中可以找到,如果找不到,就直接下载一个
可以看到,50w个cuid加载到HashMap后,其占空间的主要是3个结构:String对象、char[]数组以及HashMap的Node
在HashMap的底层是Node数组加LinkedHashMap或者红黑树,可以看到,我们大部分用到的都是Node,说明发生Hash冲突的概率比较低。