Skip to content

Commit 73ea866

Browse files
committed
book-jvm-2-classloader
1 parent 14e8d98 commit 73ea866

File tree

11 files changed

+152
-14
lines changed

11 files changed

+152
-14
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,12 @@ _site_*/
389389

390390
!.gitignore
391391
!.gitsubmodules
392+
393+
#### obsidian dir ####
394+
.obsidian
395+
396+
#### verysync dir ####
397+
.verysync
398+
399+
#### idea dir ####
400+
.idea

source/blogs/tech/2025-06-24-android-auto-javascript-detect/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,11 @@ public final boolean usbStatus(Context mContext) {
192192
接下来是娱乐时间。
193193

194194
当绕过不成功时会出现图示警告。
195-
![adb-warning.png](adb-warning.png width="150px")
195+
196+
<img src="adb-warning.png" alt="adb-warning" width="200px">
196197

197198
我这里仅仅使用了AutoJsX,所以需要考虑绕过无障碍检测部分。既然是字符串检测,我就修改包名和应用名就好了。最终结果也证明了我的方案是正确的,但是就在我欣喜之际……
198199

199-
![id-error.png](id-error.png width="150px")
200+
<img src="id-error.png" alt="id-error" width="200px">
200201

201202
![silly](emoji-silly.png)

source/blogs/tech/2025-06-26-flower-dance/README.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,20 @@ M:4/4
1212
Q:80
1313
L:1/8
1414
K:B
15+
%% score {v1|v2} % 定义声部分组和连接方式
1516
V:v1
1617
V:v2 bass
17-
[V:v1][|dcgc dcGc|dcgc dcGc|dcgc dcGc|dcgc dcGc|]
18-
[V:v2][|E,B,EF G4|F,CFG A4 |G,DGA B4 | z8 |]
19-
[V:v1][|dcgc dcGc|dcgc dcGc|dcgc dcGc|[g8ac']|]
20-
[V:v2][|E,B,EF G4|F,CFG A4 |G,DGA B4 | [G,8DG] |]
18+
[V:v1]|dcgc dcGc|dcgc dcGc|dcgc dcGc|dcgc dcGc|
19+
[V:v2]|E,B,EF G4|F,CFG A4 |G,DGA B4 | z8 |
20+
[V:v1]|dcgc dcGc|dcgc dcGc|dcgc dcGc|[g8ac']|
21+
[V:v2]|E,B,EF G4|F,CFG A4 |G,DGA B4 | [G,8DG] |
2122
[V:v1]
22-
[|d2z/2G/2B/2d/2 [c2AF] {Ac}[A2cf]|[A3/2c]a/2 g3/2^g/2 ^g2 d'2|g2 {g}[g2d'2] c'd'/2c'/2 af|g6 zd|]
23+
|d2z/2G/2B/2d/2 [c2AF] {Ac}[A2cf] \
24+
|[A3/2c]a/2 g3/2=g/2 ^g2 d'2|g2 {g}[g2d'2] c'(3c'/2d'/2c'/2 af|g6 zd|
25+
|{GB}[G2Bd]z/2 G/2B/2d/2 [F2Ac] [A2cf]|[Ac][A=g][A^g][Aa] d/2b/2c/2a/2 B/2g/2A/2f/2|g/2d'/2c'/2d'/2 g/2d'/2c'/2d'/2 g/2d'/2c'/2d'/2 g/2d'/2c'/2d'/2|
2326
[V:v2]
24-
[|zB,2 zF,CF2|D,A,DA, G,DGD|E,B,EB, F,CFC |G,DGABD G2|]
27+
|zB,2 zF,CF2|D,A,DA, G,DGD|E,B,EB, F,CFC |G,DGABD G2|
28+
|E,B,E2 F,CF2|D,A,D2 G,DG2|E,B,E2 F,CF|
2529
```
2630

2731
```abcjs max-width=640 align=center controls
@@ -34,14 +38,18 @@ M:4/4
3438
Q:80
3539
L:1/8
3640
K:B
41+
%% score {v1|v2} % 定义声部分组和连接方式
3742
V:v1
3843
V:v2 bass
39-
[V:v1][|dcgc dcGc|dcgc dcGc|dcgc dcGc|dcgc dcGc|]
40-
[V:v2][|E,B,EF G4|F,CFG A4 |G,DGA B4 | z8 |]
41-
[V:v1][|dcgc dcGc|dcgc dcGc|dcgc dcGc|[g8ac']|]
42-
[V:v2][|E,B,EF G4|F,CFG A4 |G,DGA B4 | [G,8DG] |]
44+
[V:v1]|dcgc dcGc|dcgc dcGc|dcgc dcGc|dcgc dcGc|
45+
[V:v2]|E,B,EF G4|F,CFG A4 |G,DGA B4 | z8 |
46+
[V:v1]|dcgc dcGc|dcgc dcGc|dcgc dcGc|[g8ac']|
47+
[V:v2]|E,B,EF G4|F,CFG A4 |G,DGA B4 | [G,8DG] |
4348
[V:v1]
44-
[|d2z/2G/2B/2d/2 [c2AF] {Ac}[A2cf]|[A3/2c]a/2 g3/2^g/2 ^g2 d'2|g2 {g}[g2d'2] c'd'/2c'/2 af|g6 zd|]
49+
|d2z/2G/2B/2d/2 [c2AF] {Ac}[A2cf] \
50+
|[A3/2c]a/2 g3/2=g/2 ^g2 d'2|g2 {g}[g2d'2] c'(3c'/2d'/2c'/2 af|g6 zd|
51+
|{GB}[G2Bd]z/2 G/2B/2d/2 [F2Ac] [A2cf]|[Ac][A=g][A^g][Aa] d/2b/2c/2a/2 B/2g/2A/2f/2|g/2d'/2c'/2d'/2 g/2d'/2c'/2d'/2 g/2d'/2c'/2d'/2 g/2d'/2c'/2d'/2|
4552
[V:v2]
46-
[|zB,2 zF,CF2|D,A,DA, G,DGD|E,B,EB, F,CFC |G,DGABD G2|]
53+
|zB,2 zF,CF2|D,A,DA, G,DGD|E,B,EB, F,CFC |G,DGABD G2|
54+
|E,B,E2 F,CF2|D,A,D2 G,DG2|E,B,E2 F,CF|
4755
```
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# 类的加载和类的加载器ClassLoader
2+
3+
## 类的生命周期
4+
5+
![class-lifecycle.png](class-lifecycle.png)
6+
7+
1. 加载阶段(Loading):**类加载器**将类的字节码文件(.class文件)加载到JVM内存中。
8+
- 通过类的全限定名获取类的二进制字节流,包括从本地加载、从网络加载、动态代理生成
9+
- 将字节流所代表的类结构保存到方法区
10+
- 在方法区中生成一个代表该类的`InstanceKlass`,保存了类的信息的C++对象
11+
- 在堆中生成`java.lang.Class`对象与`InstanceKlass`相关联,在Java代码中获取类的信息以及存储静态字段的数据。
12+
13+
```alert type=note
14+
- InstanceKlass在方法区,通常Java代码无法直接访问,从而控制开发者访问数据的范围,可以使用HSDB查看。它向Class类暴露了方法,可以使用Class的反射访问到InstanceKlass的部分数据。
15+
- HSDB:JDK8使用java -cp sa-jdi.jar sun.jvm.hotspot.HSDB ; JDK17使用JAVA_HOME\bin目录下的jhsdb
16+
- 静态字段:JDK8之前静态字段全部储存在方法区,8之后部分存储在堆区。
17+
```
18+
19+
2. 验证(Verification):确保被加载的类的正确性。
20+
- 文件格式验证、元数据验证、字节码验证(魔数、版本号;语义分析,如是否有父类、是否继承final类等;数据流和控制流分析)
21+
- 符号引用验证(解析阶段发生,检查引用的类、字段、方法是否存在)
22+
23+
3. 准备(Preparation):为类的静态变量分配内存并设置初始值。
24+
- 为类变量(static变量)分配内存,并设置默认初始值(如int为`0`,引用为`null`等)
25+
- 注意:final static变量在此阶段会直接赋值为代码中指定的值
26+
27+
4. 解析(Resolution):将符号引用(`cp info \#xx`)转换为指向内存的直接引用(形如`@0x000000007e0000c0`)。
28+
- 类、接口、字段、方法的解析
29+
- 解析可以在初始化之后进行(Java语言的动态绑定特性)
30+
31+
>连接(Linking):连接包含验证、准备、初始化三个阶段。连接阶段不会执行程序员写得代码。
32+
33+
5. 初始化(Initialization):执行类`<clinit>()`方法,真正初始化类变量和其他资源。
34+
- `<cinit>()`执行静态变量赋值语句;执行静态代码块(即Java中的`static{}`)
35+
- 父类的`<clinit>()`先于子类执行
36+
- 线程安全,多个线程同时初始化一个类时只有一个线程能执行
37+
38+
```alert type=note
39+
- `<cinit>()`先执行赋值语句还是先执行代码块和java代码中编写的顺序一致,所以为保证先赋值再执行代码块,需要将所有的 `static Object varX = new Object` 放到类声明紧接的下一行。
40+
- 调用 `Class.forName(String)` 、访问类的静态变量、new一个对象或者执行Main方法的类四种情况会触发初始化阶段。注意变量被final修饰且等号右边是常量,不会触发初始化,因为在3阶段变量已经被赋值。
41+
```
42+
43+
5. 使用(Using):程序正常使用类。
44+
6. 卸载(Unloading):垃圾回收器回收类。
45+
46+
47+
## 类加载器
48+
49+
类加载器是Java虚拟机提供给程序去实现获取类和接口字节码是数据的技术,这也意味着类加载器不止可以使用Java编写。类加载器只参与了第一个阶段的第一个步骤,获取字节码文件。
50+
51+
类加载器的应用场景:SPI机制、类的热部署、Tomcat类的隔离、Arthas。
52+
53+
类加载器分为虚拟机底层用C++实现的启动类加载器和JDK默认提供的Java编写的扩展类加载器和应用程序类加载器。
54+
55+
```alert type=warning
56+
本篇所描述的类加载器环境是JDK8及之前版本,JDK8之后的版本不适用。
57+
```
58+
59+
### 启动类加载器 Bootstrap ClassLoader
60+
61+
由Hotspot虚拟机提供的、使用C++编写的类加载器。默认加载 `JAVA_HOME/jre/lib`下的类文件,比如rt.jar、tools.jar、resources.jar。
62+
63+
使用`getClassLoader()`方法获取启动类加载器返回`null`,说明了启动类加载器不会被程序员直接使用。如果想使用启动类加载器加载用户jar方法有两个:
64+
- 将用户的jar放入`jre/lib`目录中
65+
- 使用 `-Xbootclasspath/a:jar包目录/jar包名` 虚拟机参数(推荐)
66+
67+
### 扩展类加载器 Extension ClassLoader
68+
69+
扩展类加载器由JDK提供使用Java编写的类加载器,默认加载`JAVA_HOME/jre/lib/ext`目录下的类文件。扩展类加载器加载用户jar的方法:
70+
- 将用户jar放入`jre/lib/ext`目录
71+
- 使用 `-Djava.ext.dirs=jar`包目录 虚拟机参数(推荐),这种方式会覆盖原始扩展目录,多个目录使用`;`(或者`:`)隔开
72+
73+
### 应用程序类加载器 Application ClassLoader
74+
75+
加载classpath下的类文件,包括项目文件和项目使用的第三方依赖文件,即运行的程序通常由应用程序类加载器加载。
76+
77+
图中展示了扩展类加载器和应用程序类加载器的继承关系。
78+
![jdk-classloader.png](jdk-classloader.png)
79+
80+
## 实操:记录父类与子类的初始化
81+
82+
现有如下代码:
83+
84+
```java
85+
package xyz.sl;
86+
87+
public class Main {
88+
public static void main(String[] args) throws IOException {
89+
// new B02(); // new一个B02对象
90+
System.out.println(B02.a);
91+
System.in.read();
92+
}
93+
}
94+
95+
class A02 {
96+
static int a = 0;
97+
static {
98+
a = 1;
99+
}
100+
}
101+
102+
class B02 extends A02{
103+
static {
104+
a = 2;
105+
}
106+
}
107+
```
108+
109+
子类`B02`的初始化需要先初始化父类`A02`,因此`A02.a`的值被赋值为1。因为main方法没有触发`B02`的初始化,所以`B02.a`不会被赋值为2,所以最终程序输出结果为1。但若取消代码中的注释,`new B02()`触发`B02`的初始化,a被赋值为2,最终会输出2。
110+
111+
1. 首先来看没有取消注释的情况。打开Jclasslib加载编译生成的Main.class文件,查看main方法的指令。第二行可以看到是 `getstatic \#13 <xyz/sl/B02.a : I>`
112+
![jclasslib-comment-new-B02.png](jclasslib-comment-new-B02.png)
113+
接下来运行main方法,然后再cmd中使用jps命令查看刚刚运行的Main程序的Pid(jps命令和java命令在同一个文件夹下面)。然后打开HSDB窗口,点击File->Attach to HotSpot->输入刚才得到的Pid->点击确定。在HSDB中呈现给我们的就是Main进程在JVM内存中的字节码。Attach成功之后,点击Tools->Class Browser->输入包名并点击回车。
114+
![jps-comment-new-B02.png](jps-comment-new-B02.png)
115+
![hsdb-comment-new-B02.png](hsdb-comment-new-B02.png)
116+
点击Main所在的class引用,再点击main()所在的方法引用,现在呈现的就是java程序实际执行的保存在JVM内存的代码。在第二行能明显看到该处与字节码文件中不一致,也就意味着实际执行的指令是 `getstatic #13 \[Field int a\] of class xyz.sl.A02 @0x000001229d000a00` 。也就是说虽然代码中通过`B02.a`访问,实际加载到内存的引用是`A02.a`
117+
![comment-new-B02-comparison.png](comment-new-B02-comparison.png)
118+
119+
2. 当取消掉注释,`new B02()`触发了新对象的创建。导致`B02`中的静态代码块被执行,最终输出2。
120+
![no-comment-new-B02.png](no-comment-new-B02.png)
97.9 KB
Loading
116 KB
Loading
48 KB
Loading
39.8 KB
Loading
175 KB
Loading
55.2 KB
Loading

0 commit comments

Comments
 (0)