最近在抽时间阅读 JDK 的源码,主要是 GC 还有 Safepoint 相关的源码,发现很多我在之前拜读网上各种 JVM 原理大作时候由于没有看源码导致我对于底层原理的误解。果然,一百个人读水浒传,就有一百种水浒传。还是需要更加深入的了解下源码,才能更好地理解 JVM,进行调优。这个系列,将在讲述 Java GC 各种原理的基础上,结合对应的源码分析,并附上源码地址。因为JVM的源码更新还是很快的,尤其是 GC 这一块,但是基本原理,应该大体不会变,附上源码地址,旨在让各位读者掌握这些原理最新实现情况。本文在撰写的时候,会紧跟最新版本的 JDK。
GC(Garbage Collection) 是目前很多编程语言自带的特性,例如Java,Python;GC是一个很好的特性,能让使用这个语言编程的程序员不去关心内存回收,并且降低内存泄漏和内存溢出发生的概率。
了解Java GC,需要先知道 Java 最基础的对象在内存中究竟是如何存储的。我们专注于 HotSpot 虚拟机实现,来详细阐述对象存储结构。首先我们来了解一个概念,对象指针(OOPs,Ordinary Object Pointers), 也就是对象头的主要部分。
1. 对象指针(OOPs,Ordinary Object Pointers)
对象指针的实现,可以参考oop.hpp:
1class oopDesc { 2 private: 3 volatile markWord _mark; 4 union _metadata { 5 Klass* _klass; 6 narrowKlass _compressed_klass; 7 } _metadata; 8}
从源代码可以简单看出,对象指针包括:
1.标记字(Mark Word): 一组标记,描述了对象的状态,包括对象默认哈希值(如果没有覆盖默认的 hashcode() 方法,则哈希值在 hashcode() 方法被调用之后,会被记录到标记字之中)、对象的形状(是否是数组)、锁状态(偏向锁等锁信息,值得一提的是偏向锁在 Java 15 中废弃:Disable and Deprecate Biased Locking)、数组长度(如果标记显示这个对象是数组,描述了数组的长度)。标记字的实现仅仅包含一个uintptr_t类型,所以,在 32 位和 64 位虚拟机上面,大小分别是 4 字节和 8 字节。可以参考源码: markWord.hpp。我们这里只讨论 64 位的 JVM,也就是标记字为 8 字节的情况。
1class markWord { 2 private: 3 uintptr_t _value; 4}
2.类型字(Class Word): 类型字是指向对象实现的 Class 的指针。Java 7 之前指向的区域位于持久带(Permanent Generation),Java 8 之后,持久带废弃,引入了元数据区的概念(Metaspace),所以Java 8 之后指向的是这个元数据区。
这个指针可能是被压缩的,就是压缩指针(Compressed OOPs)。当开启对象压缩时占用4字节(JVM默认开启),关闭时占用8字节
1.1. 标记字的具体结构
对于 64 位的虚拟机环境,标记字大小是 8 字节。先给出标记字结构:
(上图来自于:https://www.cnblogs.com/helloworldcode/p/11914053.html)
我们先来通过 jol (Java Object Layout) 工具,以及接下来的几个实例,来逐步看一下每种状态下的标记字结构。加入依赖:
1<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> 2<dependency> 3 <groupId>org.openjdk.jol</groupId> 4 <artifactId>jol-core</artifactId> 5 <version>0.13</version> 6</dependency>
1.1.1. 哈希值计算几次?去哪里找哈希值?
如果类没有覆盖hashcode(),那么实现hashcode()的是一个native方法。
1@HotSpotIntrinsicCandidate 2public native int hashCode();
具体的实现参考源码synchronizer.cpp:
1intptr_t ObjectSynchronizer::FastHashCode(Thread* self, oop obj) { 2 //如果启用了偏向锁特性 3 if (UseBiasedLocking) { 4 //如果正处于偏向锁 5 if (obj->mark().has_bias_pattern()) { 6 //取消偏向锁,不会再处于偏向锁状态,并且在下次获取锁的时候,直接从轻量锁开始 7 Handle hobj(self, obj); 8 if (SafepointSynchronize::is_at_safepoint()) { 9 BiasedLocking::revoke_at_safepoint(hobj); 10 } else { 11 BiasedLocking::revoke(hobj, self); 12 } 13 obj = hobj(); 14 assert(!obj->mark().has_bias_pattern(), "biases should be revoked by now"); 15 } 16 } 17 18 while (true) { 19 ObjectMonitor* monitor = NULL; 20 markWord temp, test; 21 intptr_t hash; 22 markWord mark = read_stable_mark(obj); 23 24 // 不能处于偏向锁状态 25 assert(!mark.has_bias_pattern(), "invariant"); 26 // 未处于任何锁状态的一般状态 27 if (mark.is_neutral()) { 28 hash = mark.hash(); 29 if (hash != 0) { // 如果已经计算过哈希值,直接返回 30 return hash; 31 } 32 hash = get_next_hash(self, obj); // 否则,计算一个新的哈希值,这个方法的详细分析在另一篇文章 33 temp = mark.copy_set_hash(hash); // 设置哈希值 34 test = obj->cas_set_mark(temp, mark); //更新为设置成功 35 if (test == mark) { // 如果更新成功,则返回 36 return hash; 37 } 38 //如果设置失败,可能发生了锁膨胀,或者是多线程同时调用了hashcode方法,循环重新尝试 39 40 } else if (mark.has_monitor()) { 41 //如果处于重量级锁状态 42 //获取对应的monitor对象 43 monitor = mark.monitor(); 44 //获取其header 45 temp = monitor->header(); 46 assert(temp.is_neutral(), "invariant: header=" INTPTR_FORMAT, temp.value()); 47 //查看header中的hashcode 48 hash = temp.hash(); 49 //若monitor中有哈希值,并且没有发生异步monitor降级(Async Monitor Deflation),就使用这个哈希值 50 //异步monitor降级(Async Monitor Deflation),请参考:https://wiki.openjdk.java.net/display/HotSpot/Async+Monitor+Deflation,这是在 Java 15 之后开始引入的新特性 51 if (hash != 0) { 52 // It has a hash. 53 54 // Separate load of dmw/header above from the loads in 55 // is_being_async_deflated(). 56 if (support_IRIW_for_not_multiple_copy_atomic_cpu) { 57 // A non-multiple copy atomic (nMCA) machine needs a bigger 58 // hammer to separate the load above and the loads below. 59 OrderAccess::fence(); 60 } else { 61 OrderAccess::loadload(); 62 } 63 if (monitor->is_being_async_deflated()) { 64 // But we can't safely use the hash if we detect that async 65 // deflation has occurred. So we attempt to restore the 66 // header/dmw to the object's header so that we only retry 67 // once if the deflater thread happens to be slow. 68 monitor->install_displaced_markword_in_object(obj); 69 continue; 70 } 71 return hash; 72 } 73 74 } else if (self->is_lock_owned((address)mark.locker())) { 75 //如果处于轻量锁状态,读取指向锁记录的指针,获取其中的哈希值 76 temp = mark.displaced_mark_helper(); 77 assert(temp.is_neutral(), "invariant: header=" INTPTR_FORMAT, temp.value()); 78 hash = temp.hash(); 79 if (hash != 0) { // if it has a hash, just return it 80 return hash; 81 } 82 } 83 //不处于以上状态或者处于以上锁状态之前,没有计算过哈希值,则直接膨胀锁到重量级锁 84 monitor = inflate(self, obj, inflate_cause_hash_code); 85 //查看是否已经有其他线程计算过哈希值 86 mark = monitor->header(); 87 assert(mark.is_neutral(), "invariant: header=" INTPTR_FORMAT, mark.value()); 88 hash = mark.hash(); 89 //如果没有,则在这里计算,并设置 90 if (hash == 0) { // if it does not have a hash 91 hash = get_next_hash(self, obj); // get a new hash 92 temp = mark.copy_set_hash(hash); // merge the hash into header 93 assert(temp.is_neutral(), "invariant: header=" INTPTR_FORMAT, temp.value()); 94 uintptr_t v = Atomic::cmpxchg((volatile uintptr_t*)monitor->header_addr(), mark.value(), temp.value()); 95 test = markWord(v); 96 //设置失败,则有其他线程已经设置,尝试读取 97 if (test != mark) { 98 // The attempt to update the ObjectMonitor's header/dmw field 99 // did not work. This can happen if another thread managed to 100 // merge in the hash just before our cmpxchg(). 101 // If we add any new usages of the header/dmw field, this code 102 // will need to be updated. 103 hash = test.hash(); 104 assert(test.is_neutral(), "invariant: header=" INTPTR_FORMAT, test.value()); 105 assert(hash != 0, "should only have lost the race to a thread that set a non-zero hash"); 106 } 107 //如果有发生异步monitor降级(Async Monitor Deflation),跳过,重新判断 108 //异步monitor降级(Async Monitor Deflation),请参考:https://wiki.openjdk.java.net/display/HotSpot/Async+Monitor+Deflation,这是在 Java 15 之后开始引入的新特性 109 if (monitor->is_being_async_deflated()) { 110 // If we detect that async deflation has occurred, then we 111 // attempt to restore the header/dmw to the object's header 112 // so that we only retry once if the deflater thread happens 113 // to be slow. 114 monitor->install_displaced_markword_in_object(obj); 115 continue; 116 } 117 } 118 // We finally get the hash. 119 return hash; 120 } 121} 122
可以看出,hashcode:
- 尽量只计算一次,计算出后,对于无锁对象,保存在对象标记字 Markword 中。
- 处于各种锁状态的话(除了偏向锁),都会修改并占用 Markword 导致需要其他地方缓存计算好的 hashcode,对于重量锁是对应的 Monitor 中保存,对于轻量锁是所指向的锁记录的指针中保存。
- 对于偏向锁,由于没法哈希值,所以只要计算过哈希值,就不会再进入偏向锁的状态,而是直接从轻量锁开始。
- 对于 JDK 15 之后引入的异步 Monitor 降级(Async Monitor Deflation),需要在这个过程完成或者未开始的时候读取 monitor对象的 hashcode 缓存。对于这个特性的详细说明,可以参考:Async Monitor Deflation
哈希值的具体计算,根据全局变量hashcode的值决定计算方式(通过-XX:hashCode=设定)。默认的哈希值计算,是hashcode=5 的情况,不同情况的计算方式的详细说明,如果感兴趣,可以参考:JDK核心JAVA源码解析(9) - hashcode 方法
我们来通过一段代码以及输出来看看这些特性
1public static void main(String[] args) throws Exception { 2 A a = new A(); 3 B b = new B(); 4 System.out.println("------After Initialization------\n" + ClassLayout.parseInstance(a).toPrintable() + "\n" + ClassLayout.parseInstance(b).toPrintable()); 5 6 System.out.println("a.hashcode: " + a.hashCode()); 7 System.out.println("b.hashcode: " + b.hashCode()); 8 System.out.println("------After call hashcode------\n" + ClassLayout.parseInstance(a).toPrintable() + "\n" + ClassLayout.parseInstance(b).toPrintable()); 9} 10 11//A没有覆盖默认的 hashcode 方法 12public static class A { 13 long d; 14} 15//B覆盖了 hashcode 方法 16public static class B { 17 long d; 18 19 @Override 20 public int hashCode() { 21 return (int) 5555; 22 } 23}
输出:
1------After Initialization------ 2com.hashjang.jdk.TestObjectAlign$A object internals: 3 OFFSET SIZE TYPE DESCRIPTION VALUE 4 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 5 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 6 8 4 (object header) 49 ce 00 20 (01001001 11001110 00000000 00100000) (536923721) 7 12 4 (alignment/padding gap) 8 16 8 long A.d 0 9Instance size: 24 bytes 10Space losses: 4 bytes internal + 0 bytes external = 4 bytes total 11 12com.hashjang.jdk.TestObjectAlign$B object internals: 13 OFFSET SIZE TYPE DESCRIPTION VALUE 14 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 15 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 8 4 (object header) 8a ce 00 20 (10001010 11001110 00000000 00100000) (536923786) 17 12 4 (alignment/padding gap) 18 16 8 long B.d 0 19Instance size: 24 bytes 20Space losses: 4 bytes internal + 0 bytes external = 4 bytes total 21 22a.hashcode: 2124046270 23b.hashcode: 5555 24------After call hashcode------ 25com.hashjang.jdk.TestObjectAlign$A object internals: 26 OFFSET SIZE TYPE DESCRIPTION VALUE 27 0 4 (object header) 01 be 5f 9a (00000001 10111110 01011111 10011010) (-1705001471) 28 4 4 (object header) 7e 00 00 00 (01111110 00000000 00000000 00000000) (126) 29 8 4 (object header) 49 ce 00 20 (01001001 11001110 00000000 00100000) (536923721) 30 12 4 (alignment/padding gap) 31 16 8 long A.d 0 32Instance size: 24 bytes 33Space losses: 4 bytes internal + 0 bytes external = 4 bytes total 34 35com.hashjang.jdk.TestObjectAlign$B object internals: 36 OFFSET SIZE TYPE DESCRIPTION VALUE 37 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 38 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 39 8 4 (object header) 8a ce 00 20 (10001010 11001110 00000000 00100000) (536923786) 40 12 4 (alignment/padding gap) 41 16 8 long B.d 0 42Instance size: 24 bytes 43Space losses: 4 bytes internal + 0 bytes external = 4 bytes total 44 45
可以看出,对于未覆盖hashcode()方法的 A 类,调用hashcode()之后,哈希值存在了对象头。哈希值为 2124046270,转换为二进制为:1111110 10011010 01011111 10111110。由于 Windows 系统中为小端存储,所以打印出来的 Header 是反过来的,可以看到这个哈希值就存储在 Header 中:
00000001 10111110 01011111 10011010 01111110 00000000 00000000 00000000
其他特性,将在接下来的 1.1.3. 偏向锁,轻量锁,重量锁 这一小节详细说明。
1.1.2. 分代年龄
对象头中的分代年龄,用于分代 GC。分代 GC 我们在后面的章节会详细讲述,这里只是看一些特性。
记录分代年龄一共 4 bit,所以最大为 2^4 - 1 = 15。所以配置最大分代年龄-XX:MaxTenuringThreshold=n这个n不能大于15,当然也不能小于 0.等于 0 的话,就直接入老年代。等于 16 的话,就是从不进入老年代,这样不符合JVM规范,所以不能大于15(感谢 CSDN @JonsonJiao 指正)。默认是 15。
在发生 Young GC,更准确地说是在 Survivor 区复制的时候,存活的对象的分代年龄会加1。我们编写程序测试下,由于 编译器会优化代码,同时调用System.gc()并不是立刻触发 GC,并且是 Full GC,可能会使对象直接进入老年代,分代年龄不再增长,所以我们可以使用 volatile 属性辅助我们真正创建对象,避免编译器优化:
1static volatile Object consumer; 2public static void main(String[] args) throws Exception { 3 //这是我们要观察的对象 4 Object instance = new Object(); 5 long lastAddr = VM.current().addressOf(instance); 6 for (int i = 0; i < 10000; i++) { 7 //查看地址是否发生了变化,代表是否发生了 Survivor 复制,或者是移动到老年代 8 long currentAddr = VM.current().addressOf(instance); 9 if (currentAddr != lastAddr) { 10 //地址发生变化的时候,打印对象结构 11 ClassLayout layout = ClassLayout.parseInstance(instance); 12 System.out.println(layout.toPrintable()); 13 lastAddr = currentAddr; 14 } 15 for (int j = 0; j < 10000; j++) { 16 //一直创建新对象 17 //因为是volatile的属性更新,不会被编译器优化 18 consumer = new Object(); 19 } 20 } 21}
可以配合 GC 日志一起观察,关于 JVM 日志配置可以参考这篇文章:OpenJDK 11 JVM日志相关参数解析与使用
首先我们用这个参数运行程序-Xmx128m -Xlog:gc=info,输出:
1[0.016s][info][gc] Using G1 2# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf 3[2.540s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 24M->1M(128M) 2.600ms 4java.lang.Object object internals: 5 OFFSET SIZE TYPE DESCRIPTION VALUE 6 0 4 (object header) 0d 00 00 00 (00001101 00000000 00000000 00000000) (13) 7 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 9 12 4 (loss due to the next object alignment) 10Instance size: 16 bytes 11Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 12 13[2.627s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.273ms 14java.lang.Object object internals: 15 OFFSET SIZE TYPE DESCRIPTION VALUE 16 0 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21) 17 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 18 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 19 12 4 (loss due to the next object alignment) 20Instance size: 16 bytes 21Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 22 23[2.675s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.063ms 24java.lang.Object object internals: 25 OFFSET SIZE TYPE DESCRIPTION VALUE 26 0 4 (object header) 1d 00 00 00 (00011101 00000000 00000000 00000000) (29) 27 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 28 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 29 12 4 (loss due to the next object alignment) 30Instance size: 16 bytes 31Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 32 33[2.724s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.068ms 34java.lang.Object object internals: 35 OFFSET SIZE TYPE DESCRIPTION VALUE 36 0 4 (object header) 25 00 00 00 (00100101 00000000 00000000 00000000) (37) 37 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 38 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 39 12 4 (loss due to the next object alignment) 40Instance size: 16 bytes 41Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 42 43[2.772s][info][gc] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.212ms 44java.lang.Object object internals: 45 OFFSET SIZE TYPE DESCRIPTION VALUE 46 0 4 (object header) 2d 00 00 00 (00101101 00000000 00000000 00000000) (45) 47 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 48 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 49 12 4 (loss due to the next object alignment) 50Instance size: 16 bytes 51Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 52 53[2.821s][info][gc] GC(5) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.202ms 54java.lang.Object object internals: 55 OFFSET SIZE TYPE DESCRIPTION VALUE 56 0 4 (object header) 35 00 00 00 (00110101 00000000 00000000 00000000) (53) 57 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 58 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 59 12 4 (loss due to the next object alignment) 60Instance size: 16 bytes 61Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 62 63[2.869s][info][gc] GC(6) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.143ms 64java.lang.Object object internals: 65 OFFSET SIZE TYPE DESCRIPTION VALUE 66 0 4 (object header) 3d 00 00 00 (00111101 00000000 00000000 00000000) (61) 67 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 68 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 69 12 4 (loss due to the next object alignment) 70Instance size: 16 bytes 71Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 72 73[2.917s][info][gc] GC(7) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.313ms 74java.lang.Object object internals: 75 OFFSET SIZE TYPE DESCRIPTION VALUE 76 0 4 (object header) 45 00 00 00 (01000101 00000000 00000000 00000000) (69) 77 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 78 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 79 12 4 (loss due to the next object alignment) 80Instance size: 16 bytes 81Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 82 83[2.969s][info][gc] GC(8) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.473ms 84java.lang.Object object internals: 85 OFFSET SIZE TYPE DESCRIPTION VALUE 86 0 4 (object header) 4d 00 00 00 (01001101 00000000 00000000 00000000) (77) 87 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 88 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 89 12 4 (loss due to the next object alignment) 90Instance size: 16 bytes 91Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 92 93[3.021s][info][gc] GC(9) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.283ms 94java.lang.Object object internals: 95 OFFSET SIZE TYPE DESCRIPTION VALUE 96 0 4 (object header) 55 00 00 00 (01010101 00000000 00000000 00000000) (85) 97 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 98 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 99 12 4 (loss due to the next object alignment) 100Instance size: 16 bytes 101Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 102 103[3.072s][info][gc] GC(10) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.648ms 104java.lang.Object object internals: 105 OFFSET SIZE TYPE DESCRIPTION VALUE 106 0 4 (object header) 5d 00 00 00 (01011101 00000000 00000000 00000000) (93) 107 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 108 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 109 12 4 (loss due to the next object alignment) 110Instance size: 16 bytes 111Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 112 113[3.122s][info][gc] GC(11) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.585ms 114java.lang.Object object internals: 115 OFFSET SIZE TYPE DESCRIPTION VALUE 116 0 4 (object header) 65 00 00 00 (01100101 00000000 00000000 00000000) (101) 117 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 118 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 119 12 4 (loss due to the next object alignment) 120Instance size: 16 bytes 121Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 122 123[3.173s][info][gc] GC(12) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.130ms 124java.lang.Object object internals: 125 OFFSET SIZE TYPE DESCRIPTION VALUE 126 0 4 (object header) 6d 00 00 00 (01101101 00000000 00000000 00000000) (109) 127 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 128 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 129 12 4 (loss due to the next object alignment) 130Instance size: 16 bytes 131Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 132 133[3.224s][info][gc] GC(13) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.078ms 134java.lang.Object object internals: 135 OFFSET SIZE TYPE DESCRIPTION VALUE 136 0 4 (object header) 75 00 00 00 (01110101 00000000 00000000 00000000) (117) 137 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 138 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 139 12 4 (loss due to the next object alignment) 140Instance size: 16 bytes 141Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 142 143[3.273s][info][gc] GC(14) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.135ms 144java.lang.Object object internals: 145 OFFSET SIZE TYPE DESCRIPTION VALUE 146 0 4 (object header) 7d 00 00 00 (01111101 00000000 00000000 00000000) (125) 147 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 148 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 149 12 4 (loss due to the next object alignment) 150Instance size: 16 bytes 151Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 152 153[3.322s][info][gc] GC(15) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.467ms 154java.lang.Object object internals: 155 OFFSET SIZE TYPE DESCRIPTION VALUE 156 0 4 (object header) 7d 00 00 00 (01111101 00000000 00000000 00000000) (125) 157 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 158 8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424) 159 12 4 (loss due to the next object alignment) 160Instance size: 16 bytes 161Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 162 163[3.404s][info][gc] GC(16) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.556ms 164[3.485s][info][gc] GC(17) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.303ms 165[3.566s][info][gc] GC(18) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.288ms 166[3.647s][info][gc] GC(19) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.317ms 167[3.727s][info][gc] GC(20) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.286ms
可以看到,在第 15 次 GC 的时候,对象进入了老年代,内存地址不再随着 Young GC 的进行而变化。
各位读者可以将-XX:MaxTenuringThreshold=设置为 0 和 16 看看效果。
1.1.3. 偏向锁,轻量锁,重量锁
我们来编写测试代码:
1A a = new A(); 2System.out.println("------After Initialization------\n" + ClassLayout.parseInstance(a).toPrintable()); 3//偏向锁 4synchronized (a) { 5 System.out.println("------After Fetched Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); 6} 7System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); 8 9System.out.println("a.hashcode: " + a.hashCode()); 10System.out.println("------After call hashcode------\n" + ClassLayout.parseInstance(a).toPrintable()); 11//由于调用了 hashcode,这里直接升级成为轻量锁 12synchronized (a) { 13 System.out.println("------After Fetched Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); 14} 15System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); 16//测试重量级锁 17Runnable r = () -> { 18 synchronized (a) { 19 System.out.println("------After " + Thread.currentThread() + " lock is fetched------\n" + ClassLayout.parseInstance(a).toPrintable()); 20 try { 21 TimeUnit.SECONDS.sleep(1); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 } 26}; 27Thread [] threads = new Thread[2]; 28for (int i = 0; i < threads.length; i++) { 29 threads[i] = new Thread(r); 30 threads[i].start(); 31} 32for (int i = 0; i < threads.length; i++) { 33 threads[i].join(); 34} 35 36System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable()); 37
输出为(我们这里省略掉我们不关心的输出):
1------After Initialization------ 2 OFFSET SIZE TYPE DESCRIPTION VALUE 3 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 5 6------After Fetched Lock------ 7 OFFSET SIZE TYPE DESCRIPTION VALUE 8 0 4 (object header) 05 90 8a 5e (00000101 10010000 10001010 01011110) (1586139141) 9 4 4 (object header) c7 02 00 00 (11000111 00000010 00000000 00000000) (711) 10 11------After Released Lock------ 12 OFFSET SIZE TYPE DESCRIPTION VALUE 13 0 4 (object header) 05 90 8a 5e (00000101 10010000 10001010 01011110) (1586139141) 14 4 4 (object header) c7 02 00 00 (11000111 00000010 00000000 00000000) (711) 15 16a.hashcode: 929776179 17------After call hashcode------ 18 OFFSET SIZE TYPE DESCRIPTION VALUE 19 0 4 (object header) 01 33 42 6b (00000001 00110011 01000010 01101011) (1799500545) 20 4 4 (object header) 37 00 00 00 (00110111 00000000 00000000 00000000) (55) 21 22------After Fetched Lock------ 23 OFFSET SIZE TYPE DESCRIPTION VALUE 24 0 4 (object header) 78 f2 0f 53 (01111000 11110010 00001111 01010011) (1393554040) 25 4 4 (object header) ee 00 00 00 (11101110 00000000 00000000 00000000) (238) 26 27------After Released Lock------ 28 OFFSET SIZE TYPE DESCRIPTION VALUE 29 0 4 (object header) 01 33 42 6b (00000001 00110011 01000010 01101011) (1799500545) 30 4 4 (object header) 37 00 00 00 (00110111 00000000 00000000 00000000) (55) 31 32------After Thread[Thread-0,5,main] lock is fetched------ 33 OFFSET SIZE TYPE DESCRIPTION VALUE 34 0 4 (object header) 02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342) 35 4 4 (object header) 69 02 00 00 (01101001 00000010 00000000 00000000) (617) 36 37------After Thread[Thread-1,5,main] lock is fetched------ 38 OFFSET SIZE TYPE DESCRIPTION VALUE 39 0 4 (object header) 02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342) 40 4 4 (object header) 69 02 00 00 (01101001 00000010 00000000 00000000) (617) 41 42------After Released Lock------ 43 OFFSET SIZE TYPE DESCRIPTION VALUE 44 0 4 (object header) 02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342) 45 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 压缩指针才会失效。
编写程序测试下:
1A a = new A(); 2System.out.println("------After Initialization------\n" + ClassLayout.parseInstance(a).toPrintable());
首先,以启动参数:-XX:ObjectAlignmentInBytes=8 -Xmx16g执行:
1------After Initialization------ 2com.hashjang.jdk.TestObjectAlign$A object internals: 3 OFFSET SIZE TYPE DESCRIPTION VALUE 4 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 5 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 6 8 4 (object header) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472) 7 12 4 (alignment/padding gap) 8 16 8 long A.d 0 9Instance size: 24 bytes 10Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
可以看到类型字大小为 4 字节48 72 06 00 (01001000 01110010 00000110 00000000) (422472),压缩指针生效。
首先,以启动参数:-XX:ObjectAlignmentInBytes=8 -Xmx32g执行:
1------After Initialization------ 2com.hashjang.jdk.TestObjectAlign$A object internals: 3 OFFSET SIZE TYPE DESCRIPTION VALUE 4 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 5 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 6 8 4 (object header) a0 5b c6 00 (10100000 01011011 11000110 00000000) (12999584) 7 12 4 (object header) b4 02 00 00 (10110100 00000010 00000000 00000000) (692) 8 16 8 long A.d 0 9Instance size: 24 bytes 10Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到类型字大小为 8 字节,压缩指针失效:
1a0 5b c6 00 (10100000 01011011 11000110 00000000) (12999584) 2b4 02 00 00 (10110100 00000010 00000000 00000000) (692)
修改对齐大小为 16 字节,也就是以-XX:ObjectAlignmentInBytes=16 -Xmx32g执行:
1------After Initialization------ 2com.hashjang.jdk.TestObjectAlign$A object internals: 3 OFFSET SIZE TYPE DESCRIPTION VALUE 4 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 5 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 6 8 4 (object header) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472) 7 12 4 (alignment/padding gap) 8 16 8 long A.d 0 9 24 8 (loss due to the next object alignment) 10Instance size: 32 bytes 11Space 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 字节对齐)还有最大堆栈大小相关。