Java 对象模型
我们先了解一下,一个JAVA对象的存储结构。在Hotspot虚拟机中,对象在内存中的存储布局分为 3 块区域:对象头( header )、实例数据(Instance Data)和对齐填充( padding )。
java 对象的大小默认是按照 8 字节对齐 ,也就是说 Java 对象的大小必须是 8 字节的倍数。若是算到最后不够 8 字节的话,那么就会进行对齐填充。
那么为何非要进行 8 字节对齐呢?这样岂不是浪费了空间资源?
其实不然,由于 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好也是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行的情况,这叫做 缓存行污染。
当然,也不是所有的指针都会压缩,一些特殊类型的指针 JVM 不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
对象头(Header)
对象头,又包括三部分:Mark Word、(Klass Word)元数据指针、数组长度。
MarkWord:用于存储对象运行时的数据,比如 HashCode 、锁状态标志、GC分代年龄等。这部分在64位操作系统下,占8字节(64bit),在32位操作系统下,占4字节(32bit)。
Mark Word布局:
Klass Word:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
这部分就涉及到一个指针压缩的概念,在开启指针压缩的情况下,占4字节(32bit),未开启情况下,占8字节(64bit),现在JVM在1.6之后,在64位操作系统下都是默认开启的。
数组长度:这部分只有是数组对象才有(int[]),如果是非数组对象,就没这部分了,这部分占4字节(32bit)。
内存对齐
想要知道为什么虚拟机要进行对齐填充,我们需要了解什么是内存对齐?在开发人员眼中,我们看到的内存是这样的:
上图表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存。相反, CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。如下图:
假设一个32位平台的 CPU,那它就会以4字节为粒度去读取内存块。那为什么需要内存对齐呢?主要有两个原因:
平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况。
性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作。
下面用图例来说明 CPU 访问非内存对齐的过程:
在上图中,假设CPU 是一次读取4字节,在这个连续的8字节的内存空间中,如果我的数据没有对齐,存储的内存块在地址1,2,3,4中,那CPU的读取就会需要进行两次读取,另外还有额外的计算操作:
1、CPU 首次读取未对齐地址的第一个内存块,读取 0-3 字节。并移除不需要的字节 0。
2、CPU 再次读取未对齐地址的第二个内存块,读取 4-7 字节。并移除不需要的字节 5、6、7 字节。
3、合并 1-4 字节的数据。
4、合并后放入 寄存器 。
所以,没有进行内存对齐就会导致CPU进行额外的读取操作,并且需要额外的计算。如果做了内存对齐,CPU可以直接从地址0开始读取,一次就读取到想要的数据,不需要进行额外读取操作和运算操作,节省了运行时间。我们用了空间换时间,这就是为什么我们需要内存对齐。
回到Java空对象填充了4个字节的问题,因为原字节头是12字节,64位机器下,内存对齐的话就是128位,也就是16字节,所以我们还需要填充4个字节。(64位机器一次读取8字节,因为64位下填充为8字节的整数倍,这里12字节,显然填充到16字节效果最佳。)
内存消耗演示
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>.9</version>
</dependency>
空对象(16 Byte )
public static class Dog {
}
public static void main(String[] args) {
Dog dog =new Dog();
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
明明是12为啥变成16了, 这个就是使用了内存对齐了, 8的倍数
基本数据类型(1~8byte)
public static class Dog {
int a;
long a;
double a;
char a3;
Float a4;
Boolean a5;
short a6;
byte a;
}
public static void main(String[] args) {
Dog dog =new Dog();
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
引用地址(4字节)
public static class Dog {
Date a;
String a;
Dog a;
}
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());
}
任何非基本类型的变量都存储的是对象的引用地址,而在java中地址是使用4字节存储的
包装类型和 String (16~24byte)
public static void main(String[] args) {
Integer a=1;
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Long a=12313123L;
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Double a=1.1;
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Character a='a';
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Float a=1.1F;
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Boolean a=true;
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Short a=1;
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Byte a=1;
System.out.println(ClassLayout.parseInstance(a).toPrintable());
String a="xxxx";
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) 67 22 00 f8 (01100111 00100010 00000000 11111000) (-134208921)
4 int Integer.value 1
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 0 bytes total
java.lang .Long object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) ae 22 00 f8 (10101110 00100010 00000000 11111000) (-134208850)
4 (alignment/padding gap)
8 long Long.value 12313123
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 4 bytes total
java.lang.Double object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) 92 21 00 f8 (10010010 00100001 00000000 11111000) (-134209134)
4 (alignment/padding gap)
8 double Double.value 1.1
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 4 bytes total
java.lang.Character object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) c6 20 00 f8 (11000110 00100000 00000000 11111000) (-134209338)
2 char Character.value a
2 (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 2 bytes external = 2 bytes total
java.lang.Float object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) 4b 21 00 f8 (01001011 00100001 00000000 11111000) (-134209205)
4 float Float.value 1.1
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 0 bytes total
java.lang.Boolean object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) 85 20 00 f8 (10000101 00100000 00000000 11111000) (-134209403)
1 boolean Boolean.value true
3 (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 3 bytes external = 3 bytes total
java.lang.Short object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) 20 22 00 f8 (00100000 00100010 00000000 11111000) (-134208992)
2 short Short.value 1
2 (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 2 bytes external = 2 bytes total
java.lang.Byte object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) d9 21 00 f8 (11011001 00100001 00000000 11111000) (-134209063)
1 byte Byte.value 1
3 (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 3 bytes external = 3 bytes total
java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
4 (object header) da 02 00 f8 (11011010 00000010 00000000 11111000) (-134216998)
4 char[] String.value [x, x, x, x]
4 int String.hash 0
4 (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code
Byte,Short,Boolean,Float,Character,Integer= 16字节
String,Double,Long=24字节
基本类型数组(16byte)
public static void main(String[] args) {
int[] a=new int[0];
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
基本类型的数组会比空对象多一个4字节,用于存储长度, 数组长度每加1那么大小就加一个类型字节
public static void main(String[] args) {
int[] a=new int[10];
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
容器(List Map Set)(16~48byte)
public static void main(String[] args) {
List<Integer> a=new ArrayList<>();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Set<Integer> a=new HashSet<>();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Map<String,String> a=new HashMap<>();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
List集合(24字节): 会比对象多2个intl类型用于计算长度和个数 ,内部属于存储在Object[]
Set集合(16字节): 内部数据存储在HashMap的key中
map集合(48字节): List集合大一倍 每一个项,key(24) ,value(24),因为多一个hash值
各种数组和容器的计算规则
基本类型计算
就是类型对应的字节大小进行相加就行
基本类型数组
数组本身16+(长度*类型字节)=最终占用内存大小
int[] a=new int[];
16+(10*4)=56(字节)
对象数组
16+(长度*4字节引用地址)+(长度*每个对象内部的大小)=最终占用内存大小
Object[] o=new Object[];
16+(10*4)+(10*16)=816(字节)
和对象数组的计算方式一样
List = 24+(长度*4字节引用地址)+(长度*每个对象内部的大小)=最终占用内存大小
List<Object> list = new ArrayList<>();
for (int i =; i < 10; i++) {
list.add(new Object());
}
24+(10 4)+(10 16)=824(字节)
计算对象
public static class Dog {
int a; //
Boolean b; //
String c; //
List<String> d; //
Map<String, String> e;//
}
12+4+4+4+4+4=36 (因为内存对齐的原因空对象才会是16)
然后我们给对象赋值
Dog dog = new Dog();
dog.a=;
dog.b= true;
dog.c= "xxxxx";
dog.d= Arrays.asList("xxxxx","xxxxx");
dog.e= new HashMap(){
{
put("a","a");put("b","b");put("c","c");}};
因为基本数据类型本身就是他的大小,在上面我们已经加进去了,我们只需要算引用类型对象的大小就行了
36+16+16+(24+2 16)+(48+2 (24+24))=268字节
然后我们将对象放入到容器中
public static void main(String[] args) {
ArrayList<Object> objects = new ArrayList<>();
for (int i =; i < 10; i++) {
Dog dog = new Dog();
dog.a=;
dog.b= true;
dog.c= "xxxxx";
dog.d= Arrays.asList("xxxxx","xxxxx");
dog.e= new HashMap(){
{
put("a","a");put("b","b");put("c","c");}};
objects.add(dog);
}
}
24+(4*10) + (10*268)=2.744(kb)
假设有100万条数据那么
24+(4*1000000) +(1000000*268)=272000024(字节)=272(mb)
学会计算java的内存会极大地降低内存溢出的风险,以及最大化利用内存来达到最好的效率 ,建议学一下 Java-垃圾收回机制 ,因为只会计算对象大小也不行的,还需要结合JVM虚拟机的GC机制和实际需求进行计算,假设你堆大小2G你在某一时间创建了2G的对象,那么会溢出吗? 可能会,也可能不会, 主要看这些对象在gc的时候能否被收回,那么如何知道这些对象满足了收回的条件,就要研究GC机制了…书山有路勤为径,学海无涯苦作舟