文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
阿里面试:说说 jvm 锁的膨胀过程?锁内存怎么变化的?
尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,很多小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试机会,遇到很多很重要的面试题:
1.请解释 JVM 偏向锁、轻量级锁、自旋锁、重量级锁什么?
2.请介绍一下什么是sychronized的自旋锁、偏向锁、轻量级锁、重量级锁?
3.请介绍一下 jvm 内置锁 的膨胀过程?
4.请介绍一下 jvm 内置锁 的膨胀过程中锁内存怎么变化的,?
5.请介绍一下 jvm 内置锁 的 从轻量级锁升级重量级锁内存怎么变化的?
6.请介绍一下 jvm 锁的 膨胀过程?锁内存怎么变化的?
最近有小伙伴在面试阿里,又遇到了 jvm 内置锁 膨胀相关的面试题。小伙伴 支支吾吾的说了几句,没说清楚,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,上面的面试题以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
本文作者:
- 第一作者 Moen (负责写初稿 ,资深架构师)
- 第二作者 尼恩 (40岁老架构师, 负责提升此文的 技术高度,让大家有一种 俯视 技术的感觉)
为啥内置锁存在多种状态?
在JDK1.6版本之前,所有的Java内置锁都是重量级锁。重量级锁会造成CPU在用户态和核心态之间频繁切换,所以代价高、效率低。
JDK1.6版本为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”实现。
所以,在JDK1.6版本里内置锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这些状态随着竞争情况逐渐升级。
内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种能升级却不能降级的策略,其目的是为了提高获得锁和释放锁的效率。
为什么会存在锁升级现象?
在 synchronized
最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间。
在java5及其以前,只有synchronized 这个是重量级锁,是操作系统级别的重量级操作。
重量级锁两大问题:
-
如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,
-
假如锁的竞争比较激烈,性能下降。
因为重量级锁 存在用户态和内核态之间的转换。
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程,就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,
用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
这也是在JDK6以前 synchronized
效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
一图总览:锁的内存变化及膨胀流程图
二、锁的四种状态
这种方式就是 synchronized
实现同步最初的方式,这也是当初开发者诟病的地方,
所以目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级
锁状态的思路以及特点
锁状态 | 存储内容 | 标志位 |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量的指针 | 11 |
如图所示:
在32位的虚拟机中:
在64位的虚拟机中:
锁对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到索竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
2.4.1 偏向锁/轻量级锁/重量级锁
总体而言,Java对象(Object实例)结构包括三部分:对象头、对象体、对齐字节。具体如图2-4所示。
1. Java对象(Object实例)的三个部分
(1)对象头
对象头包括三个字段,第一个字段叫做_mark Word(标记字),用于存储自身运行时的数据例如GC标志位、哈希码、锁状态等信息。
第二个字段叫做_klass
Pointer(类对象指针),用于存放此对象的元数据(InstanceKlass)的地址。通过_klass
指针,虚拟机通过可以确定这个对象是哪个类的实例.
第二个字段叫做Array Length(数组长度)。如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。
(2)对象体
对象体包含了对象的实例变量(成员变量)。用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。
(3)对齐字节
对齐字节也叫做填充对齐,其作用是用来保证Java对象在所占内存字节数为8的倍数(8N bytes)。HotSpot VM的内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的倍数,便需要填充数据来保证8字节的对齐。
2. 对象结构中的核心字段作用
接下来,对Object实例结构中几个重要的字段的作用做一下简要说明:
(1)_mark(标记字)字段主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode。
(2)_klass(类对象指针)字段是一个指向方法区中类元数据信息的指针,意味着该对象可随时知道自己是哪个Class(实际为InstanceKlass)的实例。
(3)Array Length(数组长度)字段也占用32位(在32位JVM中)的字节,这是可选的,只有当本对象是一个数组对象时才会有这个部分。
(4)对象体用于保存对象属性值,是对象的主体部分,占用的内存空间大小取决于对象的属性数量和类型。
(5)对齐字节并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。当对象实例数据部分没有对齐(8字节的整数倍)时,就需要通过对齐填充来补全。
3. 对象结构中的字段长度
Mark Word(表_mark成员)、_klass
Pointer(表_klass成员)、Array Length等字段的长度,都与JVM的位数有关。
Mark Word的长度为JVM的一个Word(字)大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。Klass Pointer(类对象指针)字段的长度也为JVM的一个Word(字)大小,即32位的JVM为32位,64位的JVM为64位。
所以,
-
在32位JVM虚拟机中,Mark Word和Klass Pointer这两部分都是32位的;
-
在64位JVM虚拟机中,Mark Word和Klass Pointer这两部分都是64位的。
对于对象指针而言,如果JVM中对象数量过多,使用64位的指针将浪费大量内存,通过简单统计,64位的JVM将会比32位的JVM多耗费50%的内存。
为了节约内存可以使用选项+UseCompressedOops开启指针压缩。
选项UseCompressedOops中的Oop部分为Ordinary object pointer普通对象指针的缩写。
如果开启UseCompressedOops选项,以下类型的指针将从64位压缩至32位:
2.4.2 Mark Word的结构信息
Java内置锁的涉及到很多重要信息,这些都存放在对象结构中,并且是存放于对象头的 Mark Word字段中。Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
Mark Word的位长度不会受到Oop对象指针压缩选项的影响。
Java内置锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁。
其实在 JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,在JDK 1.6之后,JVM为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁、轻量级锁的实现,从此以后Java内置锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)。
不同锁状态下的Mark word字段结构
Mark word字段的结构,与Java内置锁的状态强相关。为了让Mark word字段存储更多的信息,JVM将Mark word的最低两个位设置为Java内置锁状态位,不同锁状态下的32位Mark Word结构,如表2-1所示。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
1. 无锁状态
Java对象刚创建时,还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争它)这偏向锁标识位是0、锁状态01。
无锁状态下对象的Mark Word如图2-7所示。
图2-7 无锁状态对象的Mark Word
2. 偏向锁状态
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下,效率非常高。
偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID,内置锁会将该线程当做自己的熟人。偏向锁状态下对象的Mark Word具体如图2-8所示。
图2-8 偏向锁状态内置锁的Mark Word
3. 轻量级锁状态
当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。轻量级锁状态下对象的Mark Word具体如图2-9所示。
图2-9 轻量级锁状态内置锁的Mark Word
当锁处于偏向锁的时候,而又被另一个线程所企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。
自旋原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗 CPU的,如果一直获取不到锁,那线程也不能一直占用 CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
JVM 对于自旋周期的选择,JDK1.6之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。
4. 重量级锁状态
重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器(Monitor)对象,该监视器对象用集合的形式,来登记和管理排队的线程。重量级锁状态下对象的Mark Word具体如图2-10所示。
图2-10 重量级锁状态内置锁的Mark Word
三:如何获得偏向锁
下面的一段代码,如何获得偏向锁?
synchronized (lock)
{
lock.increase();
if (i == MAX_TURN / 2)
{
Print.tcfo("占有锁, lock 的状态: ");
lock.printObjectStruct();
}
}
偏向锁的核心原理是:如果不存在 竞争的一个线程获得了锁,那么锁就从无锁状态,进入偏向状态,此时,Mark Word 的结构变为偏向锁结构,
-
锁对象的锁标志位(lock)被改为01,
-
偏向标志位(biased_lock)被改为1,
-
然后线程的thread ID记录在锁对象的Mark Word中(使用CAS操作完成)。
以后该线程获取锁的时,判断一下线程ID和标志位,就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
偏向锁比较极致,干脆就把同步取消掉,不需要进行CAS了。
偏向锁的发现,主要得益于人们发现某个线程可以频繁的获取到锁。
偏向锁其实就是为了单个线程设计的。
如果某个锁资源一直是被某个线程获取,而且没有其它线程来获取锁,就可以在Mark Word
中记录下这个线程id,该线程就没有必要花时间来进行CAS操作了,可以直接进入到同步代码块。
直到发现有其它线程来抢占锁资源了,就会根据当前状态判断是否把偏向锁膨胀成为轻量级锁。
如果需要使用偏向锁,可以使用参数:-XX:+UseBiased
参数来添加。
在JDK1.6之后是默认开启的,但是启动时间有延迟(4秒),
在JDK1.6之后,需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动
开启偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:关闭之后程序默认会直接进入------->轻量级锁状态
-XX:-UseBiasedLocking
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
四:如何膨胀到 轻量级锁
多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。
有线程来参与锁的竞争,但是获取锁的冲突时间极短。
轻量级锁本质就是自旋锁CAS
主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。
升级时机:当关闭偏向锁功能,或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程“被“释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
轻量级锁也是在 JDK1.6 加入的,当一个线程获取偏向锁的时候,有另外的线程加入锁的竞争时,这个时候就会从偏向锁升级为轻量级锁。
在轻量级锁的状态时,虚拟机首先会在当前线程的栈帧当中建立一个锁记录(Lock Record),用于存储对象 MarkWord 的拷贝,官方称这个为 Displaced Mark Word。
然后虚拟机会使用 CAS 操作尝试将对象的 MarkWord 指向栈中的 Lock Record,如果操作成功说明这个线程获取到了锁,能够进入同步代码块执行,否则说明这个锁对象已经被其他线程占用了,线程就需要使用 CAS 不断的进行获取锁的操作,当然你可能会有疑问,难道就让线程一直死循环了吗?
这对 CPU 的花费那不是太大了吗,确实是这样的因此在 CAS 满足一定条件的时候轻量级锁就会升级为重量级锁,具体过程在重量级锁章节中分析。
当线程需要从同步代码块出来的时候,线程同样的需要使用 CAS 将 Displaced Mark Word 替换回对象的 MarkWord,如果替换成功,那么同步过程就完成了,如果替换失败就说明有其他线程尝试获取该锁,而且锁已经升级为重量级锁,此前竞争锁的线程已经被挂起,因此线程在释放锁的同时还需要将挂起的线程唤醒。
Java6之前
默认启用,默认情况下自旋的次数是10次,或者自旋线程数超过cpu核数一半。
Java6之后
变为自适应自旋锁。意味着自旋的次数不是固定不变的,而是根据:拥有锁线程的状态来决定,或者同一个锁上一次自旋的时间。
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。
在有两个以上的线程竞争同一个轻量级锁的情况下,轻量级锁不再有效(轻量级锁升级的一个条件),这个时候锁为膨胀成重量级锁,锁的标志状态变成 10,MarkWord 当中存储的就是指向重量级锁的指针,后面等待锁的线程就会被挂起。
因为这个时候 MarkWord 当中存储的已经是指向重量级锁的指针,因此在轻量级锁的情况下进入到同步代码块在出同步代码块的时候使用 CAS 将 Displaced Mark Word 替换回对象的 MarkWord 的时候就会替换失败,在前文已经提到,在失败的情况下,线程在释放锁的同时还需要将被挂起的线程唤醒。
五:如何膨胀到 重量级锁
适用于:有大量的线程参与锁的竞争,冲突性很高。
重量级锁原理
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
重量级锁就是一种开销最大的锁机制,
在这种情况下需要操作系统将 没有抢到锁的线程挂起,
JVM(Linux 操作系统下)底层是使用 pthread_mutex_lock 、 pthread_mutex_unlock 、 pthread_cond_wait 、 pthread_cond_signal 和 pthread_cond_broadcast 这几个库函数实现的,而这些函数依赖于 futex 系统调用,因此在使用重量级锁的时候因为进行了系统调用,进程需要从用户态转为内核态将线程挂起,然后从内核态转为用户态,当解锁的时候又需要从用户态转为内核态将线程唤醒,这一来二去的花费就比较大了(和 CAS 自旋锁相比)。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
锁的内存结构变化 大总结
锁状态 | bits | 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|---|
无锁状态 | 对象的hashCode | 0 | 01 |
偏向锁 | 线程ID | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 0 | 00 |
重量级锁 | 指向互斥量的指针 | 0 | 10 |
锁的膨胀流程
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中,进行详细解读。
如果没有 面试机会,可以找尼恩来帮忙,打造一个绝世好简历,实现 职业逆袭:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
说在最后:有问题找老架构取经
JVM锁的膨胀、锁的内存结构变化相关的面试题,是非常常见的面试题。也是核心面试题。也是非常难面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典》V174,在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来帮扶、领路。尼恩已经指导了大量的就业困难的小伙伴上岸.
前段时间,帮助一个40岁+就业困难小伙伴拿到了一个年薪100W的offer,小伙伴实现了 逆天改命 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》