diff --git a/_posts/2024-10-30-sync.md b/_posts/2024-10-30-sync.md new file mode 100644 index 0000000..058ac43 --- /dev/null +++ b/_posts/2024-10-30-sync.md @@ -0,0 +1,400 @@ +--- +layout: post +title: "从 markword 到 synchronized 深入解析" +date: 2024-10-30 11:00:00 +0200 +tags: [GC] +--- + +在 Java 中,synchronized 关键字是实现线程同步的基础工具,用于确保多个线程在访问共享资源时能够按照预期的顺序执行,避免数据不一致的问题。synchronized 可以作用于方法或代码块,实现锁的机制,保证在同一时刻只有一个线程能进入同步区域。它在并发编程中尤为重要,因为通过锁的获取与释放来控制代码执行的互斥性,有助于保证多线程程序的安全性和正确性。本文将深入解析 Java synchronized 关键字的实现细节,通过源码揭示 JVM 内部如何管理和优化同步操作的机制。 + +## synchronized + +在 JVM 中,synchronized 的底层实现涉及锁的多个状态转换,包括偏向锁、轻量级锁和重量级锁,以优化不同并发场景下的性能。这些锁的状态转换由 JVM 自动管理,根据线程竞争的激烈程度动态调整。 + +下面的图来自 《深入理解Java虚拟机》。 + + + +[JEP 374: Deprecate and Disable Biased Locking](https://bugs.openjdk.org/browse/JDK-8235256) 已经对偏向锁进行废弃,不在本文讨论的范围内。 + +废弃原因是偏向锁由于维护成本巨大,而目前从中获取的性能提升有限。 + +> Biased locking introduced a lot of complex code into the synchronization subsystem and is invasive to other HotSpot components as well. This complexity is a barrier to understanding various parts of the code and an impediment to making significant design changes within the synchronization subsystem. To that end we would like to disable, deprecate, and eventually remove support for biased locking. + +[jol](https://github.com/openjdk/jol) 是一款观察对象内存布局的工具,使用 jol 可以看到对象的 markword 。 + +## markword + +笔者电脑是 64 位的,下面是 markword 在 64 位机器上的布局,与 32 位区别不大。 + +```java +Mark Word (normal): + 64 39 8 3 0 + [.......................HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH.AAAA.TT] + (Unused) (Hash Code) (GC Age)(Tag) +``` + +对象创建后获取 markword 的值,见[源码](/assets//synchronized//JOLSample_01_Basic.java) + +```java +public class JOLSample_01_Basic { + + public static void main(String[] args) { + A a = new A(); + out.println(ClassLayout.parseInstance(a).toPrintable()); + + long markword = VM.current().getLong(a,0); + printMarkWordBin(markword); + printMarkBit(markword); + printbiasBit(markword); + printAgeBit(markword); + } + public static class A { + boolean f; + } +} +``` + +结果如下: + +1. markword bit 为 `0b00000101` +2. mark bit 为 `0b01` 表示未锁定 +3. bias bit 为 `0b1` 表示可偏向 +4. age bit 为 `0b0000` 表示 GC 年龄为 0 + +```java +jol.JOLSample_01_Basic$A object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) + 8 4 (object header: class) 0x00066a50 + 12 1 boolean A.f false + 13 3 (object alignment gap) +Instance size: 16 bytes +Space losses: 0 bytes internal + 3 bytes external = 3 bytes total + +markword bit: 00000101 +mark bit: 00000001 +bias bit: 00000001 +age bit: 00000000 +``` + +上面的代码是使用的是 OpenJDK 11 运行的,可以看到偏向的标志位与前文图中不符合。其原因在于 JDK 版本,下面是 OpenJDK 8 运行的结果,可以看到 bias bit 位 `0x0`。 + +```java +com.example.demo.JOLSample_01_Basic$A object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x00060a20 + 12 1 boolean A.f false + 13 3 (object alignment gap) +Instance size: 16 bytes +Space losses: 0 bytes internal + 3 bytes external = 3 bytes total + +markword bit: 00000001 +mark bit: 00000001 +bias bit: 00000000 +age bit: 00000000 +``` + +### hashcode + +当对象的 `hashcode` 方法被调用时,hash 值会被存入 markword 中,见[源码](/assets//synchronized/JOLSample_01_Hashcode.java)。 + +```java +public static void main(String[] args) { + A a = new A(); + + int hashcode = a.hashCode(); + out.println(ClassLayout.parseInstance(a).toPrintable()); + out.println("a.hashCode() : " + toBinaryWithSpaces(hashcode)); + long markword = VM.current().getLong(a,0); + printHashcodeBit(markword);//(markword >>> 8) + printMarkWordBin(markword); +} +``` + +下面使用 hashCode 方法获取的 hash 值与程序从 markword 中获取的一致。 + +```java +jol.JOLSample_01_Hashcode$A object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000005594a1b501 (hash: 0x5594a1b5; age: 0) + 8 4 (object header: class) 0x00066a50 + 12 1 boolean A.f false + 13 3 (object alignment gap) +Instance size: 16 bytes +Space losses: 0 bytes internal + 3 bytes external = 3 bytes total + +a.hashCode() : 01010101 10010100 10100001 10110101 +hash code bit: 01010101 10010100 10100001 10110101 +markword bit: 01010101 10010100 10100001 10110101 00000001 +``` + +#### 源码 + + +下面试 hashCode 源码,`ObjectSynchronizer::FastHashCode` 负责实际获取 hashCode,方法逻辑很多这里不赘述。 + +```cpp +JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle)) + // as implemented in the classic virtual machine; return 0 if object is null + return handle == nullptr ? 0 : + checked_cast(ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle))); +JVM_EN + +// Register native methods of Object +void java_lang_Object::register_natives(TRAPS) { + InstanceKlass* obj = vmClasses::Object_klass(); + Method::register_native(obj, vmSymbols::hashCode_name(), vmSymbols::void_int_signature(), + (address) &JVM_IHashCode, CHECK); +} + +intptr_t ObjectSynchronizer::FastHashCode(Thread* current, oop obj) { + //omit + hash = mark.hash(); + if (hash != 0) return hash; // if it has a hash, just return it + hash = get_next_hash(current, obj); // get a new hash + temp = mark.copy_set_hash(hash); // merge the hash into header + // try to install the hash + test = obj->cas_set_mark(temp, mark); + if (test == mark) return hash; // if the hash was installed, return it +} +``` + +### 对象年龄 + +同样从 markwor 中可以获取对象年龄,[源码](/assets/synchronized/JOLSample_01_Age.java) + +```java +public static void main(String[] args) { + A a = new A(); + for (int i = 0; i < 500; i++) { + var bytes = new byte[1024 * 1024]; + } + out.println(ClassLayout.parseInstance(a).toPrintable()); + long markword = VM.current().getLong(a, 0); + printAgeBit(markword); +} +``` + +```java +jol.JOLSample_01_Hashcode$A object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x000000000000001d (biasable; age: 3) + 8 4 (object header: class) 0x00066a50 + 12 1 boolean A.f false + 13 3 (object alignment gap) +Instance size: 16 bytes +Space losses: 0 bytes internal + 3 bytes external = 3 bytes total + +age bit: 00000011 +``` + +当垃圾回收时,对象被复制,年龄就会增加。 + +```cpp +if (dest_attr.is_young()) { + if (age < markWord::max_age) { + obj->incr_age(); + } +} + +void oopDesc::incr_age() { + markWord m = mark(); + if (m.has_displaced_mark_helper()) { //与 synchronized 有关 + m.set_displaced_mark_helper(m.displaced_mark_helper().incr_age()); + } else { + set_mark(m.incr_age()); + } +} +``` + +## 栈帧 + +看过一些资料的读者应该都知道 `synchronized` 轻量级锁需要借助于 Java 栈来实现。首先是将锁对象的 markword 拷贝到栈中,称之为 Lock Record 的区域,Lock Record 也会有指向对象,然后利用 CAS 操作将对象的 markword 指向 Lock Record。 + +下面的图来自 《深入理解Java虚拟机》。 + + + +在 JVM 线程的栈帧中,有一个叫做 `monitors` 的区域,这个区域存储的就是 Lock Record。`monitors` 是一个数组,大小根据运行时方法的情况进行分配。 + +```cpp +//src/hotspot/cpu/aarch64/frame_aarch64.hpp + +// Layout of asm interpreter frame: +// [expression stack ] * <- sp +// [monitors[0] ] \ +// ... | monitor block size = k +// [monitors[k-1] ] / +// [frame initial esp ] ( == &monitors[0], initially here) initial_sp_offset +// [byte code index/pointr] = bcx() bcx_offset +// [pointer to locals ] = locals() locals_offset +// [constant pool cache ] = cache() cache_offset +// [klass of method ] = mirror() mirror_offset +// [extended SP ] extended_sp offset +// [methodData ] = mdp() mdx_offset +// [Method ] = method() method_offset +// [last esp ] = last_sp() last_sp_offset +// [sender's SP ] (sender_sp) sender_sp_offset +// [old frame pointer ] <- fp = link() +// [return pc ] +// [last sp ] +// [oop temp ] (only for native calls) +// [padding ] (to preserve machine SP alignment) +// [locals and parameters ] +// <- sender sp +// ------------------------------ Asm interpreter ---------------------------------------- +``` + +Lock Record 在 JVM 中使用 `BasicObjectLock` 表示,`_lock` 存储对象 markword,`_obj` 指向锁对象。 + +```cpp +class BasicObjectLock { + BasicLock _lock;// the lock, must be double word aligned + oop _obj; // object holds the lock; +} +``` + +## 再谈 GC + +当 Java 进行 GC 的时候,会从 GC root 开始扫描存活的对象,其中一项内容就是就是扫描线程的栈帧。BasicObjectLock 既然会指向对象,那也必然会被扫描。 + +```cpp +void frame::oops_interpreted_do(OopClosure* f, const RegisterMap* map, bool query_oop_map_cache) const { + for ( + BasicObjectLock* current = interpreter_frame_monitor_end(); + current < interpreter_frame_monitor_begin(); + current = next_monitor_in_interpreter_frame(current) + ) { + current->oops_do(f); + } +} + +class BasicObjectLock { + // GC support + void oops_do(OopClosure* f) { f->do_oop(&_obj); } +} +``` + +`BasicObjectLock` 在栈帧中数目与 `synchronized` 一定具有某种联系。下面修改 JVM 源码打印日志来确定。 + +### 实验 + +首先修改 JVM 中 `oops_interpreted_do` 的代码,增加 count 计数器和日志输出,仅仅输出我们关心的方法 `testCountMonitorInMethod` 。 + +```cpp +void frame::oops_interpreted_do(OopClosure* f, const RegisterMap* map, bool query_oop_map_cache) const { + Thread *thread = Thread::current(); + methodHandle m (thread, interpreter_frame_method()); + int count = 0; + for ( + BasicObjectLock* current = interpreter_frame_monitor_end(); + current < interpreter_frame_monitor_begin(); + current = next_monitor_in_interpreter_frame(current) + ) { + count++; + current->oops_do(f); + } + const char* method_name = m()->name()->as_C_string(); + if (strcmp(method_name, "testCountMonitorInMethod") == 0) { + tty->print_cr("Current method name: %s, basicObjectLock: %d", method_name, count); + } +} +``` + +下面是 Java 代码,[见源码](/assets/synchronized/SyncTest.java)。 + +```java +public class SyncTest { + private Object lock = new Object(); + private Object lock1 = new Object(); + + public static void main(String[] args) { + new Thread(()->{ new SyncTest().testCountMonitorInMethod();}).start(); + smallObj(); + } + + public void testCountMonitorInMethod() { + synchronized (lock) { + synchronized (lock1) { + sleep(); + } + } + } +} +``` + +首先确认 Java 版本是我们自己编译的版本,[JDK 如何编译](https://yoa1226.github.io/2024/08/01/region-pinning-for-g1.html#%E7%BC%96%E8%AF%91-jdk-21): + + + +下面是运行结果,结果显示栈帧中 BasicObjectLock 的数目位 2。 + + + +反复试验,结果如下表所示。 + + + +注意,这里的数目是指在 GC 过程中,正在使用的 BasicObjectLock 数目,并不是栈帧初始化时分配的 BasicObjectLock 数目。 + +可以看到实际使用的 BasicObjectLock 数目和当前持有锁对象的数目一致。 + +至于 BasicObjectLock 在栈帧中的容量,笔者猜测,与方法运行时持有的锁对象最多情况下一致,也就是最多时有多少 synchronized 代码块嵌套。 + + +## 解释器 + +在 HotSpot 中,有两种解释器,模板解释器和 C++ 解释器,关于解释的讨论见 [Interpreter模块](https://book.douban.com/annotation/31407691/)。 HotSpot 默认使用的是模板解释器是汇编代码写的,较为晦涩,阅读难度较大。 + +[Interpreter模块](https://book.douban.com/annotation/31407691/) 文中说可以使用通过配置使用 C++ 解释器,笔者没有找到相关的配置,网上相关说法在最新 OpenJDK 中都无法生效。 + +后文笔者以 C++ 解释器,即 bytecodeInterpreter 介绍 synchronized 实现。 + +## 轻量级锁 + +C++ 解释器 解析器的入口在 `BytecodeInterpreter::run`,synchronized 关键字最终会生成 `monitorenter` 和 `monitorexit` 字节码指令。 + +`CASE(_monitorenter):` 对 `monitorenter` 指令进行解释执行。 + +```cpp +void BytecodeInterpreter::run(interpreterState istate) { + //omit + CASE(_monitorenter): { + oop lockee = STACK_OBJECT(-1); + BasicObjectLock* limit = istate->monitor_base(); + BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base(); + BasicObjectLock* entry = nullptr; + while (most_recent != limit ) { + if (most_recent->obj() == nullptr) entry = most_recent; + else if (most_recent->obj() == lockee) break; + most_recent++; + } + //omit + } + //omit +} +``` + +对 monitors 进行遍历,找到符合条件或者的 `BasicObjectLock`,遍历从低地址一直到高地址。 + +```cpp +intptr_t* _stack_base; // base of expression stack +// Layout of asm interpreter frame: +// [expression stack ] * <- sp +// [monitors[0] ] \ +// ... | monitor block size = k +// [monitors[k-1] ] / +// [frame initial esp ] ( == &monitors[0], initially here) initial_sp_offset +``` + +//todo + +## 重量级锁 + +//todo + +## 总结 + +//todo diff --git a/assets/synchronized/JOLSample_01_Age.java b/assets/synchronized/JOLSample_01_Age.java new file mode 100644 index 0000000..f12b011 --- /dev/null +++ b/assets/synchronized/JOLSample_01_Age.java @@ -0,0 +1,68 @@ +package jol; + +import org.openjdk.jol.info.ClassLayout; +import org.openjdk.jol.vm.VM; + +import static java.lang.System.out; + +/** + * @author Aleksey Shipilev + */ +public class JOLSample_01_Age { + + public static void main(String[] args) { + A a = new A(); + for (int i = 0; i < 500; i++) { + var bytes = new byte[1024 * 1024]; + } + out.println(ClassLayout.parseInstance(a).toPrintable()); + long markword = VM.current().getLong(a, 0); + printAgeBit(markword); + } + + public static class A { + boolean f; + } + + public static void printMarkWordBin(long markword) { + out.printf("markword bit: %s\n", toBinaryWithSpaces(markword)); + } + + public static void printMarkBit(long markword) { + out.printf("mark bit: %s\n", toBinaryWithSpaces(markword & 0b11)); + } + + public static void printbiasBit(long markword) { + out.printf("bias bit: %s\n", toBinaryWithSpaces(markword >> 2 & 0b1)); + } + + public static void printAgeBit(long markword) { + out.printf("age bit: %s\n", toBinaryWithSpaces(markword >> 3 & 0b1111)); + } + + public static void printHashcodeBit(long markword) { + out.printf("hash code bit: %s\n", toBinaryWithSpaces((markword >>> 8))); + } + + + public static String toHexWithSpaces(long number) { + String hex = Long.toHexString(number); + // 如果长度是奇数,在前面补零 + if (hex.length() % 2 != 0) { + hex = "0" + hex; + } + // 每两位分隔开 + return hex.replaceAll("(?<=..)(..)", " $1").toUpperCase(); + } + + public static String toBinaryWithSpaces(long number) { + String binary = Long.toBinaryString(number); + // 如果长度不是8的倍数,前面补零 + while (binary.length() % 8 != 0) { + binary = "0" + binary; + } + // 每8位分隔开 + return binary.replaceAll("(.{8})", "$1 "); + } + +} \ No newline at end of file diff --git a/assets/synchronized/JOLSample_01_Basic.java b/assets/synchronized/JOLSample_01_Basic.java new file mode 100644 index 0000000..1a21544 --- /dev/null +++ b/assets/synchronized/JOLSample_01_Basic.java @@ -0,0 +1,70 @@ +package jol; + +import org.openjdk.jol.info.ClassLayout; +import org.openjdk.jol.vm.VM; + +import static java.lang.System.out; + +/** + * @author Aleksey Shipilev + */ +public class JOLSample_01_Basic { + + public static void main(String[] args) { + A a = new A(); + out.println(ClassLayout.parseInstance(a).toPrintable()); + + long markword = VM.current().getLong(a,0); + printMarkWordBin(markword); + printMarkBit(markword); + printbiasBit(markword); + printAgeBit(markword); + } + + public static class A { + boolean f; + } + + public static void printMarkWordBin(long markword) { + out.printf("markword bit: %s\n", toBinaryWithSpaces(markword)); + } + + public static void printMarkBit(long markword) { + out.printf("mark bit: %s\n", toBinaryWithSpaces(markword & 0b11)); + } + + public static void printbiasBit(long markword) { + out.printf("bias bit: %s\n", toBinaryWithSpaces(markword>>2 & 0b1)); + } + + public static void printAgeBit(long markword) { + out.printf("age bit: %s\n", toBinaryWithSpaces(markword >> 3 & 0b1111)); + } + + public static void printHashcodeBit(long markword) { + out.printf("hash code bit: %s\n", toBinaryWithSpaces((markword >>> 8))); + } + + + + public static String toHexWithSpaces(long number) { + String hex = Long.toHexString(number); + // 如果长度是奇数,在前面补零 + if (hex.length() % 2 != 0) { + hex = "0" + hex; + } + // 每两位分隔开 + return hex.replaceAll("(?<=..)(..)", " $1").toUpperCase(); + } + + public static String toBinaryWithSpaces(long number) { + String binary = Long.toBinaryString(number); + // 如果长度不是8的倍数,前面补零 + while (binary.length() % 8 != 0) { + binary = "0" + binary; + } + // 每8位分隔开 + return binary.replaceAll("(.{8})", "$1 "); + } + +} \ No newline at end of file diff --git a/assets/synchronized/JOLSample_01_Hashcode.java b/assets/synchronized/JOLSample_01_Hashcode.java new file mode 100644 index 0000000..39c6031 --- /dev/null +++ b/assets/synchronized/JOLSample_01_Hashcode.java @@ -0,0 +1,70 @@ +package jol; + +import org.openjdk.jol.info.ClassLayout; +import org.openjdk.jol.vm.VM; + +import static java.lang.System.out; + +/** + * @author Aleksey Shipilev + */ +public class JOLSample_01_Hashcode { + + public static void main(String[] args) { + A a = new A(); + + int hashcode = a.hashCode(); + out.println(ClassLayout.parseInstance(a).toPrintable()); + out.println("a.hashCode() : " + toBinaryWithSpaces(hashcode)); + long markword = VM.current().getLong(a,0); + printHashcodeBit(markword);//(markword >>> 8) + printMarkWordBin(markword); + } + + public static class A { + boolean f; + } + + public static void printMarkWordBin(long markword) { + out.printf("markword bit: %s\n", toBinaryWithSpaces(markword)); + } + + public static void printMarkBit(long markword) { + out.printf("mark bit: %s\n", toBinaryWithSpaces(markword & 0b11)); + } + + public static void printbiasBit(long markword) { + out.printf("bias bit: %s\n", toBinaryWithSpaces(markword>>2 & 0b1)); + } + + public static void printAgeBit(long markword) { + out.printf("age bit: %s\n", toBinaryWithSpaces(markword >> 3 & 0b1111)); + } + + public static void printHashcodeBit(long markword) { + out.printf("hash code bit: %s\n", toBinaryWithSpaces((markword >>> 8))); + } + + + + public static String toHexWithSpaces(long number) { + String hex = Long.toHexString(number); + // 如果长度是奇数,在前面补零 + if (hex.length() % 2 != 0) { + hex = "0" + hex; + } + // 每两位分隔开 + return hex.replaceAll("(?<=..)(..)", " $1").toUpperCase(); + } + + public static String toBinaryWithSpaces(long number) { + String binary = Long.toBinaryString(number); + // 如果长度不是8的倍数,前面补零 + while (binary.length() % 8 != 0) { + binary = "0" + binary; + } + // 每8位分隔开 + return binary.replaceAll("(.{8})", "$1 "); + } + +} \ No newline at end of file diff --git a/assets/synchronized/SyncTest.java b/assets/synchronized/SyncTest.java new file mode 100644 index 0000000..d105868 --- /dev/null +++ b/assets/synchronized/SyncTest.java @@ -0,0 +1,36 @@ +import java.util.ArrayList; + +public class SyncTest { + private Object lock = new Object(); + private Object lock1 = new Object(); + + public static void main(String[] args) { + new Thread(()->{ + new SyncTest().testCountMonitorInMethod(); + }).start(); + smallObj(); + } + + public void testCountMonitorInMethod() { + synchronized (lock) { + synchronized (lock1) { + sleep(); + } + } + } + + public static void smallObj() { + ArrayList list = new ArrayList(); + for (int i = 0; i < 100000; i++) { + list.add(new byte[1024*1024/2]); + } + } + + public void sleep(){ + try { + Thread.sleep(1000_1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/assets/synchronized/count-monitor-when-gc.png b/assets/synchronized/count-monitor-when-gc.png new file mode 100644 index 0000000..da2db6c Binary files /dev/null and b/assets/synchronized/count-monitor-when-gc.png differ diff --git a/assets/synchronized/count-monitor.png b/assets/synchronized/count-monitor.png new file mode 100644 index 0000000..40bc5da Binary files /dev/null and b/assets/synchronized/count-monitor.png differ diff --git a/assets/synchronized/java-version.png b/assets/synchronized/java-version.png new file mode 100644 index 0000000..d267415 Binary files /dev/null and b/assets/synchronized/java-version.png differ diff --git a/assets/synchronized/lightweight-lock-record.png b/assets/synchronized/lightweight-lock-record.png new file mode 100644 index 0000000..060a670 Binary files /dev/null and b/assets/synchronized/lightweight-lock-record.png differ diff --git a/assets/synchronized/lock-level.png b/assets/synchronized/lock-level.png new file mode 100644 index 0000000..c70c628 Binary files /dev/null and b/assets/synchronized/lock-level.png differ