1.1.3. 偏向锁,轻量锁,重量锁
我们来编写测试代码:
A a = new A(); System.out.println("------After Initialization------\n" + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 synchronized (a) { System.out.println("------After Fetched Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); } System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); System.out.println("a.hashcode: " + a.hashCode()); System.out.println("------After call hashcode------\n" + ClassLayout.parseInstance(a).toPrintable()); //由于调用了 hashcode,这里直接升级成为轻量锁 synchronized (a) { System.out.println("------After Fetched Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); } System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); //测试重量级锁 Runnable r = () -> { synchronized (a) { System.out.println("------After " + Thread.currentThread() + " lock is fetched------\n" + ClassLayout.parseInstance(a).toPrintable()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread [] threads = new Thread[2]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(r); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable());
输出为(我们这里省略掉我们不关心的输出):
------After Initialization------ 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) ------After Fetched Lock------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 90 8a 5e (00000101 10010000 10001010 01011110) (1586139141) 4 4 (object header) c7 02 00 00 (11000111 00000010 00000000 00000000) (711) ------After Released Lock------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 90 8a 5e (00000101 10010000 10001010 01011110) (1586139141) 4 4 (object header) c7 02 00 00 (11000111 00000010 00000000 00000000) (711) a.hashcode: 929776179 ------After call hashcode------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 33 42 6b (00000001 00110011 01000010 01101011) (1799500545) 4 4 (object header) 37 00 00 00 (00110111 00000000 00000000 00000000) (55) ------After Fetched Lock------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 78 f2 0f 53 (01111000 11110010 00001111 01010011) (1393554040) 4 4 (object header) ee 00 00 00 (11101110 00000000 00000000 00000000) (238) ------After Released Lock------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 33 42 6b (00000001 00110011 01000010 01101011) (1799500545) 4 4 (object header) 37 00 00 00 (00110111 00000000 00000000 00000000) (55) ------After Thread[Thread-0,5,main] lock is fetched------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342) 4 4 (object header) 69 02 00 00 (01101001 00000010 00000000 00000000) (617) ------After Thread[Thread-1,5,main] lock is fetched------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342) 4 4 (object header) 69 02 00 00 (01101001 00000010 00000000 00000000) (617) ------After Released Lock------ OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342) 4 4 (object header) 69 02 00 00 (01101001 00000010 00000000 00000000) (617)
我们这里通过第一字节 8 位的末尾两位判断锁状态:
- 创建对象后,初始为无锁状态:第一字节为00000101,01 代表处于无锁状态。同时,偏向锁是开启状态,因为00000101,倒数第三位为1,这个根据前面的结构图可以知道是偏向锁标记。
- 第一次 main 线程获取锁,由于没有争抢,同时启动参数中也没有关闭偏向锁,采用偏向锁:第一字节为00000101,代表处于偏向锁状态,后面保存的是指向线程的指针。
- 第一次 main 线程释放锁,由于没有其他争抢,保持这个偏向锁状态(Hotspot 取消偏向锁需要全局 safepoint,关于这个相关的分析,可以参考:JVM相关 - SafePoint 与 Stop The World 全解(基于OpenJDK 11版本))。
- 调用 hashcode,根据之前的源码分析,需要取消偏向锁,同时将 hashcode 写入 header:第一字节为00000001,代表处于无锁状态,偏向锁关闭,倒数第三位为 0。
- 调用 hashcode 之后 main 线程获取锁,由于偏向锁关闭,直接从轻量锁开始:第一字节为01111000,00 代表轻量锁。后面保存了指向锁记录的指针。
- 调用 hashcode 之后 main 线程释放锁,释放轻量锁,锁记录会被回收,所以 hashcode 回到 header 保存。
- 多线程导致对象升级为重量级锁之后:第一字节为 00000010,10 代表重量级锁,由于 monitor 一旦生成一直存在,所以这个对象头会一直保留 monitor 的指针,hashcode 也会保存在 monitor 上。
- 最后锁释放后,header 没有改变,也是上面说的原因。
1.2. 类型字压缩指针与 JVM 最大内存
压缩指针这个属性默认是打开的,可以通过-XX:-UseCompressedOops
关闭。
首先说一下为何需要压缩指针呢?32 位的存储,可以描述多大的内存呢?假设每一个1代表1字节,那么可以描述 0~2^32-1 这 2^32 字节也就是 4 GB 的内存。
但是呢,Java 默认是 8 字节对齐的内存,也就是一个对象占用的空间,必须是 8 字节的整数倍,不足的话会填充到 8 字节的整数倍。也就是其实描述内存的时候,不用从 0 开始描述到 8(就是根本不需要定位到之间的1,2,3,4,5,6,7)因为对象起止肯定都是 8 的整数倍。所以,2^32 字节如果一个1代表8字节的话,那么最多可以描述 2^32 * 8 字节也就是 32 GB 的内存。
这就是压缩指针的原理。如果配置最大堆内存超过 32 GB(当 JVM 是 8 字节对齐),那么压缩指针会失效。 但是,这个 32 GB 是和字节对齐大小相关的,也就是-XX:ObjectAlignmentInBytes
配置的大小(默认为8字节,也就是 Java 默认是 8 字节对齐)。-XX:ObjectAlignmentInBytes
可以设置为 8 的整数倍,最大 128。也就是如果配置-XX:ObjectAlignmentInBytes
为 24,那么配置最大堆内存超过 96 GB 压缩指针才会失效。
编写程序测试下:
A a = new A(); System.out.println("------After Initialization------\n" + ClassLayout.parseInstance(a).toPrintable());
首先,以启动参数:-XX:ObjectAlignmentInBytes=8 -Xmx16g
执行:
------After Initialization------ com.hashjang.jdk.TestObjectAlign$A 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) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472) 12 4 (alignment/padding gap) 16 8 long A.d 0 Instance size: 24 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
可以看到类型字大小为 4 字节48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
,压缩指针生效。
首先,以启动参数:-XX:ObjectAlignmentInBytes=8 -Xmx32g
执行:
------After Initialization------ com.hashjang.jdk.TestObjectAlign$A 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) a0 5b c6 00 (10100000 01011011 11000110 00000000) (12999584) 12 4 (object header) b4 02 00 00 (10110100 00000010 00000000 00000000) (692) 16 8 long A.d 0 Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到类型字大小为 8 字节,压缩指针失效:
a0 5b c6 00 (10100000 01011011 11000110 00000000) (12999584) b4 02 00 00 (10110100 00000010 00000000 00000000) (692)
修改对齐大小为 16 字节,也就是以-XX:ObjectAlignmentInBytes=16 -Xmx32g
执行:
------After Initialization------ com.hashjang.jdk.TestObjectAlign$A 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) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472) 12 4 (alignment/padding gap) 16 8 long A.d 0 24 8 (loss due to the next object alignment) Instance size: 32 bytes Space losses: 4 bytes internal + 8 bytes external = 12 bytes total
可以看到类型字大小为 4 字节48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
,压缩指针生效。
总结
- 对象指针包括标记字与类型字。
- 标记字中保存了:对象默认哈希值(如果没有覆盖默认的 hashcode() 方法,则哈希值在 hashcode() 方法被调用之后,会被记录到标记字之中)、对象的形状(是否是数组)、锁状态(偏向锁等锁信息,值得一提的是偏向锁在 Java 15 中废弃:Disable and Deprecate Biased Locking)、数组长度(如果标记显示这个对象是数组,描述了数组的长度)。标记字的实现仅仅包含一个uintptr_t类型,所以,在 32 位和 64 位虚拟机上面,大小分别是 4 字节和 8 字节。可以参考源码: markWord.hpp
- 类型字中保存了:指向对象实现的 Class 的指针。类型字默认是被压缩的,压缩指针指的就是这里。
- 默认哈希值计算,对于偏向锁是否生效,是有影响的。就是默认哈希值与偏向锁不能共存。
- 默认哈希值有缓存:无锁缓存在标记字;轻量锁缓存在锁记录,标记字中有指针指向锁记录,轻量锁释放后,锁记录中的哈希值复制到标记字中;重量锁缓存在 monitor 对象,标记字中有指针指向 monitor 对象,释放后,哈希值依然缓存在 monitor 对象中。
- 默认哈希值计算,需要考虑异步 monitor 降级的情况,这是 Java 15 中的新特性:Async Monitor Deflation
- 分代年龄在每次 Young GC 复制之后 +1,最大是
-XX:MaxTenuringThreshold=n
配置的值,大于这个值就进入老年代了。 - 压缩指针是否启用和 Java 对齐字节大小(
-XX:ObjectAlignmentInBytes
,默认是 8,也就是 8 字节对齐)还有最大堆栈大小相关。