目录
一、Java中对象内存内部结构
在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充,如下。
- 普通对象
- 数组对象
1.1、对象头
对象头包括两个部分,对象标记和类型指针,对象头类似http
请求的请求头,存放一个对象的关键信息。
1.1.1、对象标记(Mark Word)
对象标记主要存放锁信息和GC标记,Java的synchronized
锁不是一加锁就是重量锁,有一个锁的升级过程(无锁态->偏向锁->轻量锁->重量锁),锁信息中存放的就是这个过程所需要的信息,而GC标记就是GC分代信息,用于标记对象的年龄和存活状态,以便进行垃圾回收。
1.1.2、类型指针(Klass Point)
类型指针比较好理解,是表示指向该对象类元数据的地址,JDK1.8的类元信息存储在元空间中,那么这个类型指针指向的就是元空间中类信息的地址。
1.1.3、数组长度(Length)
如果是数组对象,在对象头还会存储一个数组长度值。
1.2、实例数据
实例数据中存放一个对象中的其它信息,比如一个User
类,这个类中有一个Long
类型的id
字段,那么这个类实例化对象后实例数据就是这个id
字段,简单的可以理解实例数据就是一个类中声明的成员变量。
1.3、对其填充
Java中的对象要求是8个字节的整数倍,因为在Java虚拟机中,对象的内存布局是按照一定的规则进行排列的,这种排列方式叫做对象对齐(Object Alignment)。
对象对齐的目的是为了让对象的成员变量在内存中的地址能够被自然地对齐,从而提高CPU访问内存的效率,当 对象头 + 实例数据 占用字节不等于8的整数倍时会进行对其填充,比如对象头 + 实例数据 占用字节为12,那么会对其填充 4个字节。
二、指针压缩
同一个对象,不开启指针压缩,类型指针(Klass Point)占用8字节,开启指针压缩占用4字节,如果不开启指针压缩,会占用更多内存,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,所以为了减少64位平台下内存的消耗和避免GC压力过大,JDK1.6以后默认启用指针压缩功能。
类型 | 开启指针压缩 | 关闭指针压缩 |
---|---|---|
Object | 16 | 16 |
int数组 | 16 | 24 |
int | 4 | 4 |
Integer | 4 | 8 |
long | 8 | 8 |
Long | 4 | 8 |
String | 4 | 8 |
- JDK1.6 update14开始,在64位操作系统中,JVM支持指针压缩,并且是默认开启的。
- 启用指针压缩 -XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops
三、代码分析对象结构和占用内存大小
我这里使用的是64位系统,JDK1.8,要查看对象结构和占用内存大小还需要引入jol-core
包。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
2.1、分析Object对象
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
-
默认开启指针压缩
这里可以看到一个Java的Object空对象需要占用16个字节,其中Mark Word 和 Klass Point 合计占用12个字节,因为不满足8的倍数这里会进行对齐填充4个字节,也就是说一个最小的对象要占用16个字节。 -
关闭指针压缩
# 加上关闭指针压缩启动参数 -XX:-UseCompressedOops
关闭指针压缩后 Klass Point 占用8个字节,Mark Word 和 Klass Point 合计占用16个字节满足8的倍数不需要对齐填充。
2.2、自定义对象分析
- 自定义User类
public class User {
private Long id;
private Integer age;
private String nickName;
}
- 打印
User
对象内存结构
public static void main(String[] args) {
User user = new User();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
- 开启指针压缩
在开启指针压缩的情况下实例数据合计占用24个字节,基本类型和包装类型占用字节数是有区别的,可以。
- 关闭指针压缩
关闭指针压缩后实例数据合计占用36个字节,并且还有对齐填充的4个字节,占用内存比开启指针压缩多了16个字节。
四、JVM堆内存最好不要超过32G
JDK1.6 update14开始,在64位操作系统中,JVM堆内存小于32G的时候会默认开启一个内存对象指针压缩技术。在java中,所有的对象都分配在堆上,然后有一个指针引用它。指向这些对象的指针大小通常是CPU的字长的大小,不是32bit就是64bit,这取决于你的处理器,指针指向了你的值的精确位置。
对于32位系统,你的内存最大可使用4G。对于64系统可以使用更大的内存。但是64位的指针意味着更大的浪费,因为你的指针本身大了。浪费内存不算,更糟糕的是,更大的指针在主内存和缓存器(例如LLC, L1等)之间移动数据的时候,会占用更多的带宽。
Java 使用一个叫内存指针压缩的技术来解决这个问题。它的指针不再表示对象在内存中的精确位置,而是表示偏移量。这意味着32位的指针可以引用40亿个对象,而不是40亿个字节。最终,也就是说堆内存长到32G的物理内存,也可以用32bit的指针表示。
一旦越过那个神奇的30-32G的边界,指针就会切回普通对象的指针,每个对象的指针都变长了,就会使用更多的CPU内存带宽,也就是说你实际上失去了更多的内存。事实上当内存到达40-50GB的时候,有效内存才相当于使用内存对象指针压缩技术时候的32G内存。
即便有足够的内存,也尽量不要超过32G,因为它浪费了内存,降低了CPU的性能,还会让GC应对大内存。