对象的结构
在JVM中,一般来说,Java对象都是分配在堆中,那么对象在堆中长什么样呢?
对象头包含以下几个部分:
- MarkWord:包含对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode、分代年龄等。
- Class Pointer:一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例,默认开启压缩指针,占32位,关闭压缩,占64位。
- 数组的长度:可选的,只有当对象是一个数组对象时才会有这个部分。
- 对象的属性:占用内存空间取决于对象的属性数量和类型。
- 对齐:为了保证对象头的字节数是8的倍数。
MarkWord
MarkWord用来存储线程的锁状态、hashcode、分代年龄等信息,当对象处于不同状态时,MarkWord中的数据也跟随着对象的状态而变化。
以上是Java对象处于5种不同状态时,Mark Word中64个bit的表现形式,上面每一行代表对象处于某种状态时的样子。
注意以下几点:
- 偏向锁标志位+锁标志位共同表示当前对象锁的状态。
- 分代年龄:在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC(除了CMS垃圾收集器)的年龄阈值为15,并发GC(CMS垃圾收集器)的年龄阈值为6。由于分代年龄只有4位,所以最大值为15,这就是
-XX:MaxTenuringThreshold
参数最大值为15的原因。 - hashCode:31位的对象标识hashCode,采用延迟计算方式,只有在使用时才会计算,并会将结果写到该对象头中。如果对象在加锁前计算了hashcode,此时状态为无锁不可偏向,因为hashcode占用了偏向锁标志位的数据区域,这里的hashcode是指系统中最初始的hashcode,也就是调用基类Object.hashCode()方法产生的,而不是重写Object.hashCode()方法产生的hashcode,也可以使用System.identityHashCode(object)方法来获取。
项目中可以引入JOL(Java Object Layout)来打印java对象头在内存中的字节码。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
下面通过代码对上面的5中状态进行验证:
无锁不可偏向
JVM刚启动时4s(默认值为4s,可以通过JVM参数-XX:BiasedLockingStartupDelay
修改)之内创建的对象的锁状态为无锁不可偏向。
@Test
public void testNoLockByBeforeFiveSecond() {
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
运行结果如下:
java.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) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
当一个对象计算了hashCode,锁的状态就会变成无锁不可偏向,因为hashcode占用了偏向锁标志位的数据区域。
@Test
public void testNoLockByHashCode() throws InterruptedException {
TimeUnit.SECONDS.sleep(5); // 参数是4s,这里用休眠5s
Object object = new Object();
System.out.println(Integer.toHexString(object.hashCode()));
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
运行结果如下:
5e8c92f4
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 f4 92 8c (00000001 11110100 10010010 10001100) (-1936526335)
4 4 (object header) 5e 00 00 00 (01011110 00000000 00000000 00000000) (94)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以发现对象的hashcode为5e8c92f4,与对象头中的hashcode一致。
匿名偏向锁
JVM启动4s后创建的对象锁的状态为匿名偏向锁。
public void testAnonymousBiasLock() throws InterruptedException {
TimeUnit.SECONDS.sleep(5); // 或者设置JVM args:-XX:BiasedLockingStartupDelay=0
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
运行结果如下:
java.lang.Object 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) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁
当没有发生资源的竞争,锁会偏向第一个持有锁的线程。
@Test
public void testBiasLock() throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Object object = new Object();
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
运行结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 98 4e 00 (00000101 10011000 01001110 00000000) (5150725)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
注意此时对象头中会存有一个线程ID,但是此ID不是java中thread对象getId()方法返回的id,也不是操作系统中ID,这个ID是JVM生成的,可以通过jstack命令查看线程的ID。
$ jstack 16344
....
"main" #1 prio=5 os_prio=0 tid=0x0000000002239800 nid=0x4c18 in Object.wait() [0x000000000280e000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b605d30> (a java.lang.Thread)
at java.lang.Thread.join(Thread.java:1252)
- locked <0x000000076b605d30> (a java.lang.Thread)
at java.lang.Thread.join(Thread.java:1326)
at com.morris.concurrent.syn.upgrade.SynchronizedStatus.testBiasLock(SynchronizedStatus.java:74)
....
tid对应对象头中的线程id,而nid对应操作系统级别的线程id。
轻量级锁
当对象头中的锁偏向于线程T1,T1释放锁后,其他线程来获取,此时偏向锁会升级为轻量级锁(线程之间交替执行,没有资源竞争)。
/**
* 偏向锁 -> 轻量级锁
*/
@Test
public void testLightLock() throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Object object = new Object();
Thread t = new Thread(() -> {
synchronized (object) {
}
});
t.start();
t.join();
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
运行结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) f0 e0 7c 02 (11110000 11100000 01111100 00000010) (41738480)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.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) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
当JVM刚启动或者使用JVM参数-XX:-UseBiasedLocking=false
禁用了偏向锁,这时无锁不可偏向锁就会升级为轻量级锁。
@Test
public void testLightLock2() throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
运行结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 88 e5 7a 02 (10001000 11100101 01111010 00000010) (41608584)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
注意轻量级锁释放后,对象头中锁的状态会变为无锁不可偏向。
重量级锁
当产生了资源竞争,轻量级锁会升级为重量级锁。
@Test
public void testHeavyLock() throws InterruptedException {
Object object = new Object();
Thread t = new Thread(() -> {
synchronized (object) {
}
});
t.start();
synchronized (object) {
TimeUnit.SECONDS.sleep(1);
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
运行结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) fa c1 ec 1b (11111010 11000001 11101100 00011011) (468500986)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Class Pointer
JVM通过这个指针确定对象是哪个类的实例,该指针的长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops
开启指针压缩,默认开启,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
- 每个Class的属性指针(即静态变量)
- 每个对象的属性指针(即对象变量)
- 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如JDK8中指向元空间的Class对象指针、本地变量、堆栈元素、入参、返回值和NULL指针等。
问题:Object object = new Object()
中object对象到底占用多少个字节?
答案:16个字节。
开启压缩指针:markword(64bit) + classpointer(32bit) + 对齐(32bit) = 16byte 。
不开启压缩指针:markword(64bit) + classpointer(64bit) = 16byte 。