├── .DS_Store
├── .gitignore
├── README.md
├── SUMMARY.md
├── _config.yml
├── chapter01
├── 01.md
├── 02.md
├── 03.md
├── 04.md
├── 05.md
├── 06.md
├── README.md
└── images
│ ├── ch01_01_01.jpg
│ ├── ch01_01_02.jpg
│ ├── ch01_01_03.jpg
│ ├── ch01_01_04.jpg
│ ├── ch01_01_05.jpg
│ ├── ch01_02_01.jpg
│ ├── ch01_03_01.jpg
│ ├── ch01_04_01.jpg
│ ├── ch01_04_02.jpg
│ ├── ch01_05_01.jpg
│ ├── ch01_05_02.jpg
│ ├── ch01_05_03.jpg
│ ├── ch01_05_04.jpg
│ └── ch01_05_05.jpg
├── chapter02
├── 00.md
├── 01.md
├── 02.md
├── 03.md
├── 04.md
├── 05.md
├── 06.md
├── 07.md
├── 08.md
├── 09.md
├── 10.md
├── 11.md
├── 12.md
├── 13.md
├── README.md
└── images
│ ├── chapter02_01_01.png
│ ├── chapter02_01_02.png
│ ├── chapter02_02_01.png
│ ├── chapter02_03_01.png
│ ├── chapter02_03_02.png
│ ├── chapter02_04_01.png
│ ├── chapter02_04_02.png
│ ├── chapter02_05_01.png
│ ├── chapter02_05_02.png
│ ├── chapter02_05_03.png
│ ├── chapter02_06_01.png
│ ├── chapter02_06_02.png
│ ├── chapter02_07_01.png
│ ├── chapter02_07_02.png
│ ├── chapter02_08_01.png
│ ├── chapter02_09_01.png
│ ├── chapter02_11_01.png
│ ├── chapter02_12_01.png
│ ├── chapter02_12_02.png
│ ├── chapter02_12_03.png
│ └── chapter02_12_04.png
├── chapter03
├── 01.md
├── 02.md
├── 03.md
├── 04.md
├── 05.md
├── 06.md
├── 07.md
├── 08.md
├── 09.md
├── 10.md
├── 11.md
├── 12.md
├── README.md
└── images
│ ├── chapter03_01_01.png
│ ├── chapter03_01_02.png
│ ├── chapter03_01_03.png
│ ├── chapter03_03_01.png
│ ├── chapter03_03_02.png
│ ├── chapter03_03_03.png
│ ├── chapter03_03_04.png
│ ├── chapter03_03_05.png
│ ├── chapter03_03_06.png
│ ├── chapter03_03_07.png
│ ├── chapter03_03_08.png
│ ├── chapter03_04_01.png
│ ├── chapter03_04_02.png
│ ├── chapter03_04_03.png
│ ├── chapter03_04_04.png
│ ├── chapter03_04_05.png
│ ├── chapter03_04_06.png
│ ├── chapter03_04_07.png
│ ├── chapter03_04_08.png
│ ├── chapter03_04_09.png
│ ├── chapter03_04_10.png
│ └── chapter03_04_11.png
├── chapter04
├── 01.md
├── 02.md
├── 03.md
├── 04.md
├── 05.md
├── README.md
└── images
│ ├── chapter04-01-01.png
│ ├── chapter04-01-02.png
│ ├── chapter04-02-01.jpg
│ ├── chapter04-02-02.png
│ ├── chapter04-02-03.png
│ ├── chapter04-02-04.png
│ ├── chapter04-02-05.jpg
│ ├── chapter04-02-06.jpg
│ ├── chapter04-02-07.jpg
│ ├── chapter04-02-08.png
│ ├── chapter04-02-09.png
│ └── chapter04-03-01.jpg
├── chapter05
├── 01.md
├── 02.md
├── 03.md
├── 04.md
├── 05.md
├── 06.md
├── 07.md
├── 08.md
├── 09.md
├── README.md
└── images
│ ├── chapter05-02-01.png
│ ├── chapter05-04-01.png
│ ├── chapter05-04-02.png
│ ├── chapter05-06-01.png
│ ├── chapter05-06-02.png
│ ├── chapter05-06-03.png
│ ├── chapter05-06-04.png
│ └── chapter05-06-05.png
├── opensourceprojects
├── README.md
├── asyncframework.md
├── beemite.md
├── jcg.md
└── vine.md
├── qrcode
├── .DS_Store
└── wujiuye_dashang.png
└── xuyan.md
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node rules:
2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
3 | .grunt
4 |
5 | ## Dependency directory
6 | ## Commenting this out is preferred by some people, see
7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git
8 | node_modules
9 |
10 | # Book build output
11 | _book
12 |
13 | # eBook build output
14 | *.epub
15 | *.mobi
16 | *.pdf
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 《深入浅出JVM字节码》
2 |
3 | 《Java虚拟机字节码从入门到实战》写于2020年3月,定位是帮助初学者快速入门Java虚拟机字节码。由于这是我写的第一本书,各方面都没什么经验,最终只选择出电子版,目前已上架Kindle中国、京东读书、网易云阅读、当当云阅读等电子书平台。
4 |
5 | 本书《深入浅出JVM字节码》是[《Java虚拟机字节码从入门到实战》](https://www.amazon.com/Chinese-%E5%90%B4%E5%B0%B1%E4%B8%9A-ebook/dp/B08G8KYVFJ)的第二版,并更名为《深入浅出JVM字节码》。
6 |
7 | * Github链接:https://wujiuye.github.io/JVMByteCodeGitBook/
8 | * 博客网站地址:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/preface
9 |
10 | ## 本书目录
11 |
12 | [目录](SUMMARY.md)
13 |
14 | ## 为什么选择开源
15 |
16 | 开源,是为了更好!
17 |
18 | 我喜欢电子书可以随时修改,避免错误的理解误导更多读者,以及随时追加更多内容,因此我选择开源。
19 |
20 | 开源,也为了搜索引擎能检索到内容,希望真正意义上的帮助到更多想要深入学习JVM字节码的同行。
21 |
22 | 同时,我也希望能有更多的读者参与到本书的写作中,征求大家的修改意见,只为越改越好。
23 |
24 | ## 关于作者
25 |
26 | * 吴就业, 中间件/基础架构研发工程师,个人博客网站:[https://www.wujiuye.com/](https://www.wujiuye.com/)。
27 |
28 | ## 持续更新中
29 |
30 | 由于需要人工将Word文档一章一章的转换为Markdown文档,在此过程中作者也会修改内容,需要花费很多时间,因此暂定每周六晚10点更新一章。(已暂停更新)
31 |
32 | 笔者开源的一些字节码实战案例:
33 |
34 | * [实现类Spring框架@Async注解功能的asyncframework](opensourceprojects/asyncframework.md)
35 | * [一款轻量级的分布式调用链路追踪Java探针vine](opensourceprojects/vine.md)
36 | * [运行时解析json生成class的json-class-generator](opensourceprojects/jcg.md)
37 | * [JavaAgent入门级项目Beemite](opensourceprojects/beemite.md)
38 |
39 | ## 如何贡献
40 |
41 | 可将您的修改提交到自己的分支,并提交合并master分支请求,我会审核您的提交,只要内容没有错误观点且语句通顺一般都会采纳。
42 |
43 | 要求:
44 |
45 | * 允许修改章节标题、二级标题、三级标题;
46 | * 允许修改或重写任意章节内容;
47 | * 允许添加章节;
48 | * 提交请带上姓名-邮箱-简介-提交说明;
49 |
50 | ## 打赏作者
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # 目录
2 |
3 | * [《深入浅出JVM字节码》](README.md)
4 | * [序言](xuyan.md)
5 | * [第一章 Java虚拟机基础](chapter01/README.md)
6 | * [为什么会出现StackOverflowError](chapter01/01.md)
7 | * [JVM运行时内存结构](chapter01/02.md)
8 | * [线程、栈与栈桢](chapter01/03.md)
9 | * [局部变量表与操作数栈](chapter01/04.md)
10 | * [基于栈的指令集架构](chapter01/05.md)
11 | * [本章小结](chapter01/06.md)
12 | * [第二章 深入理解Class文件结构](chapter02/README.md)
13 | * [class文件结构](chapter02/00.md)
14 | * [动手实现class文件结构解析器](chapter02/01.md)
15 | * [解析魔数](chapter02/02.md)
16 | * [解析版本号](chapter02/03.md)
17 | * [解析常量池](chapter02/04.md)
18 | * [解析class文件的访问标志](chapter02/05.md)
19 | * [解析this与super符号引用](chapter02/06.md)
20 | * [解析接口表](chapter02/07.md)
21 | * [解析字段表](chapter02/08.md)
22 | * [解析方法表](chapter02/09.md)
23 | * [解析class文件的属性表](chapter02/10.md)
24 | * [解析整个Class文件结构](chapter02/11.md)
25 | * [属性二次解析](chapter02/12.md)
26 | * [本章小结](chapter02/13.md)
27 | * [第三章 字节码指令集](chapter03/README.md)
28 | * [从Hello Word出发](chapter03/01.md)
29 | * [字段与方法描述符](chapter03/02.md)
30 | * [读写局部变量表与操作数栈](chapter03/03.md)
31 | * [基于对象的操作](chapter03/04.md)
32 | * [访问静态字段与静态方法](chapter03/05.md)
33 | * [调用方法的四条指令](chapter03/06.md)
34 | * [不同类型返回值对应的指令](chapter03/07.md)
35 | * [创建数组与访问数组元素](chapter03/08.md)
36 | * [条件分支语句的实现](chapter03/09.md)
37 | * [循环语句的实现](chapter03/10.md)
38 | * [异常处理的实现](chapter03/11.md)
39 | * [本章小结](chapter03/12.md)
40 | * [第四章 深入理解类加载](chapter04/README.md)
41 | * [动态加载类的两种方式](chapter04/01.md)
42 | * [类加载过程](chapter04/02.md)
43 | * [双亲委派模型](chapter04/03.md)
44 | * [自定义类加载器](chapter04/04.md)
45 | * [本章小结](chapter04/05.md)
46 | * [第五章 ASM快速上手](chapter05/README.md)
47 | * [框架简介](chapter05/01.md)
48 | * [访问者模式在ASM框架中的应用](chapter05/02.md)
49 | * [在项目中使用ASM](chapter05/03.md)
50 | * [创建类并创建方法](chapter05/04.md)
51 | * [给类添加字段](chapter05/05.md)
52 | * [改写类并改写方法](chapter05/06.md)
53 | * [创建类并实现接口](chapter05/07.md)
54 | * [继承类并重写父类方法](chapter05/08.md)
55 | * [本章小结](chapter05/09.md)
56 | * [第六章 实战一:JDK与Cglib动态代理]()
57 | * [JDK动态代理实现原理分析]()
58 | * [动手实现JDK动态代理]()
59 | * [Cglib动态代理实现原理分析]()
60 | * [动手实现Cglib动态代理]()
61 | * [本章小结]()
62 | * [第七章 实战二:APM数据采集之探针埋点]()
63 | * [Instrumentation简介]()
64 | * [编写Java Agent插件]()
65 | * [在类加载之前修改类的字节码]()
66 | * [使用ASM为方法插入埋点]()
67 | * [在类加载之后修改类的字节码]()
68 | * [本章小结]()
69 | * [第八章 进阶篇]()
70 | * [深入理解类型检查与栈映射桢]()
71 | * [深入理解泛型与泛型方法调用]()
72 | * [字节码实战开源项目](opensourceprojects/README.md)
73 | * [实现类Spring框架@Async注解功能的asyncframework](opensourceprojects/asyncframework.md)
74 | * [一款轻量级的分布式调用链路追踪Java探针vine](opensourceprojects/vine.md)
75 | * [运行时解析json生成class的json-class-generator](opensourceprojects/jcg.md)
76 | * [JavaAgent入门级项目Beemite](opensourceprojects/beemite.md)
77 |
78 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/chapter01/01.md:
--------------------------------------------------------------------------------
1 | # 为什么会出现StackOverflowError
2 |
3 | StackOverflowError这个错误常出现在较深的方法调用以及递归方法中,平时很少会遇到。我们以一道经典的递归算法题为例,求1到n的和。为了查看在发生栈溢出时方法一共递归了多少次,我们在方法中打印当前n的值。如代码清单1-1所示。
4 |
5 |
代码清单1-1 使用递归算法求1到n的和
6 |
7 | ```java
8 | public class RecursionAlgorithmMain {
9 | public static int sigma(int n) {
10 | System.out.println("current 'n' value is " + n);
11 | return n + sigma(n + 1);
12 | }
13 | public static void main(String[] args) throws IOException {
14 | new Thread(() -> sigma(1))
15 | .start();
16 | System.in.read();
17 | }
18 | }
19 | ```
20 |
21 | 在默认栈大小情况下,程序抛出栈溢出错误并终止线程时方法递归调用了5961次,如图1.1所示。
22 |
23 | 图1.1 使用默认栈大小时的栈溢出
24 |
25 | 
26 |
27 | 在默认栈大小的情况下,多次运行代码清单1-1的代码,得出的结果是相差不大的。并且我们会发现,在发生StackOverflowError时,进程并没有结束,这是因为一个线程的StackOverflowError并不影响整个进程。
28 |
29 | 现在我们将配置JVM的启动参数-Xss,以调整虚拟机栈的大小为256k。如果你是使用IDEA运行本例代码,可直接在VM options配置加上-Xss256K。如果你是使用java命令运行,可在java命令后面加上-Xss256k,启动命令如下。
30 |
31 | ```shell
32 | java -Xss256k -jar RecursionAlgorithmMain.jar
33 | ```
34 |
35 | 在调整栈的大小后,再次运行代码清单1-1的代码,sigma方法只递归调用了1601次,如图1.2所示。
36 |
37 | 图1.2 栈大小为256K时的栈溢出
38 |
39 | 
40 |
41 | 这与调整栈大小之前似乎存在着某种关系,用调整栈大小之前程序发生栈溢出时方法的调用次数除以栈大小调整后的,结果约为4。这是不是说明栈的大小默认为1024K呢。当然,以这个测试结果来说明其实并不严谨。
42 |
43 | 我们可以通过打印虚拟机参数查看默认的栈大小。使用jinfo[^1]命令行工具可查看某个Java进程当前虚拟机栈的大小,这是jdk提供的工具,不需要额外下载安装。使用jinfo查看Java进程的线程栈大小如下:
44 |
45 | ```shell
46 | wjy$ jinfo -flag ThreadStackSize 29643
47 | ```
48 |
49 | * 其中29643为进程ID。
50 |
51 | 在不修改任何配置的情况下,在64位Linux系统上执行jinfo命令,查询出来的默认栈大小为1M,如图1.3所示。
52 |
53 | 图1.3 jinfo查看默认线程栈大小
54 |
55 | 
56 |
57 | 栈大小除了可以使用jinfo命令行工具查看之外,我们还可以通过NAT[^2]工具查看,并且使用NAT工具还可以查看方法区的大小。
58 |
59 | 以使用Java命令启动Java进程为例,在Java命令后面加上开启NAT的配置参数NativeMemoryTracking,如下:
60 |
61 | ```shell
62 | java -XX:NativeMemoryTracking=summary -jar xxx.jar
63 | ```
64 |
65 | 进程启动后,可通过jcmd[^3]命令行工具查看该进程的内存占用信息,如图1.4所示。
66 |
67 | 图1.4 NAT打印内存占用信息
68 |
69 | 
70 |
71 | 从图1.4中,我们能看到当前进程Java堆的大小、用于存储类元数据信息使用的内存大小、线程栈总共占用的内存大小等。从线程栈信息来看,被查看的进程当前线程数为63,使用内存为63696K,也就是每个线程栈占用1M内存。
72 |
73 | NAT工具也用于排查内存泄露问题,当项目中依赖了一些使用直接内存的第三方jar包时,可能会因为使用不当而造成内存泄露。如堆内存没有用满,但top[^4]命令查看内存使用率却接近百分百,这种情况就很有可能是程序使用堆外直接内存造成的。
74 |
75 | -Xss参数在多线程项目中常用于JVM调优。假设项目中开启1024个线程,那么使用默认栈大小的情况下,虚拟机栈将会占用1G的内存,而如果将栈大小调整为256K,虚拟机将只花费256M内存用于1024个栈的分配。
76 |
77 | 最后,我们也可以在HotSpot源码中找到关于栈大小的设置[^5]。以Linux操作系统且处理器架构为AMD 64位为例,HotSpot虚拟机配置的默认栈大小为1M,编译线程的栈大小默认为4M,如代码清单1-2所示。
78 |
79 | 代码清单1-2 获取默认栈大小的方法
80 |
81 | ```c++
82 | // return default stack size for thr_type
83 | size_t os::Linux::default_stack_size(os::ThreadType thr_type) {
84 | // default stack size (compiler thread needs larger stack)
85 | #ifdef AMD64
86 | size_t s = (thr_type == os::compiler_thread ? 4 * M : 1 * M);
87 | #else
88 | size_t s = (thr_type == os::compiler_thread ? 2 * M : 512 * K);
89 | #endif // AMD64
90 | return s;
91 | }
92 | ```
93 |
94 | 栈也有最小值,该最小值因操作系统及处理器架构而议。如果在64位的Linxu系统下,我们使用java命令启动一个jar包并将-Xss参数配置为128K,进程将会异常终止,并提示创建Java虚拟机失败,要求栈最小值为228K。如图1.5所示。
95 |
96 | 图1.5 设置栈大小为128k进程启动失败
97 |
98 | 
99 |
100 | 虚拟机栈的最小值是在虚拟机启动时、解析完全局参数之后调用os::init_2方法设置。栈的最小取值不仅受当前处理器架构是32位还是64位的影响,也受系统页大小影响。例如,在64位Linxu操作系统下,HotSopt所允许设置栈大小的最小值为228K[^6],如代码清单1-3所示。
101 |
102 | 代码清单1-3 设置栈的最小值
103 |
104 | ```c++
105 | // os::init_2(void)
106 | os::Linux::min_stack_allowed = MAX2(os::Linux::min_stack_allowed,
107 | (size_t)(StackYellowPages+StackRedPages+StackShadowPages) * Linux::page_size() + (2*BytesPerWord COMPILER2_PRESENT(+1)) * Linux::vm_default_page_size());
108 | ```
109 |
110 | [^1]: jinfo是JDK提供的命令行工具,可以用来查看正在运行的 java 应用程序的扩展参数。
111 | [^2]: Native Memory Tracking是一个Java Hotspot VM新特性,用于跟踪热点VM的内部内存使用情况。
112 | [^3]: jcmd是JDK提供的命令行工具,使用jcmd可访问NMT数据。
113 | [^4]: Linux操作系统提供的top命令可用于查看系统当前每个进程对内存的占用率以及CPU的使用率等信息。
114 | [^5]: 源码在hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp文件中,方法名为default_stack_size。
115 | [^6]: 源码在hotspot/src/os/linux/vm/os_linux.cpp文件中
116 |
117 | ---
118 |
119 | 发布于:2021 年 06 月 23 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter01_01.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
120 |
121 |
--------------------------------------------------------------------------------
/chapter01/02.md:
--------------------------------------------------------------------------------
1 | # JVM运行时内存结构
2 |
3 | Java虚拟机运行时数据区域分为方法区、堆、虚拟机栈、本地方法栈和程序计数器[^1],如图1.6所示。
4 |
5 | 图1.6 JVM运行时内存结构
6 |
7 | 
8 |
9 | ## Java堆
10 |
11 | Java堆(Java Heap)是线程共享的,用于存放对象实例。然而,并非所有的对象都会存放在堆中。比如开启内存逃逸分析后,JIT即时编译器会将多次被执行的字节码编译为机器码,同时也会分析方法体内的对象创建,如果方法体内创建的对象没有逃离出方法体之外,即不会被别的地方引用,没有别的线程使用,那么就不需要将对象分配到堆中,而是直接分配到虚拟机栈上。
12 |
13 | ## 方法区
14 |
15 | 方法区(Method Area)也是线程共享的,用于存放虚拟机加载的类信息、常量、静态变量等数据。在JDK1.8之前,HotSpot虚拟机使用永久代实现方法区,而1.8及之后使用元数据区实现方法区。运行时常量池是方法区的一部分,用于存放类被加载后的常量池表。
16 |
17 | ## 虚拟机栈&本地方法栈
18 |
19 | Java虚拟机栈(Java Virtual Machine Satck)是线程私有的,它的生命周期与线程的生命周期相同。根据《Java虚拟机规范》的规定,native方法应该在本地方法栈中执行,但在HotSpot虚拟机中,本地方法栈已与虚拟机栈合二为一。
20 |
21 | ## 程序计数器
22 |
23 | 程序计数器(Program Counter,PC),在JVM中,JVM使用一个一块比较小的内存空间作为PC,PC存储的是当前线程当前栈帧字节码执行的偏移量,在栈帧出栈时,会从方法出口获取前一个栈帧的下一条指令的偏移量。
24 |
25 | 我们知道多线程的实现是多个线程轮流占用CPU而实现的,在线程切换的时候需要保存当前线程的执行状态,在这个线程重新占用CPU的时候才能恢复到之前的状态,而JVM线程状态的保存是依赖PC实现的,所以PC是线程所私有的内存区域。
26 |
27 | ---
28 |
29 | [^1]: 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 第二章,运行时数据区域。
30 |
31 | 发布于:2021 年 06 月 23 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter01_02.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
32 |
--------------------------------------------------------------------------------
/chapter01/03.md:
--------------------------------------------------------------------------------
1 | # 线程、栈与栈桢
2 |
3 | 在Java中,Java线程与操作系统一对一绑定,Java虚拟机栈也与操作系统线程栈映射,操作系统线程在Java线程创建时创建。前面介绍使用-Xss参数可配置虚拟机栈的大小,实际上就是指定操作系统线程栈的大小。
4 |
5 | 我们以Java命令启动一个Java程序其实就是启动一个JVM进程,JVM启动后会加载类的字节码执行,而操作系统是以线程为调度单位的,Java线程又与操作系统线程一对一绑定,所以我们编写的Java代码最终都会在线程上执行。
6 |
7 | 程序中的main方法是Java程序的入口,JVM会为main方法的执行分配一个线程,叫main线程。虽然我们看不到main方法被放在Java线程中执行,但我们可以打印main方法所在线程名,如代码清单1-4所示。
8 |
9 | 代码清单1-4 打印main方法所在线程名
10 |
11 | ```java
12 | public static void main(String[] args) throws IOException {
13 | // 输出:main
14 | System.out.println(Thread.currentThread().getName());
15 | }
16 | ```
17 |
18 | 在Java中创建Thread对象并调用start方法时,JVM会为其创建一个Java线程,并创建一个操作系统线程,将操作系统线程绑定到Java线程上。HotSpot虚拟机线程start流程如下:
19 |
20 | ```c++
21 | // 第一步:jvm.cpp文件中,JVM_StartThread方法
22 | native_thread = new JavaThread(&thread_entry, sz);
23 | // 第二步: thread.cpp文件中,JavaThread的构建方法
24 | os::create_thread(this, thr_type, stack_sz);
25 | // 第三步:os_linux.cpp文化中,os::create_thread方法
26 | OSThread* osthread = new OSThread(NULL, NULL);
27 | thread->set_osthread(osthread);
28 | ```
29 |
30 | 虽然Java是一门面向对象的语言,但程序运行依然是基于方法的调用,每个方法对应一个栈桢,方法的调用对应栈桢的入栈和出栈。Java类中的每个方法经过编译处理后最终变为字节码指令存储在Code属性中,所以调用方法时需指定调用哪个类的方法,而调用类的静态方法与非静态方法区别只在于调用方法时是否需要传递this引用,这与过程调用并无差异。栈与栈桢的关系如图1.7所示。
31 |
32 | 图1.7 线程、栈与栈桢的关系
33 |
34 | 
35 |
36 | 在调用Thread对象的start方法时,该线程对应的虚拟机栈的第一个栈桢是run方法。run方法中每调用一个方法就对应一个栈桢的入栈,一个方法只有执行结束才会出栈。方法执行结束包括方法抛出异常结束、return命令返回。
37 |
38 | 栈的大小是固定的,默认栈大小是1M,可通过-Xss参数配置。因此,从run方法开始,如果调用链路过深,如递归方法,在栈没有足够的空间容纳下一个栈桢的入栈时,就会出现StackOverflowError错误,同时当前栈被销毁,当前线程结束。HotSpot虚拟机的实现源码[^1]如代码清单1-6所示。
39 |
40 | 代码清单1-6 HotSpot方法调用源码
41 |
42 | ```c++
43 | void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
44 | os::os_exception_wrapper(call_helper, result, &method, args, THREAD);
45 | }
46 |
47 | void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
48 | methodHandle method = *m;
49 | JavaThread* thread = (JavaThread*)THREAD;
50 | .......
51 | // 判断当前线程的调用栈是否有足够的内存
52 | if (!os::stack_shadow_pages_available(THREAD, method)) {
53 | // 内存不足,抛出stack_overflow异常
54 | Exceptions::throw_stack_overflow_exception(THREAD, __FILE__, __LINE__, method);
55 | return;
56 | } else {
57 | // 占用足够的内存
58 | os::bang_stack_shadow_pages();
59 | }
60 | ......
61 | }
62 | ```
63 |
64 | ---
65 |
66 | [^1]: 源码在hotspot/src/share/vm/runtime/javaCalls.cpp文件中
67 |
68 | 发布于:2021 年 06 月 23 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter01_03.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
--------------------------------------------------------------------------------
/chapter01/04.md:
--------------------------------------------------------------------------------
1 | # 局部变量表与操作数栈
2 |
3 | 在了解线程、栈与栈桢的关系后,我们还要重点关注栈桢中的局部变量表与操作数栈,这两个数据结构是字节码指令执行所依赖的。
4 |
5 | ## 局部变量表
6 |
7 | 局部变量表存储方法中声明的变量、方法参数,如果是非静态方法还会存放this引用。局部变量表的大小是固定的,在编译时就已经确定。这也是我们在操作字节码时需要注意的一点,我们需要计算方法的局部变量表需要多大,如果设置过大就会造成内存资源的浪费。
8 |
9 | 局部变量表的结构是一个数组,数组的单位是Slot(变量槽),Slot的大小是多少个字节由虚拟机决定。在32位的HotSpot虚拟机中,一个Slot槽的大小等于4个字节,而在64位的HotSpot虚拟机中,一个Slot槽的大小等于8个字节。但64位的HotSpot虚拟机可以配置是否开启指针压缩,如果开启指针压缩,那么一个Slot槽的大小也等于4个字节。局部变量表的结构如图1.8所示。
10 |
11 | 图1.8 局部变量表结构
12 |
13 | 
14 |
15 | 在此提出几个问题:在方法中new一个对象是否会将对象的引用存入局部变量表?try-catch块,catch括号中的异常是否存在局部变量表?使用build构造者模式时,链式调用方法会不会每次调用都将返回的对象先存储到局部变量表?这些问题将在第三章解答。
16 |
17 | ## 操作数栈
18 |
19 | 操作数栈与局部变量表有相似的地方,其大小也是固定的,栈的大小在编译期确定,单位也是Slot。但不同的是,操作数栈的大小并不仅与局部变量的个数有关。操作数栈的结构如图1.9所示。
20 |
21 | 图1.9 操作数栈结构
22 |
23 | 
24 |
25 | 操作数栈用于存储执行字节码指令所需要的参数。比如获取对象自身的字段,需要先将this引用压入栈顶,再执行getfield字节码指令;比如执行new指令后,栈顶会存放该new指令返回的对象的引用。
26 |
27 | 局部变量表与操作数栈大小的设置,也会影响到栈桢的大小,从而影响栈所能容纳的栈桢的最大数量。以前面栈溢出的例子说明,默认1M大小的栈大概能调用六千次的递归求和方法,而如果递归方法中再写得复杂些,也会导致调用次数的下降。因此,在使用ASM框架操作字节码时,我们需要合理设置这个结构的大小。
28 |
29 | ---
30 |
31 | 发布于:2021 年 06 月 23 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter01_04.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
--------------------------------------------------------------------------------
/chapter01/05.md:
--------------------------------------------------------------------------------
1 | # 基于栈的指令集架构
2 |
3 | 在汇编语言中,除直接内存操作的指令外,其它指令的执行都依赖寄存器,如跳转指令、循环指令、加减法指令等。汇编指令集是由硬件直接支持的,不同架构的CPU提供的汇编指令集也会不一样[^1]。
4 |
5 | 以一个经典的++i面试题为例,使用c语言编写的实现如下。
6 |
7 | ```c++
8 | int m = ++i;
9 | ```
10 |
11 | 反汇编后对应的32位x86 CPU的汇编指令如下。
12 |
13 | ```assembly
14 | inc dword ptr [ebp-44h]
15 | mov eax,dword prt [ebp-44h]
16 | mov dword ptr [ebp-4ch],eax
17 | ```
18 |
19 | 这三条指令的意思是,先将[ebp-44h]内存块存储的值加1,而dword ptr相当于c语言中的类型声明。接着将[ebp-44h]内存块存储的值放入到eax寄存器,最后将eax寄存器存储的值放到[ebp-4ch]内存块,也就是赋值给变量m。由于i和m是在栈上分配的内存,因此[ebp-44h]对应i的内存地址,[ebp-4ch]对应m的内存地址。
20 |
21 | 汇编指令不能直接操作将一块内存的值赋值给另一块内存,必须要通过寄存器。32位x86 CPU包括8个通用寄存器,EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI,其中EBP、ESP用做指针寄存器,存放堆栈内存储单元的偏移量。这些看不懂没关系,这也不是本书的重点。
22 |
23 | 上述++i的例子使用java代码实现如下。
24 |
25 | ```java
26 | public static void main(String[] args) {
27 | int a = 10;
28 | int result = ++a;
29 | System.out.println(result);
30 | }
31 | ```
32 |
33 | 使用javap命令输出这段代码的字节码如下。
34 |
35 | ```assembly
36 | public static void main(java.lang.String[]);
37 | Code:
38 | 0: bipush 10
39 | 2: istore_1
40 | 3: iinc 1, 1
41 | 6: iload_1
42 | 7: istore_2
43 | 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
44 | 11: iload_2
45 | 12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
46 | 15: return
47 | ```
48 |
49 | 字节码指令前面的编号我们暂时可以理解为行号。在本例中,行号0到7的字节码指令完成的工作是将变量a自增后的值赋值给result变量。下面将详细分析这几条指令的执行过程。
50 |
51 | 1. bipush指令是将立即数10放入到操作数栈顶,该指令执行完成后操作数栈的变化如图1.10所示。
52 |
53 | 图1.10 bipush指令执行过程
54 |
55 | 
56 |
57 | 2. istore_1指令是将操作数栈顶的元素从操作数栈顶弹出,并存放到局部变量表中索引为1的Slot,即赋值给变量a。该指令执行完成后局部变量表的变化如图1.11所示。
58 |
59 | 图1.11 istore指令执行过程
60 |
61 | 
62 |
63 | 3. iinc这条字节码指令比较特别,它可以直接操作局部变量表的变量,而不需要经过操作数栈。该指令是将局部变量表中索引为1的Slot所存储的整数值自增1,也就是将局部变量a自增1,如图1.12所示。
64 |
65 | 图1.12 iinc指令执行过程
66 |
67 | 
68 |
69 | 4. iload_1指令是将自增后的变量a放入操作数栈的栈顶,该指令执行完成后操作数栈的变化如图1.13所示。
70 |
71 | 图1.13 iload_1指令执行过程
72 |
73 | 
74 |
75 | 5. 最后,istore_2指令是将当前操作数栈顶的元素从操作数栈中弹出,并存放到局部变量表中索引为2的Slot,也就是给result变量赋值,如图1.14所示。
76 |
77 | 图1.14 istore_2指令执行过程
78 |
79 | 
80 |
81 | 从++i的例子中,我们可以看出,字节码是依赖操作数栈工作的。在虚拟机上执行的字节码指令虽然最终也是编译为机器码执行,但编写字节码指令时并不需要我们考虑需要使用哪些寄存器的问题,这些都交由JVM去实现。
82 |
83 | 使用汇编指令编写代码,我们需要考虑CPU的架构,有多少个寄存器可选,了解硬件,需要关心每条指令操作多少个字节,在使用寄存器之前需要考虑是否需要备份寄存器的当前值,指令执行完之后是否需要恢复寄存器的值。而使用依赖栈工作的字节码指令编写代码,我们只需要关心每条字节码指令需要多少个参数,按顺序将参数push到操作数栈顶。如果指令执行完有返回值,在指令执行完成后,操作数栈顶存储的就是返回值。
84 |
85 | ---
86 |
87 | [^1]: cpu架构是cpu厂商给属于同一系列的cpu产品定的一个规范。
88 |
89 | 发布于:2021 年 06 月 23 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter01_05.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
--------------------------------------------------------------------------------
/chapter01/06.md:
--------------------------------------------------------------------------------
1 | # 本章小结
2 |
3 | 本章我们从栈溢出的例子出发,了解了栈与线程的关系、栈与栈桢的关系,同时也介绍在多线程项目中如何通过配置-Xss参数调优,降低进程占用的内存,以及如何通过NAT工具查看进程使用的内存情况。
4 |
5 | 在理解栈桢之后又重点分析局部变量表与操作数栈,回顾栈溢出的例子,理解一个栈桢的大小与这两者的关系。最后通过++i的例子列举了汇编指令与字节码指令在架构上的不同,并简单分析字节码解释执行的过程。
6 |
7 | 本章介绍的栈、栈桢、局部变量表与操作数栈是学习Java字节码必备的基础知识。
8 |
9 | ---
10 |
11 | 发布于:2021 年 06 月 23 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter01_06.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
--------------------------------------------------------------------------------
/chapter01/README.md:
--------------------------------------------------------------------------------
1 | # 第一章 认识Java虚拟机栈
2 |
3 | 本章介绍学习JVM字节码必备的Java虚拟机基础知识,理解Java线程、Java虚拟机栈、栈帧、局部变量表、操作数栈是理解字节码指令的基础。
4 |
5 |
--------------------------------------------------------------------------------
/chapter01/images/ch01_01_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_01_01.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_01_02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_01_02.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_01_03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_01_03.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_01_04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_01_04.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_01_05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_01_05.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_02_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_02_01.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_03_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_03_01.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_04_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_04_01.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_04_02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_04_02.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_05_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_05_01.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_05_02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_05_02.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_05_03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_05_03.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_05_04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_05_04.jpg
--------------------------------------------------------------------------------
/chapter01/images/ch01_05_05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter01/images/ch01_05_05.jpg
--------------------------------------------------------------------------------
/chapter02/00.md:
--------------------------------------------------------------------------------
1 | # Class文件结构
2 |
3 | 根据《Java虚拟机规范》的规定,class文件结构如下表所示。
4 |
5 | class文件结构
6 |
7 | | 类型 | 说明 |
8 | | -------------- | :------------------------------------------ |
9 | | u4 | Magic(魔数) |
10 | | u2 | Minor Version(副版本号) |
11 | | u2 | Major Version(主版本号) |
12 | | u2 | Constant Pool Count(常量池计数) |
13 | | cp_info | Constant Pool[常量池计数-1] (常量池数组) |
14 | | u2 | Access Flags(访问标志,如public) |
15 | | u2 | This Class(类索引,在常量池表中的索引) |
16 | | u2 | Super Class(父类索引,在常量池表中的索引) |
17 | | u2 | Interfaces Count(接口总数) |
18 | | u2 | Interfaces[接口总数] |
19 | | u2 | Fields Count (字段总数) |
20 | | field_info | Fields[字段总数] (字段表) |
21 | | u2 | Methods Count(方法总数) |
22 | | method_info | Methods[方法总数-1] (方法表) |
23 | | u2 | Attributes Count(属性总数) |
24 | | attribute_info | Attributes[属性总数](属性表) |
25 |
26 | 其中类型u2、u4是《Java虚拟机规范》中定义的类型,分别表示两个字节、四个字节,而cp_info、field_info、method_info、attribute_info都是结构体,与class文件结构一样,它们也有固定的格式,这些结构体我们后面在实现class文件结构解析器过程中再详细介绍。
27 |
28 | 从表“class文件结构”可以看出,一个完整的class文件按照顺序存储了魔数、版本号、常量池计数、常量池数组、该类的访问标志、this符号索引、继承的父类的符号索引、实现的接口总数与接口表、字段总数与字段表、方法总数与方法表、属性总数与属性表。
29 |
30 | ---
31 |
32 | 发布于:2021 年 07 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_00.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
33 |
34 |
--------------------------------------------------------------------------------
/chapter02/01.md:
--------------------------------------------------------------------------------
1 | # 动手实现class文件结构解析器
2 |
3 | 可用于分析class文件结构的工具有很多,笔者将其分为两类:一类是基于十六进制的分析工具,如010editer、UE;另一类是可视化的class文件结构分析工具,如开源的classpy[^1]。为能让读者更好的理解class文件结构,本书将介绍如何使用Java语言编写一个解析class文件的工具类项目,通过该项目实现class文件的解析,并从编写该项目的过程中慢慢掌握class文件结构。
4 |
5 | 首先,根据 class文件结构以及class文件的解析流程设计该项目的技术架构图,如下图所示。
6 |
7 | 
8 |
9 | 接着,根据架构图搭建项目的框架,框架搭建如下图所示。
10 |
11 | 
12 |
13 | 我们先定义对应class文件结构中各项结构的类型(type包),如常量池(CpInfo.class)、字段表(FieldInfo.class)、方法表(MethodInfo.class)、属性表(AttributeInfo.class)、U2(class文件结构的基本单位,占两个字节)、U4(class文件结构的基本单位,占四个字节);再定义各项的解析器(handler包),如魔数解析器、版本号解析器等;最后通过工具类ClassFileAnalysiser使用责任链模式协调各个解析器的工作完成class文件的解析。
14 |
15 | 在框架搭建好后,我们再开始编码完成各个解析器。
16 |
17 | 根据 class文件结构创建ClassFile类,ClassFile类代码如下。
18 |
19 | ```java
20 | public class ClassFile {
21 | private U4 magic; // 魔数
22 | private U2 minor_version; // 副版本号
23 | private U2 magor_version; // 主版本号
24 | private U2 constant_pool_count; // 常量池计数器
25 | private CpInfo[] constant_pool; // 常量池
26 | private U2 access_flags; // 访问标志
27 | private U2 this_class; // 类索引
28 | private U2 super_class; // 父类索引
29 | private U2 interfaces_count; // 接口总数
30 | private U2[] interfaces; // 接口数组
31 | private U2 fields_count; // 字段总数
32 | private FieldInfo[] fields; // 字段表
33 | private U2 methods_count; // 方法总数
34 | private MethodInfo[] methods; // 方法表
35 | private U2 attributes_count; // 属性总数
36 | private AttributeInfo[] attributes; // 属性表
37 | }
38 | ```
39 |
40 | ClassFile类中的每个字段是按照class文件结构中各项的顺序声明的,其中CpInfo、FieldInfo、MethodInfo、AttributeInfo这几个类目前并未添加任何字段,只是一个空的类,代码如下。
41 |
42 | ```java
43 | public class CpInfo {
44 | }
45 | public class FieldInfo {
46 | }
47 | public class MethodInfo {
48 | }
49 | public class AttributeInfo {
50 | }
51 | ```
52 |
53 | U2和U4是class文件结构的基本单位,长度分别为两个字节和四个字节,我们需要为这两种基本单位创建对应的Java类。这两个类都只需要一个字段,类型为Byte数组,长度在构造方法中控制,要求构造方法必须传入数组每个元素的值。
54 |
55 | U2:
56 |
57 | ```java
58 | public class U2 {
59 |
60 | private byte[] value;
61 |
62 | public U2(byte b1, byte b2) {
63 | value = new byte[]{b1, b2};
64 | }
65 |
66 | public Integer toInt() {
67 | return (value[0] & 0xff) << 8 | (value[1] & 0xff);
68 | }
69 |
70 | public String toHexString() {
71 | char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
72 | StringBuilder hexStr = new StringBuilder();
73 | for (int i = 1; i >= 0; i--) {
74 | int v = value[i] & 0xff;
75 | while (v > 0) {
76 | int c = v % 16;
77 | v = v >>> 4;
78 | hexStr.insert(0, hexChar[c]);
79 | }
80 | if (((hexStr.length() & 0x01) == 1)) {
81 | hexStr.insert(0, '0');
82 | }
83 | }
84 | return "0x" + (hexStr.length() == 0 ? "00" : hexStr.toString());
85 | }
86 |
87 | }
88 | ```
89 |
90 | U4:
91 |
92 | ```java
93 | public class U4 {
94 |
95 | private byte[] value;
96 |
97 | public U4(byte b1, byte b2, byte b3, byte b4) {
98 | value = new byte[]{b1, b2, b3, b4};
99 | }
100 |
101 | public int toInt() {
102 | int a = (value[0] & 0xff) << 24;
103 | a |= (value[1] & 0xff) << 16;
104 | a |= (value[2] & 0xff) << 8;
105 | return a | (value[3] & 0xff);
106 | }
107 |
108 | public String toHexString() {
109 | char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
110 | StringBuilder hexStr = new StringBuilder();
111 | for (int i = 3; i >= 0; i--) {
112 | int v = value[i] & 0xff;
113 | while (v > 0) {
114 | int c = v % 16;
115 | v = v >>> 4;
116 | hexStr.insert(0, hexChar[c]);
117 | }
118 | if (((hexStr.length() & 0x01) == 1)) {
119 | hexStr.insert(0, '0');
120 | }
121 | }
122 | return "0x" + hexStr.toString();
123 | }
124 | }
125 | ```
126 |
127 | 提示:为便于验证解析结果是否正确以及解析结果的可读性,我们还给这两个类添加了一个byte[]转int的方法(toInt),以及byte[]转16进制字符串的方法(toHexString)。
128 |
129 | 接着我们需要创建一个接口BaseByteCodeHandler,抽象出class文件结构各项的解析器行为。每个解析器应该只负责完成class文件结构中某一项的解析工作,如常量池解析器就只负责解析常量池。
130 |
131 | BaseByteCodeHandler接口:
132 |
133 | ```java
134 | public interface BaseByteCodeHandler {
135 | /**
136 | * 解释器的排序值
137 | * @return
138 | */
139 | int order();
140 |
141 | /**
142 | * 读取
143 | * @param codeBuf
144 | * @param classFile
145 | */
146 | void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception;
147 | }
148 | ```
149 |
150 | BaseByteCodeHandler接口定义了一个read方法,该方法要求传入class文件的字节缓存[^2]和ClassFile对象。每个继承BaseByteCodeHandler的解析器都可以在read方法中从字节缓存读取相应的字节数据写入ClassFile对象。
151 |
152 | 由于解析是按顺序解析的,因此BaseByteCodeHandler接口还定义了一个返回排序值的方法,该值用于实现解析器的排序。比如,版本号解析器应该排在魔数解析器的后面,因此,版本号解析器order方法的返回值应该比魔数解析器order方法的返回值大。
153 |
154 | 有了解析器之后,我们还需要实现一个管理和调度解析器工作的总指挥:ClassFileAnalysiser ,代码如下。
155 |
156 | ```java
157 | public class ClassFileAnalysiser {
158 |
159 | private final static List handlers = new ArrayList<>();
160 |
161 | static {
162 | // 添加各项的解析器
163 | handlers.add(new MagicHandler());
164 | handlers.add(new VersionHandler());
165 | ......
166 | // 解析器排序,要按顺序调用
167 | handlers.sort((Comparator.comparingInt(BaseByteCodeHandler::order)));
168 | }
169 |
170 | // 将传入的从class文件读取的字节缓存,解析生成一个ClassFile 对象
171 | public static ClassFile analysis(ByteBuffer codeBuf) throws Exception {
172 | // 重置ByteBuffer的读指针,从头开始
173 | codeBuf.position(0);
174 | ClassFile classFile = new ClassFile();
175 | // 遍历解析器,调用每个解析器的解析方法
176 | for (BaseByteCodeHandler handler : handlers) {
177 | handler.read(codeBuf, classFile);
178 | }
179 | return classFile;
180 | }
181 |
182 | }
183 | ```
184 |
185 | ClassFileAnalysiser 的静态代码块负责实例化各个解释器并排好序。
186 |
187 | ClassFileAnalysiser 暴露analysis方法给外部调用,由analysis方法根据解析器的排序顺序去调用各个解析器的read方法完成class文件结构各项的解析工作,由各项解析器将解析结果赋值给ClassFile对象的对应字段。
188 |
189 | analysis方法的入参是class文件内容的字节缓存,从class文件中读取而来。在该项目中使用ByteBuffer而不直接使用byte[]缓存加载的class文件,是因为使用ByteBuffer能更好的控制顺序读取。
190 |
191 | 假设我们已经实现了所有解析器,那么我们只需要实现将class文件加载到内存中,再调用ClassFileAnalysiser的analysis方法就能实现将一个class文件解析为一个ClassFile对象,例如。
192 |
193 | ```java
194 | public class ClassFileAnalysisMain {
195 |
196 | public static ByteBuffer readFile(String classFilePath) throws Exception {
197 | File file = new File(classFilePath);
198 | if (!file.exists()) {
199 | throw new Exception("file not exists!");
200 | }
201 | byte[] byteCodeBuf = new byte[4096];
202 | int lenght;
203 | try (InputStream in = new FileInputStream(file)) {
204 | lenght = in.read(byteCodeBuf);
205 | }
206 | if (lenght < 1) {
207 | throw new Exception("not read byte code.");
208 | }
209 | // 将字节数组包装为ByteBuffer
210 | return ByteBuffer.wrap(byteCodeBuf, 0, lenght).asReadOnlyBuffer();
211 | }
212 |
213 | public static void main(String[] args) throws Exception {
214 | // 读取class文件
215 | ByteBuffer codeBuf = readFile("xxx.class");
216 | // 解析class文件
217 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
218 | // 打印魔数解析器解析出来的Magic
219 | System.out.println(classFile.getMagic().toHexString());
220 | }
221 |
222 | }
223 | ```
224 |
225 | 当然,这只是整体的框架搭建,class文件结构各项的解释器还没有实现。接下来,我们就按照解析class文件结构的顺序实现各项解析器。
226 |
227 | ---
228 |
229 | [^1]: classpy是一个开源的class文件结构分析工具:https://github.com/zxh0/classpy
230 | [^2]: class文件字节缓存是指从class文件读入内存的字节缓存,这是一个数组,大小即为class文件的大小。
231 |
232 | 发布于:2021 年 07 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_01.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
233 |
--------------------------------------------------------------------------------
/chapter02/02.md:
--------------------------------------------------------------------------------
1 | # 解析魔数
2 |
3 | 魔数,只是用来判断一个文件是否是一个class文件,魔数占四个字节,固定值为0xCAFEBABE,这个值永远不会改变,存储在class文件的前4个字节,如下图所示。
4 |
5 | 
6 |
7 | 魔数解析的实现非常简单,我们只需要从class文件字节缓存中连续读取四个字节,将这四个字节转为一个U4对象,并赋值给ClassFile对象的magic字段,实现代码如下。
8 |
9 | ```java
10 | public class MagicHandler implements BaseByteCodeHandler {
11 | // 排序排在第一个
12 | @Override
13 | public int order() {
14 | return 0;
15 | }
16 | @Override
17 | public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
18 | // 连续读取四个字节并转为U4对象
19 | classFile.setMagic(new U4(codeBuf.get(), codeBuf.get(),codeBuf.get(), codeBuf.get()));
20 | if (!"0xCAFEBABE".equalsIgnoreCase(classFile.getMagic() .toHexString())) {
21 | throw new Exception("这不是一个Class文件");
22 | }
23 | }
24 | }
25 | ```
26 |
27 | ---
28 |
29 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_02.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
30 |
31 |
--------------------------------------------------------------------------------
/chapter02/03.md:
--------------------------------------------------------------------------------
1 | # 解析版本号
2 |
3 | class文件结构的版本号分为主版本号和副版本号,它们共同构成class文件格式的版本号[^1],副版本号在前,主版本号在后,分别占两个字节。比如一个class文件的主版本号为52,副版本号为0,那么这个class文件结构的版本号就是52.0。版本号在class文件中的存储位置如下图所示。
4 |
5 | 
6 |
7 | > 提示:本章为描述class文件结构各项分别在class文件中的位置所绘制的示例图是连贯的。
8 |
9 | 版本号解析器的职责是从class文件字节缓存中读取出副版本号和主版本号,按顺序读取,先读取两个字节的副版本号,再读取两个字节的主版本号,因此版本号解析器的实现代码如下。
10 |
11 | ```java
12 | public class VersionHandler implements BaseByteCodeHandler {
13 | // 版本号解析器排在魔数解析器的后面
14 | @Override
15 | public int order() {
16 | return 1;
17 | }
18 | @Override
19 | public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
20 | // 读取副版本号
21 | U2 minorVersion = new U2(codeBuf.get(), codeBuf.get());
22 | classFile.setMinor_version(minorVersion);
23 | // 读取主版本号
24 | U2 majorVersion = new U2(codeBuf.get(), codeBuf.get());
25 | classFile.setMagor_version(majorVersion);
26 | }
27 | }
28 | ```
29 |
30 | class文件格式的各个版本与JDK版本的对应关系如下表格所示。
31 |
32 | | ***Class文件格式版本*** | ***JDK版本号*** | ***16进制表示*** |
33 | | ----------------------- | --------------- | ---------------- |
34 | | 45.0 | 1.1 | 0x 00 00 00 2D |
35 | | 46.0 | 1.2 | 0x 00 00 00 2E |
36 | | 47.0 | 1.3 | 0x 00 00 00 2F |
37 | | 48.0 | 1.4 | 0x 00 00 00 30 |
38 | | 49.0 | 1.5 | 0x 00 00 00 31 |
39 | | 50.0 | 1.6 | 0x 00 00 00 32 |
40 | | 51.0 | 1.7 | 0x 00 00 00 33 |
41 | | 52.0 | 1.8 | 0x 00 00 00 34 |
42 |
43 | 我们可以编写单元测试来验证框架是否能正确解析出该class文件的魔数和版本号,单元测试代码如下。
44 |
45 | ```java
46 | public class MagicAndVersionTest {
47 |
48 | @Test
49 | public void testMagicAndVersionHandler() throws Exception {
50 | // 将class文件读取到ByteBuffer
51 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("RecursionAlgorithmMain.class");
52 | // 解析class文件
53 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
54 | System.out.println(classFile.getMagic().toHexString()); // 打印魔数
55 | System.out.println(classFile.getMinor_version().toInt()); // 打印副版本号
56 | System.out.println(classFile.getMagor_version().toInt()); // 打印主版本号
57 | }
58 |
59 | }
60 | ```
61 |
62 | 单元测试结果如下图所示。
63 |
64 | 
65 |
66 | ---
67 |
68 | [^1]: 《Java虚拟机规范》Java SE8版本ClassFile结构
69 |
70 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_03.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
71 |
72 |
--------------------------------------------------------------------------------
/chapter02/05.md:
--------------------------------------------------------------------------------
1 | # 解析class文件的访问标志
2 |
3 | Class文件结构中的访问标志项access_flags是用U2类型存储的,也就是2个字节。用某个bit位的值是否为1判断该类或接口的访问权限、属性。
4 |
5 | 访问标志与类或接口的访问权限、属性的映射
6 |
7 | | ***标志名*** | ***值*** | ***描述*** |
8 | | -------------- | -------- | ------------------------------------- |
9 | | ACC_PUBLIC | 0x0001 | 声明为public |
10 | | ACC_FINAL | 0x0010 | 声明为final,不允许继承 |
11 | | ACC_SUPER | 0x0020 | Jdk1.0.2之后编译的class文件都会有此值 |
12 | | ACC_INTERFACE | 0x0200 | 声明该class是接口 |
13 | | ACC_ABSTRACT | 0x0400 | 声明为抽象类 |
14 | | ACC_SYNTHETIC | 0x1000 | 表示该class文件并非由java代码编译生成 |
15 | | ACC_ANNOTATION | 0x2000 | 标志这是一个注解类型 |
16 | | ACC_ENUM | 0x4000 | 标志这是一个枚举类型 |
17 |
18 | 如何判断一个类设置了上表中的哪些标志呢?
19 |
20 | 首先从Class文件字节缓存中读取到access_flags的值,再将access_flags转为int类型,将转换后的值“算术与(&)”上各个标志的值,判断结果是否等于这个标志的值,实现代码如下。
21 |
22 | ```java
23 | public class ClassAccessFlagUtils {
24 |
25 | private static final Map classAccessFlagMap = new HashMap<>();
26 |
27 | static {
28 | // 公有类型
29 | classAccessFlagMap.put(0x0001, "public");
30 | // 不允许有子类
31 | classAccessFlagMap.put(0x0010, "final");
32 | classAccessFlagMap.put(0x0020, "super");
33 | // 接口
34 | classAccessFlagMap.put(0x0200, "interface");
35 | // 抽象类
36 | classAccessFlagMap.put(0x0400, "abstract");
37 | // 该class非java代码编译后生成
38 | classAccessFlagMap.put(0x1000, "synthetic");
39 | // 注解类型
40 | classAccessFlagMap.put(0x2000, "annotation");
41 | // 枚举类型
42 | classAccessFlagMap.put(0x4000, "enum");
43 | }
44 |
45 | /**
46 | * 获取16进制对应的访问标志字符串表示 (仅用于类的访问标志)
47 | *
48 | * @param flag 访问标志
49 | * @return
50 | */
51 | public static String toClassAccessFlagsString(U2 flag) {
52 | final int flagVlaue = flag.toInt();
53 | StringBuilder flagBuild = new StringBuilder();
54 | classAccessFlagMap.keySet()
55 | .forEach(key -> {
56 | if ((flagVlaue & key) == key) {
57 | flagBuild.append(classAccessFlagMap.get(key)).append(",");
58 | }
59 | });
60 | return flagBuild.length() > 0 && flagBuild.charAt(flagBuild.length() - 1) == ',' ?
61 | flagBuild.substring(0, flagBuild.length() - 1) : flagBuild.toString();
62 | }
63 |
64 | }
65 | ```
66 |
67 | 在class文件中紧挨着常量池存储的就是access_flags,如下图所示。
68 |
69 | 
70 |
71 | 现在我们来实现class文件访问标志解析器AccessFlagsHandler,并将AccessFlagsHandler解析器交给ClassFileAnalysiser管理。AccessFlagsHandler的排序值设置为3,即放在常量池解析器之后,约定在常量池解析器解析完成之后再到访问标志解析器解析。AccessFlagsHandler的实现代码如下。
72 |
73 | ```java
74 | public class AccessFlagsHandler implements BaseByteCodeHandler {
75 |
76 | @Override
77 | public int order() {
78 | return 3;
79 | }
80 |
81 | @Override
82 | public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
83 | classFile.setAccess_flags(new U2(codeBuf.get(), codeBuf.get()));
84 | }
85 |
86 | }
87 | ```
88 |
89 | 最后编写单元测试,验证class文件访问标志解析器是否能正常完成解析。在单元测试中,调用ClassAccessFlagUtils 工具类的toClassAccessFlagsString方法将访问标志输出为字符串。
90 |
91 | ```java
92 | public class AccessFlagsHandlerTest {
93 |
94 | @Test
95 | public void testAccessFlagsHandlerHandler() throws Exception {
96 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("RecursionAlgorithmMain.class");
97 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
98 | // 获取访问标志
99 | U2 accessFlags = classFile.getAccess_flags();
100 | // 输出为字符串
101 | System.out.println(ClassAccessFlagUtils.toClassAccessFlagsString(accessFlags));
102 | }
103 |
104 | }
105 | ```
106 |
107 | 单元测试结果如下图所示。
108 |
109 | 
110 |
111 |
112 |
113 | ---
114 |
115 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_05.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
116 |
117 |
--------------------------------------------------------------------------------
/chapter02/06.md:
--------------------------------------------------------------------------------
1 | # 解析this与super符号引用
2 |
3 | > 提示:这里指的解析this与super符号引用,并非类加载阶段的解析符号引用。
4 |
5 | 在Class文件结构中,紧挨着访问标志access_flags项的是this_class和super_class这两项,也都是U2类型,如下图所示。
6 |
7 | 
8 |
9 | this_class存储的是常量池中某项常量的索引,super_class要么为0,要么也是存储常量池中某项常量的索引[^1]。this_class和super_class指向的常量必须是一个CONSTANT_Class_info结构的常量。
10 |
11 | 只有Object类的super_class可以为0,接口的super_class指向常量池中描述Object类的CONSTANT_Class_info常量。
12 |
13 | this_class与super_class符号解析器实现代码如下。
14 |
15 | ```java
16 | public class ThisAndSuperClassHandler implements BaseByteCodeHandler {
17 |
18 | @Override
19 | public int order() {
20 | return 4;
21 | }
22 |
23 | @Override
24 | public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
25 | classFile.setThis_class(new U2(codeBuf.get(), codeBuf.get()));
26 | classFile.setSuper_class(new U2(codeBuf.get(), codeBuf.get()));
27 | }
28 |
29 | }
30 | ```
31 |
32 | 由于该项目已经完成了常量池的解析,在解析获取到this_class与super_class之后,我们就可以先根据this_class的值到常量池中取得对应的CONSTANT_Class_info常量,再从取得的CONSTANT_Class_info常量中获取该常量的name_index的值,最后根据name_index再回到常量池中取得对应的CONSTANT_Utf8_info常量,这样就能获取到具体的类名了。该过程描述如下图所示。
33 |
34 | 
35 |
36 | 现在我们编写单元测试验证解析器。为了直观的显示解析结果,我们需要在单元测试用例中,将解析后的this_class与super_class指向的CONSTANT_Class_info常量转为CONSTANT_Utf8_info常量,直接输出字符串类名,代码如下。
37 |
38 | ```java
39 | public class ThisAndSuperHandlerTest {
40 |
41 | @Test
42 | public void testThisAndSuperHandlerHandler() throws Exception {
43 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("RecursionAlgorithmMain.class");
44 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
45 | // this_class
46 | U2 this_class = classFile.getThis_class();
47 | // 根据this_class 到常量池获取CONSTANT_Class_info常量
48 | // 由于常量池的索引是从1开始的,所以需要将索引减1取得数组下标
49 | CONSTANT_Class_info this_class_cpInfo = (CONSTANT_Class_info) classFile.getConstant_pool()[this_class.toInt() - 1];
50 | CONSTANT_Utf8_info this_class_name= (CONSTANT_Utf8_info)
51 | classFile.getConstant_pool()
52 | [this_class_cpInfo.getName_index().toInt()-1];
53 | System.out.println(this_class_name);
54 | // super_class
55 | U2 super_class = classFile.getSuper_class();
56 | CONSTANT_Class_info super_class_cpInfo = (CONSTANT_Class_info)
57 | classFile.getConstant_pool() [super_class.toInt() - 1];
58 |
59 | CONSTANT_Utf8_info supor_class_name = (CONSTANT_Utf8_info)
60 | classFile.getConstant_pool()
61 | [super_class_cpInfo.getName_index().toInt()-1];
62 | System.out.println(supor_class_name);
63 | }
64 |
65 | }
66 | ```
67 |
68 | 单元测试结果如下。
69 |
70 | 
71 |
72 | 从输出的结果可以看出,该class文件的类名为RecursionAlgorithmMain,父类类名为Object。
73 |
74 | ---
75 |
76 | [^1]: 常量池索引都是从1开始的,0就表示不使用。
77 |
78 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_06.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
79 |
80 |
--------------------------------------------------------------------------------
/chapter02/07.md:
--------------------------------------------------------------------------------
1 | # 解析接口表
2 |
3 | 在class文件中,继this_class与super_class之后,存储的就是该class实现的接口总数以及该class实现的所有接口。
4 |
5 | 假设,某个class实现的接口总数为1,则接口表需要占用两个字节,用于存储描述该接口的CONSTANT_Class_info常量在常量池中的索引,如下所示。
6 |
7 | 
8 |
9 | 接口解析器InterfacesHandler的实现代码如下。
10 |
11 | ```java
12 | public class InterfacesHandler implements BaseByteCodeHandler {
13 |
14 | @Override
15 | public int order() {
16 | return 5;
17 | }
18 |
19 | @Override
20 | public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
21 | // 1)
22 | classFile.setInterfaces_count(new U2(codeBuf.get(), codeBuf.get()));
23 | int interfaces_count = classFile.getInterfaces_count().toInt();
24 | U2[] interfaces = new U2[interfaces_count];
25 | classFile.setInterfaces(interfaces);
26 | // 2)
27 | for (int i = 0; i < interfaces_count; i++) {
28 | interfaces[i] = new U2(codeBuf.get(), codeBuf.get());
29 | }
30 | }
31 |
32 | }
33 | ```
34 |
35 | * 1)、先获取接口总数interfaces_count,根据interfaces_count创建接口表interfaces[];
36 | * 2)、根据接口总数按顺序读取接口,接口表中的每项都是一个常量索引,指向常量池表中的CONSTANT_Class_info结构的常量。
37 |
38 | 接着我们将解析器注册到ClassFileAnalysiser,然后编写单元测试。
39 |
40 | 由于接口表中的每一项都是指向常量池表中CONSTANT_Class_info常量的引用,因此,我们可以在单元测试中,根据CONSTANT_Class_info的name_index获取到对应的CONSTANT_Utf8_info常量,拿到接口的类型名称。单元测试代码如下。
41 |
42 | ```java
43 | public class InterfacesHandlerTest {
44 |
45 | @Test
46 | public void testInterfacesHandlerHandler() throws Exception {
47 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("InterfacesHandler.class");
48 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
49 | System.out.println("接口总数:" + classFile.getInterfaces_count().toInt());
50 | if (classFile.getInterfaces_count().toInt() == 0) {
51 | return;
52 | }
53 | U2[] interfaces = classFile.getInterfaces();
54 | // 遍历接口表
55 | for (U2 interfacesIndex : interfaces) {
56 | // 根据索引从常量池中取得一个CONSTANT_Class_info常量
57 | CONSTANT_Class_info interfaces_class_info =
58 | (CONSTANT_Class_info) classFile.getConstant_pool()
59 | [interfacesIndex.toInt() - 1];
60 | // 根据CONSTANT_Class_info的name_index从常量池取得一个
61 | // CONSTANT_Utf8_info常量
62 | CONSTANT_Utf8_info interfaces_class_name_info =
63 | (CONSTANT_Utf8_info) classFile.getConstant_pool()
64 | [interfaces_class_info.getName_index().toInt() - 1];
65 | System.out.println(interfaces_class_name_info);
66 | }
67 | }
68 |
69 | }
70 | ```
71 |
72 | 单元测试结果如下。
73 |
74 | 
75 |
76 | 从结果可以看出,该class文件实现的接口总数为1,实现的接口为BaseByteCodeHandler。
77 |
78 | ---
79 |
80 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_07.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
81 |
82 |
--------------------------------------------------------------------------------
/chapter02/08.md:
--------------------------------------------------------------------------------
1 | # 解析字段表
2 |
3 | 在同一个Class文件中,不允许存在两个相同的字段,相同指的是字段名与类型描述符都相同。
4 |
5 | 字段结构与Class文件结构一样,都有访问标志项,但两者的访问标志项在访问权限和属性上有些区别。
6 |
7 | 字段中的访问权限和属性标志
8 |
9 | | ***标志名*** | ***十六进制值*** | ***说明*** |
10 | | ------------- | ---------------- | ---------------------------------- |
11 | | ACC_PUBLIC | 0x 00 01 | 声明为public |
12 | | ACC_PRIVATE | 0x 00 02 | 声明为private |
13 | | ACC_PROTECTED | 0x 00 04 | 声明为protected |
14 | | ACC_STATIC | 0x 00 08 | 声明为static |
15 | | ACC_FINAL | 0x 00 10 | 声明为final |
16 | | ACC_VOLATILE | 0x 00 40 | 声明为volatile |
17 | | ACC_TRANSIENT | 0x 00 80 | 声明为transient |
18 | | ACC_SYNTHTIC | 0x 10 00 | 标志该字段由编译器产生,不在源码中 |
19 | | ACC_ENUM | 0x 40 00 | 声明为枚举类型的成员 |
20 |
21 | 字段的结构如下表所示。
22 |
23 | | ***字段名*** | ***类型*** | ***说明*** |
24 | | ---------------- | -------------- | -------------------------------------- |
25 | | access_flags | u2 | 访问标志 |
26 | | name_index | u2 | 字段名,指向常量池中某个常量的索引 |
27 | | descriptor_index | u2 | 类型描述符,指向常量池中某个常量的索引 |
28 | | attributes_count | u2 | 属性总数 |
29 | | attributes | attribute_info | 属性 |
30 |
31 | 其中,access_flags是字段的访问标志,name_index是字段名称常量的索引,descriptor_index是字段的类型描述符常量索引。字段结构与方法结构、Class文件结构都有属性表attributes,属性表的属性个数可以是0个或多个。
32 |
33 | 属性的通用结构如下表所示。
34 |
35 | | ***字段名*** | ***类型*** | ***说明*** |
36 | | -------------------- | ---------- | ---------------------------------- |
37 | | attribute_name_index | u2 | 属性名称,指向常量池某个常量的索引 |
38 | | attribute_length | u4 | 属性长度,属性info的字节总数 |
39 | | info | u1 | 属性信息,不同的属性不同的解析 |
40 |
41 | > 提示:关于属性,我们先了解属性的通用结构,实现属性的初步解析,让字段解析器能够完成字段的解析工作,至于属性info是什么我们暂时先不关心。
42 |
43 | 首先创建字段结构对应的Java类FieldInfo,代码如下。
44 |
45 | ```java
46 | public class FieldInfo {
47 | private U2 access_flags;
48 | private U2 name_index;
49 | private U2 descriptor_index;
50 | private U2 attributes_count;
51 | private AttributeInfo[] attributes;
52 | }
53 | ```
54 |
55 | 接着创建属性结构对应的Java类AttributeInfo,代码如下。
56 |
57 | ```java
58 | public class AttributeInfo {
59 | private U2 attribute_name_index;
60 | private U4 attribute_length;
61 | private byte[] info;
62 | }
63 | ```
64 |
65 | 最后创建字段表解析器FieldHandler,实现字段表的解析。字段结构的属性表的解析工作也由字段表解析器完成,解析流程如下:
66 |
67 | * 1、先从class文件字节缓存中读取到字段总数,根据字段总数创建字段表;
68 | * 2、循环解析出每个字段;
69 | * 3、解析字段的属性表时,先解析获取到属性总数,再根据属性总数创建属性表;
70 | * 4、使用通用属性结构循环解析出字段的每个属性;
71 | * 5、解析属性时,先解析出attribute_name_index,再解析attribute_length获取属性info的长度,根据长度读取指定长度的字节数据存放到属性的info字段。
72 |
73 | 字段表解析器的实现代码如下。
74 |
75 | ```java
76 | public class FieldHandler implements BaseByteCodeHandler {
77 |
78 | @Override
79 | public int order() {
80 | // 排在接口解析器的后面
81 | return 6;
82 | }
83 |
84 | @Override
85 | public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
86 | // 读取字段总数
87 | classFile.setFields_count(new U2(codeBuf.get(), codeBuf.get()));
88 | int len = classFile.getFields_count().toInt();
89 | if (len == 0) {
90 | return;
91 | }
92 | // 创建字段表
93 | FieldInfo[] fieldInfos = new FieldInfo[len];
94 | classFile.setFields(fieldInfos);
95 | // 循环解析出每个字段
96 | for (int i = 0; i < fieldInfos.length; i++) {
97 | // 解析字段
98 | fieldInfos[i] = new FieldInfo();
99 | // 读取字段的访问标志
100 | fieldInfos[i].setAccess_flags(new U2(codeBuf.get(), codeBuf.get()));
101 | // 读取字段名称
102 | fieldInfos[i].setName_index(new U2(codeBuf.get(), codeBuf.get()));
103 | // 读取字段类型描述符索引
104 | fieldInfos[i].setDescriptor_index(new U2(codeBuf.get(), codeBuf.get()));
105 | // 读取属性总数
106 | fieldInfos[i].setAttributes_count(new U2(codeBuf.get(), codeBuf.get()));
107 | // 获取字段的属性总数
108 | int attr_len = fieldInfos[i].getAttributes_count().toInt();
109 | if (attr_len == 0) {
110 | continue;
111 | }
112 | // 创建字段的属性表
113 | fieldInfos[i].setAttributes(new AttributeInfo[attr_len]);
114 | // 循环解析出每个属性,先使用通用属性结构解析每个属性
115 | for (int j = 0; j < attr_len; j++) {
116 | // 解析字段的属性
117 | fieldInfos[i].getAttributes()[j]
118 | .setAttribute_name_index(new U2(codeBuf.get(),codeBuf.get()));
119 | // 获取属性info的长度
120 | U4 attr_info_len = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get());
121 | fieldInfos[i].getAttributes()[j].setAttribute_length(attr_info_len);
122 | // 解析info
123 | byte[] info = new byte[attr_info_len.toInt()];
124 | codeBuf.get(info, 0, attr_info_len.toInt());
125 | fieldInfos[i].getAttributes()[j].setInfo(info);
126 | }
127 | }
128 | }
129 |
130 | }
131 | ```
132 |
133 | 为验证解析结果,我们还需要编写将字段的访问标志access_flags转为字符串输出的工具类FieldAccessFlagUtils ,代码如下。
134 |
135 | ```java
136 | public class FieldAccessFlagUtils {
137 | private static final Map fieldAccessFlagMap = new HashMap<>();
138 |
139 | static {
140 | fieldAccessFlagMap.put(0x0001, "public");
141 | fieldAccessFlagMap.put(0x0002, "private");
142 | fieldAccessFlagMap.put(0x0004, "protected");
143 | fieldAccessFlagMap.put(0x0008, "static");
144 | fieldAccessFlagMap.put(0x0010, "final");
145 | fieldAccessFlagMap.put(0x0040, "volatile");
146 | fieldAccessFlagMap.put(0x0080, "transient");
147 | fieldAccessFlagMap.put(0x1000, "synthtic");
148 | fieldAccessFlagMap.put(0x4000, "enum");
149 | }
150 |
151 | /**
152 | * 获取16进制对应的访问标志和属性字符串表示 (仅用于类的访问标志)
153 | *
154 | * @param flag 字段的访问标志
155 | * @return
156 | */
157 | public static String toFieldAccessFlagsString(U2 flag) {
158 | final int flagVlaue = flag.toInt();
159 | StringBuilder flagBuild = new StringBuilder();
160 | fieldAccessFlagMap.keySet()
161 | .forEach(key -> {
162 | if ((flagVlaue & key) == key) {
163 | flagBuild.append(fieldAccessFlagMap.get(key)).append(",");
164 | }
165 | });
166 | return flagBuild.length() > 0 && flagBuild.charAt(flagBuild.length() - 1) == ',' ?
167 | flagBuild.substring(0, flagBuild.length() - 1) : flagBuild.toString();
168 | }
169 |
170 | }
171 | ```
172 |
173 | 字段表解析器编写完成后,我们先将字段解析器注册到ClassFileAnalysiser,再编写单元测试。要求单元测试需要将解析后的所有字段的名称、类型、以及字段的访问标志转为字符串打印出来,以验证结果是否正确。
174 |
175 | 字段表解析器单元测试代码如下。
176 |
177 | ```java
178 | public class FieldHandlerTest {
179 |
180 | private static String getName(U2 name_index, ClassFile classFile) {
181 | CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info)
182 | classFile.getConstant_pool()[name_index.toInt() - 1];
183 | return name_info.toString();
184 | }
185 |
186 | @Test
187 | public void testFieldHandlerHandler() throws Exception {
188 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("Builder.class");
189 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
190 | System.out.println("字段总数:" + classFile.getFields_count().toInt());
191 | System.out.println();
192 | FieldInfo[] fieldInfos = classFile.getFields();
193 | // 遍历字段表
194 | for (FieldInfo fieldInfo : fieldInfos) {
195 | System.out.println("访问标志和属性:" + FieldAccessFlagUtils
196 | .toFieldAccessFlagsString(fieldInfo.getAccess_flags()));
197 | System.out.println("字段名:"
198 | + getName(fieldInfo.getName_index(), classFile));
199 | System.out.println("字段的类型描述符:"
200 | + getName(fieldInfo.getDescriptor_index(), classFile));
201 | System.out.println("属性总数:"
202 | + fieldInfo.getAttributes_count().toInt());
203 | System.out.println();
204 | }
205 | }
206 |
207 | }
208 | ```
209 |
210 | 单元测试结果如下图所示。
211 |
212 | 
213 |
214 | 从输出的结果可以看出,该class有三个字段,访问权限都是private的,字段名分别是a、b、c,并且类型描述符都是“I”,即字段的类型都是整型。
215 |
216 | ---
217 |
218 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_08.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
219 |
220 |
--------------------------------------------------------------------------------
/chapter02/09.md:
--------------------------------------------------------------------------------
1 | # 解析方法表
2 |
3 | class文件的方法表用于存放一个类或者接口的所有方法,方法结构与字段结构一样,都有属性表,方法编译后的字节码指令是存放在方法结构的属性表中的,对应Code属性。但不是所有方法都会有Code属性,如接口中的方法不一定会有Code属性,如抽象方法一定没有Code属性。方法包括静态方法、以及类的初始化方法和类的实例初始化方法。
4 |
5 | 方法表结构如下。
6 |
7 | | ***字段名*** | ***类型*** | ***说明*** |
8 | | ---------------- | -------------- | ------------------------------------------ |
9 | | access_flags | u2 | 方法的访问标志和属性 |
10 | | name_index | u2 | 方法名,值为指向常量池中某个常量的索引 |
11 | | descriptor_index | u2 | 方法描述符,值为指向常量池中某个常量的索引 |
12 | | attributes_count | u2 | 属性总数 |
13 | | attributes | attribute_info | 属性表 |
14 |
15 | 其中方法名称索引、方法描述符索引与字段结构中的字段名索引和字段类型描述符索引,都是指向常量池中CONSTABT_Utf8_info结构的常量。与字段结构一样,方法结构也拥有属性总数和属性表,只是也会存在一些差异,如方法有Code属性而字段没有。访问标志也与字段的访问标志有些区别,如字段有ACC_VOLATILE标志而方法没有。
16 |
17 | 方法的访问权限及属性标志
18 |
19 | | ***标志名*** | ***十六进制取值*** | ***说明*** |
20 | | ---------------- | ------------------ | ------------------------------ |
21 | | ACC_PUBLIC | 0x 00 01 | 声明方法访问权限为public |
22 | | ACC_PRIVATE | 0x 00 02 | 声明方法访问权限为private |
23 | | ACC_PROTECTED | 0x 00 04 | 声明方法访问权限为protected |
24 | | ACC_STATIC | 0x 00 08 | 声明方法为static |
25 | | ACC_FINAL | 0x 00 10 | 声明方法为final,不允许覆盖 |
26 | | ACC_SYNCHRONIZED | 0x 00 20 | 同步方法,多线程调用加锁 |
27 | | ACC_BRIDGE | 0x 00 40 | 声明为bridge方法,由编译器产生 |
28 | | ACC_VARARGS | 0x 00 80 | 方法有可变长参数 |
29 | | ACC_NATIVE | 0x 01 00 | native方法 |
30 | | ACC_ABSTRACT | 0x 04 00 | 抽象方法 |
31 | | ACC_STRICT | 0x 08 00 | 使用FP-strict浮点模式 |
32 | | ACC_SYNTHETIC | 0x 10 00 | 非源代码编译出来的 |
33 |
34 | 首先,根据方法结构创建对应的Java类MethodInfo,代码如下。
35 |
36 | ```java
37 | public class MethodInfo {
38 | private U2 access_flags;
39 | private U2 name_index;
40 | private U2 descriptor_index;
41 | private U2 attributes_count;
42 | private AttributeInfo[] attributes;
43 | }
44 | ```
45 |
46 | 其次是创建方法表解析器,代码如下。
47 |
48 | ```java
49 | public class MethodHandler implements BaseByteCodeHandler {
50 |
51 | @Override
52 | public int order() {
53 | // 排在字段解析器的后面
54 | return 7;
55 | }
56 |
57 | @Override
58 | public void read(ByteBuffer codeBuf, ClassFile classFile)
59 | throws Exception {
60 | classFile.setMethods_count(new U2(codeBuf.get(), codeBuf.get()));
61 | // 获取方法总数
62 | int len = classFile.getMethods_count().toInt();
63 | if (len == 0) {
64 | return;
65 | }
66 | // 创建方法表
67 | MethodInfo[] methodInfos = new MethodInfo[len];
68 | classFile.setMethods(methodInfos);
69 | for (int i = 0; i < methodInfos.length; i++) {
70 | // 解析方法
71 | methodInfos[i] = new MethodInfo();
72 | methodInfos[i].setAccess_flags(new U2(codeBuf.get(),codeBuf.get()));
73 | methodInfos[i].setName_index(new U2(codeBuf.get(), codeBuf.get()));
74 | methodInfos[i].setDescriptor_index(new U2(codeBuf.get(), codeBuf.get()));
75 | methodInfos[i].setAttributes_count(new U2(codeBuf.get(), codeBuf.get()));
76 | // 获取方法的属性总数
77 | int attr_len = methodInfos[i].getAttributes_count().toInt();
78 | if (attr_len == 0) {
79 | continue;
80 | }
81 | // 创建方法的属性表
82 | methodInfos[i].setAttributes(new AttributeInfo[attr_len]);
83 | for (int j = 0; j < attr_len; j++) {
84 | methodInfos[i].getAttributes()[j] = new AttributeInfo();
85 | // 解析方法的属性
86 | methodInfos[i].getAttributes()[j]
87 | .setAttribute_name_index(new U2(codeBuf.get(), codeBuf.get()));
88 | // 获取属性info的长度
89 | U4 attr_info_len = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get());
90 | methodInfos[i].getAttributes()[j] .setAttribute_length(attr_info_len);
91 | if (attr_info_len.toInt() == 0) {
92 | continue;
93 | }
94 | // 解析info
95 | byte[] info = new byte[attr_info_len.toInt()];
96 | codeBuf.get(info, 0, attr_info_len.toInt());
97 | methodInfos[i].getAttributes()[j].setInfo(info);
98 | }
99 | }
100 | }
101 |
102 | }
103 | ```
104 |
105 | > 提示:与字段表的解析流程一样,我们暂时不关心属性表的具体属性的解析,属性表的解析只使用通用属性结构解析。
106 |
107 | 最后是将方法表解析器注册到ClassFileAnalysiser,这一步省略。
108 |
109 | 现在我们来编写单元测试,验证方法表解析器解析结果的正确性,方法表解析器的单元测试与字段表解析器的单元测试逻辑差不多,代码如下。
110 |
111 | ```java
112 | public class MethodHandlerTest {
113 |
114 | private static String getName(U2 name_index, ClassFile classFile) {
115 | CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info)
116 | classFile.getConstant_pool()[name_index.toInt() - 1];
117 | return name_info.toString();
118 | }
119 |
120 | @Test
121 | public void testMethodHandlerHandler() throws Exception {
122 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("Builder.class");
123 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
124 | System.out.println("方法总数:" + classFile.getMethods_count().toInt());
125 | System.out.println();
126 | MethodInfo[] methodInfos = classFile.getMethods();
127 | // 遍历方法表
128 | for (MethodInfo methodInfo : methodInfos) {
129 | System.out.println("访问标志和属性:" + FieldAccessFlagUtils
130 | .toFieldAccessFlagsString(methodInfo.getAccess_flags()));
131 | System.out.println("方法名:" + getName(methodInfo.getName_index(), classFile));
132 | System.out.println("方法描述符:"
133 | + getName(methodInfo.getDescriptor_index(), classFile));
134 | System.out.println("属性总数:" + methodInfo.getAttributes_count().toInt());
135 | System.out.println();
136 | }
137 | }
138 | }
139 | ```
140 |
141 | 单元测试结果如下。
142 |
143 | 
144 |
145 | 从单元测试结果可以看出,该单元测试解析的class文件有5个方法,访问权限都是public,其中有一个方法是静态方法。这五个方法的属性表都只有一个属性,实际都是Code属性。这五个方法的方法名称分别是、setA、setB、setC和main。并且从输出的结果还能看出各个方法的方法描述符。
146 |
147 | ---
148 |
149 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_09.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
--------------------------------------------------------------------------------
/chapter02/10.md:
--------------------------------------------------------------------------------
1 | # 解析class文件的属性表
2 |
3 | 字段结构和方法结构也都有属性表,所以要注意不要将这些属性表混在一起理解。但所有属性都有一个通用的结构,这在解析字段那部分已经介绍。因此,解析class文件结构的属性表我们也可以使用通用的属性结构来解析。
4 |
5 | 解析步骤是先从class文件字节缓存中读取两个字节,如果前面的解析工作都正常,那么现在读取到的这两个字节就是该class文件属性表的长度。接着根据长度创建属性表,使用通用属性结构循环解析出每个属性,循环次数为属性的总数。
6 |
7 | class文件结构的属性表解析器AttributesHandler 的实现代码如下。
8 |
9 | ```java
10 | public class AttributesHandler implements BaseByteCodeHandler {
11 |
12 | @Override
13 | public int order() {
14 | return 8;
15 | }
16 |
17 | @Override
18 | public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
19 | classFile.setAttributes_count(new U2(codeBuf.get(), codeBuf.get()));
20 | // 获取属性总数
21 | int len = classFile.getAttributes_count().toInt();
22 | if (len == 0) {
23 | return;
24 | }
25 | // 创建属性表
26 | AttributeInfo[] attributeInfos = new AttributeInfo[len];
27 | classFile.setAttributes(attributeInfos);
28 | for (int i = 0; i < len; i++) {
29 | // 创建属性
30 | AttributeInfo attributeInfo = new AttributeInfo();
31 | attributeInfos[i] = attributeInfo;
32 | // 解析属性
33 | attributeInfo.setAttribute_name_index(new U2(codeBuf.get(), codeBuf.get()));
34 | attributeInfo.setAttribute_length(new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get()));
35 | int attr_len = attributeInfo.getAttribute_length().toInt();
36 | if (attr_len == 0) {
37 | continue;
38 | }
39 | // 解析属性的info项
40 | byte[] bytes = new byte[attr_len];
41 | codeBuf.get(bytes, 0, bytes.length);
42 | attributeInfo.setInfo(bytes);
43 | }
44 | }
45 |
46 | }
47 | ```
48 |
49 | 与其它解析器一样,在编写完成属性表解析器后,我们还需要将class文件结构的属性表解析器AttributesHandler 注册到ClassFileAnalysiser。
50 |
51 | ---
52 |
53 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_10.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
54 |
--------------------------------------------------------------------------------
/chapter02/11.md:
--------------------------------------------------------------------------------
1 | # 解析整个Class文件结构
2 |
3 | 目前为止,我们已经编写完成class文件结构各项的解析器,并且都已经注册到ClassFileAnalysiser,现在ClassFileAnalysiser类持有的解析器有:
4 |
5 | * MagicHandler(魔数解析器)
6 | * VersionHandler(版本号解析器)
7 | * ConstantPoolHandler(常量池解析器)
8 | * AccessFlagsHandler(访问标志解析器)
9 | * ThisAndSuperClassHandler(this_class与super_class解析器)
10 | * InterfacesHandler(接口表解析器)
11 | * FieldHandler(字段表解析球)
12 | * MethodHandler(方法表解析器)
13 | * AttributesHandler(属性表解析器)
14 |
15 | ClassFileAnalysiser类的完整代码如下。
16 |
17 | ```java
18 | public class ClassFileAnalysiser {
19 |
20 | private final static List handlers = new ArrayList<>();
21 |
22 | static {
23 | handlers.add(new MagicHandler());
24 | handlers.add(new VersionHandler());
25 | handlers.add(new ConstantPoolHandler());
26 | handlers.add(new AccessFlagsHandler());
27 | handlers.add(new ThisAndSuperClassHandler());
28 | handlers.add(new InterfacesHandler());
29 | handlers.add(new FieldHandler());
30 | handlers.add(new MethodHandler());
31 | handlers.add(new AttributesHandler());
32 | // 如果解析器是按顺序注册的,那么排序可以忽略
33 | handlers.sort((Comparator.comparingInt(BaseByteCodeHandler::order)));
34 | }
35 |
36 | public static ClassFile analysis(ByteBuffer codeBuf) throws Exception {
37 | ClassFile classFile = new ClassFile();
38 | codeBuf.position(0);
39 | for (BaseByteCodeHandler handler : handlers) {
40 | handler.read(codeBuf, classFile);
41 | }
42 | System.out.println("class文件结构解析完成,解析是否正常(剩余未解析的字节数):" + codeBuf.remaining());
43 | return classFile;
44 | }
45 |
46 | }
47 | ```
48 |
49 | 最后我们还需要对ClassFileAnalysiser进行一次单元测试,重点关注解析完成后,class文件字节缓存中是否还有未读取的字节,如果有则说明某个解析器的某个解析步骤出错了,如果没有,则所有解析器都正常工作。因此,我们在ClassFileAnalysiser 的analysis方法中添加打印剩余未读取的字节总数语句。
50 |
51 | 单元测试代码如下。
52 |
53 | ```java
54 | public class AllHandlerTest {
55 |
56 | @Test
57 | public void test() throws Exception {
58 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("RecursionAlgorithmMain.class");
59 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
60 | }
61 |
62 | }
63 | ```
64 |
65 | 单元测试结果如下图。
66 |
67 | 
68 |
69 | 从输出的结果中可以看出,在所有解析器解析完成后,class文件字节缓存ByteBuffer对象剩余可读的字节数为0,即ByteBuffer对象的读指针与limit指针重合,表示读完,因此解析正常。
70 |
71 | 至此,我们对整个class文件结构的解析工作就已经基本完成了。而对于属性的解析,我们都只是使用通用的解析器解析。在《Java虚拟机规范》Java SE 8版本中,预定义属性就有23个,但本书不会对每个属性都进行详细介绍。
72 |
73 | ---
74 |
75 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_11.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
76 |
--------------------------------------------------------------------------------
/chapter02/12.md:
--------------------------------------------------------------------------------
1 | # 属性二次解析
2 |
3 | 如果想要深入理解某个属性,我们可再对其进行二次解析。如何使用我们编写的项目对class文件结构、字段结构、方法结构的属性表中的属性进行二次解析呢?我们以字段的ConstantValue属性和方法的Code属性为例。
4 |
5 | ## 解析ConstantValue属性
6 |
7 | ConstantValue属性用于通知虚拟机在类或接口初始化阶段为被标志为ACC_STATIC & ACC_FINAL的静态常量字段自动赋值常量。
8 |
9 | > 提示:在接口中声明的字段会被编译器加上ACC_STATIC & ACC_FINAL标志。
10 |
11 | 字段结构的属性表最多只能有一个ConstantValue属性。字段的类型必须是基本数据类型或者String类,因为从常量池中只能引用到基本类型和String类型的字面量。
12 |
13 | ConstantValue属性的结构如下:
14 |
15 | ```java
16 | public class ConstantValue_attribute {
17 | private U2 attribute_name_index;
18 | private U4 attribute_length;
19 | private U2 constantvalue_index;
20 | }
21 | ```
22 |
23 | * attribute_length:由于ConstantValue属性是定长属性,因此attribute_length的值固定为2;
24 | * attribute_name_index:指向常量池中的CONSTANT_Utf8_info常量,该常量表示的字符串为“ConstantValue”;
25 | * constantvalue_index:指向基本数据类型或String类型常量。
26 |
27 | 与常量池的常量结构不同,常量池的常量都有一个tag字段映射到哪种常量结构,而属性表的属性结构只能通过attribute_name_index判断。
28 |
29 | 以一个例子来说明ConstantValue属性的使用场景。
30 |
31 | 在一个接口中定义一个字段并赋值,通过分析其Class文件结构,找到这个字段的属性表,看是否有ConstantValue属性。该测试接口代码如下。
32 |
33 | ```java
34 | public interface TestConstantValueInterface {
35 | int value = 1000;
36 | }
37 | ```
38 |
39 | 现在,我们需要编写一个属性解析工具类,添加支持解析ConstantValue属性的静态方法,以完成属性的二次解析工作,代码如下。
40 |
41 | ```java
42 | public class AttributeProcessingFactory {
43 |
44 | public static ConstantValue_attribute processingConstantValue(AttributeInfo attributeInfo) {
45 | ConstantValue_attribute attribute = new ConstantValue_attribute();
46 | attribute.setAttribute_name_index(attributeInfo.getAttribute_name_index());
47 | attribute.setAttribute_length(attributeInfo.getAttribute_length());
48 | attribute.setConstantvalue_index(new U2(attributeInfo.getInfo()[0], attributeInfo.getInfo()[1]));
49 | return attribute;
50 | }
51 |
52 | }
53 | ```
54 |
55 | * processingConstantValue:方法要求传入一个经过一次解析后的ConstantValue属性对象(AttributeInfo ),方法返回二次解析后生成的ConstantValue属性对象(ConstantValue_attribute)。
56 |
57 | 因为当前只实现了ConstantValue属性的解析,所以单元测试中只对名称为“ConstantValue”的属性进行二次解析,单元测试代码如下。
58 |
59 | ```java
60 | public class ConstantValueAttributeTest {
61 |
62 | @Test
63 | public void testConstantValue() throws Exception {
64 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile(".../TestConstantValueInterface.class");
65 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
66 | // 获取所有字段
67 | FieldInfo[] fieldInfos = classFile.getFields();
68 | for (FieldInfo fieldInfo : fieldInfos) {
69 | // 获取字段的所有属性
70 | AttributeInfo[] attributeInfos = fieldInfo.getAttributes();
71 | if (attributeInfos == null || attributeInfos.length == 0) {
72 | continue;
73 | }
74 | System.out.println("字段:" + classFile.getConstant_pool()
75 | [fieldInfo.getName_index().toInt() - 1]);
76 | // 遍历所有属性
77 | for (AttributeInfo attributeInfo : attributeInfos) {
78 | // 获取属性的名称
79 | U2 name_index = attributeInfo.getAttribute_name_index();
80 | CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info)
81 | classFile.getConstant_pool()[name_index.toInt() - 1];
82 | String name = new String(name_info.getBytes());
83 | // 如果属性名是ConstantValue,则对该属性进行二次解析
84 | if (name.equalsIgnoreCase("ConstantValue")) {
85 | // 属性二次解析
86 | ConstantValue_attribute constantValue = AttributeProcessingFactory.processingConstantValue(attributeInfo);
87 | // 取得constantvalue_index,从常量池中取值
88 | U2 cv_index = constantValue.getConstantvalue_index();
89 | Object cv = classFile.getConstant_pool() [cv_index.toInt() - 1];
90 | // 需要判断常量的类型
91 | if (cv instanceof CONSTANT_Utf8_info) {
92 | System.out.println("ConstantValue:" + cv.toString());
93 | } else if (cv instanceof CONSTANT_Integer_info) {
94 | System.out.println("ConstantValue:" +
95 | ((CONSTANT_Integer_info) cv).getBytes().toInt());
96 | } else if (cv instanceof CONSTANT_Float_info) {
97 | // todo
98 | } else if (cv instanceof CONSTANT_Long_info) {
99 | // todo
100 | } else if (cv instanceof CONSTANT_Double_info) {
101 | // todo
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
108 | }
109 | ```
110 |
111 | 对字段的属性进行二次解析的步骤:
112 |
113 | * 1、解析Java类编译后生成的class文件,以便获取该接口的所有字段信息;
114 | * 2、遍历字段表;
115 | * 3、遍历每个字段的属性表;
116 | * 4、根据属性的名称在常量池中的索引到常量池中取到属性名,再根据属性名去调用对应属性结构的二次解析方法对其进行二次解析。
117 |
118 | 单元测试结果如下图所示。
119 |
120 | 
121 |
122 | 如图所示,字段名为value的字段其属性表有一个ConstantValue属性,常量值是1000。
123 |
124 | ## 解析Code属性
125 |
126 | 一个方法编译后生成的字节码指令存储在方法结构的属性表的Code属性中,但并非每个方法都有Code属性,如声明为native的方法、abstract抽象方法、接口中的非default方法就没有Code属性。实例初始化方法、类或接口的初始化方法都会有Code属性。
127 |
128 | 这一节我们将通过完成对Code属性的二次解析了解Code属性,了解字节码指令是如何存储的在Code属性中的。
129 |
130 | 方法结构的属性表中最多只能有一个Code属性,Code属性是种可变长的属性,属性中包含了方法的字节码指令及相关辅助信息。
131 |
132 | Code属性的结构:
133 |
134 | | ***字段名*** | ***类型*** | ***说明*** |
135 | | ---------------------- | ---------------- | ------------------------------------------------------------ |
136 | | attribute_name_index | u2 | 指向常量池中“Code”常量的索引 |
137 | | attribute_length | u4 | 属性的长度(不包括attribute_name_index和attribute_length占用的长度) |
138 | | max_stack | u2 | 操作数栈的最大深度 |
139 | | max_locals | u2 | 局部变量表的最大深度 |
140 | | code_length | u4 | code数组的大小 |
141 | | code | u1[] | 存储字节码指令 |
142 | | exception_table_length | u2 | 异常表的长度 |
143 | | exception_table | exception[] | 异常表 |
144 | | attributes_count | u2 | 属性总数 |
145 | | attributes | attribute_info[] | 属性表 |
146 |
147 | max_stack与max_locals分别对应操作数栈的大小和局部变量表的大小。
148 |
149 | code项用一个字节数组存储该方法的所有字节码指令,code_length存储该code数组的大小。
150 |
151 | 属性也可以有属性表,attributes项便是Code属性的属性表,在Code属性中,属性表可能存在的属性如LineNumerTable属性、LocalVariableTable属性。
152 |
153 | LineNumerTable属性:被用来映射源码文件中给定的代码行号对应code[]字节码指令中的哪一部分,在调试时用到,在方法抛出异常打印异常栈信息也会用到。
154 |
155 | LocalVariableTable属性:用来描述code[]中的某个范围内,局部变量表中某个位置存储的变量的名称是什么,用于与源码文件中局部变量名做映射。该属性不一定会编译到class文件中,如果没有该属性,那么查看反编译后的java代码将会使用诸如arg0、arg1、arg2之类的名称代替局部变量的名称。
156 |
157 | exception_table用于存储方法中的所有try-catch信息,exception_table_length存储该异常表数组的大小。异常表的每一项都是固定的结构体,异常结构如下表格所示。
158 |
159 | | ***字段名*** | ***类型*** | ***说明*** |
160 | | ------------ | ---------- | ------------------------------------------------------------ |
161 | | start_pc | u2 | try的开始位置,在code[]中的索引 |
162 | | end_pc | u2 | try的结束位置,在code[]中的索引。 |
163 | | handler_pc | u2 | 异常处理的起点,在code[]中的索引。 |
164 | | catch_type | u2 | 为0相当finally块。不为0时,指向常量池中某个CONSTANT_Class_info常量的索引,表示需要捕获的异常,只有[start_pc,end_pc)范围内抛出的异常是指定的类或子类的实例,才会跳转到handler_pc指向的字节码指令继续执行。 |
165 |
166 | 为Code属性结构创建Java类。
167 |
168 | ```java
169 | public class Code_attribute {
170 |
171 | private U2 attribute_name_index;
172 | private U4 attribute_length;
173 | private U2 max_stack;
174 | private U2 max_locals;
175 | private U4 code_length;
176 | private byte[] code;
177 | private U4 exception_table_length;
178 | private Exception[] exception_table;
179 | private U2 attributes_count;
180 | private AttributeInfo[] attributes;
181 | // 异常表中每项的结构
182 | public static class Exception {
183 | private U2 start_pc;
184 | private U2 end_pc;
185 | private U2 handler_pc;
186 | private U2 catch_type;
187 | }
188 |
189 | }
190 | ```
191 |
192 | 对Code属性进行二次解析主要是想拿到字节码信息,属性表和异常表我们就不解析了,异常表在本书第三章再详细介绍。
193 |
194 | 对Code属性二次解析代码如下。
195 |
196 | ```java
197 | public class AttributeProcessingFactory{
198 | public static Code_attribute processingCode(AttributeInfo attributeInfo) {
199 | Code_attribute code = new Code_attribute();
200 | ByteBuffer body = ByteBuffer.wrap(attributeInfo.getInfo());
201 | // 操作数栈大小
202 | code.setMax_stack(new U2(body.get(),body.get()));
203 | // 局部变量表大小
204 | code.setMax_locals(new U2(body.get(),body.get()));
205 | // 字节码数组长度
206 | code.setCode_length(new U4(body.get(),body.get(),body.get(),body.get()));
207 | // 解析获取字节码
208 | byte[] byteCode = new byte[code.getCode_length().toInt()];
209 | body.get(byteCode,0,byteCode.length);
210 | code.setCode(byteCode);
211 | return code;
212 | }
213 | }
214 | ```
215 |
216 | 现在编写单元测试,先将class文件解析为一个ClassFile对象,然后再遍历该ClassFile中的方法表,获取每个方法中的Code属性,再对Code属性进行二次解析。
217 |
218 | 单元测试代码如下。
219 |
220 | ```java
221 | public class CodeAttributeTest {
222 |
223 | @Test
224 | public void testCodeAttribute() throws Exception {
225 | ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("RecursionAlgorithmMain.class");
226 | ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
227 | // 获取方法表
228 | MethodInfo[] methodInfos = classFile.getMethods();
229 | // 遍历方法表
230 | for (MethodInfo methodInfo : methodInfos) {
231 | // 获取方法的属性表
232 | AttributeInfo[] attributeInfos = methodInfo.getAttributes();
233 | if (attributeInfos == null || attributeInfos.length == 0) {
234 | continue;
235 | }
236 | System.out.println("方法:" + classFile.getConstant_pool()
237 | [methodInfo.getName_index().toInt() - 1]);
238 | // 遍历属性表
239 | for (AttributeInfo attributeInfo : attributeInfos) {
240 | // 获取属性的名称
241 | U2 name_index = attributeInfo.getAttribute_name_index();
242 | CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info)
243 | classFile.getConstant_pool()[name_index.toInt() - 1];
244 | String name = new String(name_info.getBytes());
245 | // 对Code属性二次解析
246 | if (name.equalsIgnoreCase("Code")) {
247 | // 属性二次解析
248 | Code_attribute code = AttributeProcessingFactory.processingCode(attributeInfo);
249 | System.out.println("操作数栈大小:" + code.getMax_stack().toInt());
250 | System.out.println("局部变量表大小:" + code.getMax_locals().toInt());
251 | System.out.println("字节码数组长度:" + code.getCode_length().toInt());
252 | System.out.println("字节码:");
253 | for (byte b : code.getCode()) {
254 | System.out.print((b & 0xff) + " ");
255 | }
256 | System.out.println("\n");
257 | }
258 | }
259 | }
260 | }
261 |
262 | }
263 | ```
264 |
265 | 例子中使用到的RecursionAlgorithmMain类代码如下。
266 |
267 | ```java
268 | public class RecursionAlgorithmMain {
269 |
270 | private static volatile int value = 0;
271 |
272 | static int sigma(int n) {
273 | value = n;
274 | System.out.println("current 'n' value is " + n);
275 | return n + sigma(n + 1);
276 | }
277 |
278 | public static void main(String[] args) throws IOException {
279 | new Thread(() -> sigma(1)).start();
280 | System.in.read();
281 | System.out.println(value);
282 | }
283 |
284 | }
285 | ```
286 |
287 | 以RecursionAlgorithmMain的sigma方法为例,首先使用javap查看sigma方法的字节码,目的是对比我们编写的解析工具解析的结果,验证解析结果是否正确。
288 |
289 | 
290 |
291 | 使用编写好的解析工具解析sigma方法的Code属性,结果如下图所示。
292 |
293 | 
294 |
295 | 这里我们还需要将字节码转为十六进制输出,方便与字节码指令表对照。
296 |
297 | byte[]转十六进制字符串工具类的实现代码如下。
298 |
299 | ```java
300 | public class HexStringUtils {
301 |
302 | public static String toHexString(byte[] codes) {
303 | StringBuilder codeStrBuild = new StringBuilder();
304 | int i=0;
305 | for (byte code : codes) {
306 | // toHexString将字节转十六进制,实现略...
307 | codeStrBuild.append(toHexString(code)).append(" ");
308 | if(++i==9){
309 | i=0;
310 | codeStrBuild.append("\n");
311 | }
312 | }
313 | return codeStrBuild.toString();
314 | }
315 |
316 | }
317 | ```
318 |
319 | 将字节码转为十六进制字符串后输出的结果如下图所示。
320 |
321 | 
322 |
323 | 其中,字节码部分的0x1A对应iload_0指令,0xB3对应putstatic指令,由于putstatic指令需要一个操作数,因此0xB3后面的0x00 02就是指令所需的操作数。
324 |
325 | ---
326 |
327 | 发布于:2021 年 07 月 24 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter02_12.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
--------------------------------------------------------------------------------
/chapter02/13.md:
--------------------------------------------------------------------------------
1 | # 本章小结
2 |
3 | 本章我们通过编写一个简单的Class文件结构解析项目,对Class文件结构有了深刻的了解。
4 |
5 | 了解Class文件结构是看懂Java虚拟机字节码指令的前提条件,也是学习使用ASM 工具操作类的字节码的前提条件。
6 |
7 |
--------------------------------------------------------------------------------
/chapter02/README.md:
--------------------------------------------------------------------------------
1 | # 深入理解Class文件结构
2 |
3 | Java代码并不能被虚拟机直接解释执行,任何想运行在JVM虚拟机上的语言,都必须通过编译器编译为虚拟机所能识别的字节码,而字节码文件后缀名为.class,并且每个文件存储的内容都有固定的结构,即Class文件结构。class文件是二进制文件,在程序运行时由类加载器加载解析生成类元数据存放在方法区中,最后由虚拟机解释执行。本章我们深入学习Class文件结构,虽然内容有些枯燥,但掌握Class文件结构是入门JVM字节码的必修课。
4 |
5 | 本章内容安排如下:
6 |
7 | * [class文件结构](00.md)
8 | * [动手实现class文件结构解析器](01.md)
9 | * [解析魔数](02.md)
10 | * [解析版本号](03.md)
11 | * [解析常量池](04.md)
12 | * [解析class文件的访问标志](05.md)
13 | * [解析this与super符号引用](06.md)
14 | * [解析接口表](07.md)
15 | * [解析字段表](08.md)
16 | * [解析方法表](09.md)
17 | * [解析class文件的属性表](10.md)
18 | * [解析整个Class文件结构](11.md)
19 | * [属性二次解析](12.md)
20 | * [本章小结](13.md)
21 |
22 |
--------------------------------------------------------------------------------
/chapter02/images/chapter02_01_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_01_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_01_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_01_02.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_02_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_02_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_03_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_03_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_03_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_03_02.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_04_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_04_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_04_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_04_02.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_05_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_05_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_05_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_05_02.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_05_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_05_03.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_06_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_06_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_06_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_06_02.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_07_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_07_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_07_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_07_02.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_08_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_08_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_09_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_09_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_11_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_11_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_12_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_12_01.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_12_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_12_02.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_12_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_12_03.png
--------------------------------------------------------------------------------
/chapter02/images/chapter02_12_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter02/images/chapter02_12_04.png
--------------------------------------------------------------------------------
/chapter03/01.md:
--------------------------------------------------------------------------------
1 | # 从Hello Word出发
2 |
3 | 与学习一门编程语言一样,我们从使用Java代码编写一个Hello World程序、使用javap[^1]工具查看Hello World程序的字节码并分析每条字节码指令的执行过程开始,入门Java虚拟机字节码指令。
4 |
5 | 使用Java代码编写的Hello World程序,代码如下。
6 |
7 | ```java
8 | public class HelloWord {
9 | public static void main(String[] args) {
10 | System.out.println("Hello Word");
11 | }
12 | }
13 | ```
14 |
15 | 使用javap命令输出Hello World程序的字节码如下
16 |
17 | ```java
18 | public static void main(java.lang.String[]);
19 | Code:
20 | 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21 | 3: ldc #3 // String Hello Word
22 | 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23 | 8: return
24 | ```
25 |
26 | 第一条指令是getstatic,对应的操作码是0xB2,该指令需要一个操作数,该操作数必须是常量池中某个CONSTANT_Fieldref_info常量的索引。在本例中,该指令表示获取System的out静态字段,该静态字段的类型为java.io.PrintStream。该指令执行完成后,操作数栈顶存放的就是System的out静态字段的引用,如下图所示。
27 |
28 | 
29 |
30 | 第二条指令是ldc,对应的操作码是0x12,该指令也需要一个操作数,值为常量池中的某个CONSTANT_String_info常量的索引。在本例中,其作用是将常量池中的“Hello Word”字符串的引用放入操作数栈顶。该指令执行完后,操作数栈顶存放的就是字符串“Hello Word”的引用,如下图所示。
31 |
32 | 
33 |
34 | 第三条指令是invokevirtual,对应的操作码是0xB6,该指令也需要一个操作数,值为常量池中某个CONSTANT_Methodref_info常量的索引。在本例中,它的作用是调用PrintStream对象的println方法。
35 |
36 | invokevirtual指令要求将调用目标方法所需要的参数压入栈顶,除静态方法、类初始化方法之外,每个类的成员方法以及类的实例初始化方法的第一个参数都是this引用,在java代码中不需要传递,由编译器编译后生成。
37 |
38 | 在本例中invokevirtual指令执行之前,操作数栈必须存在一个System.out对象的引用,和println方法所需的参数,并且顺序是严格要求的,正是前面getstatic、ldc两条指令执行的结果。invokevirtual指令执行完成后操作栈的变化如下图所示。
39 |
40 | 
41 |
42 | ---
43 |
44 | [^1]: javap是JDK提供的命令行工具,是专门用于分析class文件字节码的工具
45 |
46 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_01.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
47 |
48 |
--------------------------------------------------------------------------------
/chapter03/02.md:
--------------------------------------------------------------------------------
1 | # 字段与方法描述符
2 |
3 | 描述符本是第二章的内容,没有在第二章详解是考虑到放在第二章可能不容易理解,因此关于描述符放在本章作为补充内容讲解。
4 |
5 | 描述符分字段描述符和方法描述符,是一个描述字段类型或方法类型的字符串。在Hello Word例子中,我们已经看到方法描述符的使用。
6 |
7 | Class文件结构的字段表中每个字段都有一个描述符索引,该索引指向常量池中表示该字段的类型描述符的CONSTANT_Utf8_info常量。参照《Java虚拟机规范》,类型描述符规范如下。
8 |
9 | ```
10 | BaseType: one of
11 | B C D F I J S Z
12 | ObjectType:
13 | L ClassName ;
14 | ArrayType:
15 | [ ComponentType
16 | ComponentType:
17 | FieldType(BaseType、ObjectType、ArrayType)
18 | ```
19 |
20 | 对象类型(ObjectType)的ClassName表示一个类或接口的名称,如String类型的类型描述符为“Ljava/lang/String;”;数组类型(ArrayType)的ComponentType表示BaseType、ObjectType、ArrayType的其中一种,比如字符串数组的类型描述符为“[Ljava/lang/String;”。类型与字段描述符的映射如下表所示。
21 |
22 | | *字段描述符* | *类型* | ***含义*** |
23 | | ------------- | --------- | ------------------- |
24 | | B | byte | 基本数据类型byte |
25 | | C | char | 基本数类型char |
26 | | D | double | 基本数据类型double |
27 | | F | float | 基本数据类型float |
28 | | I | int | 基本数据类型int |
29 | | J | long | 基本数据类型long |
30 | | L ClassName ; | reference | 引用类型 |
31 | | S | short | 基本数据类型short |
32 | | Z | boolean | 基本数据类型boolean |
33 | | [ FieldType | reference | 数组 |
34 |
35 | 字段描述符不仅用于描述字段的类型,也用于描述局部变量的类型。
36 |
37 | 方法描述符包含0个或多个参数描述符和一个返回值描述符。参数描述符表示该方法所能接受的参数类型,返回值描述符表示该方法的返回值类型。JVM根据方法名称和方法描述符在指定的类中寻找一个符合条件的方法来调用。方法的重载正是利用方法描述符区分不同的方法。
38 |
39 | 方法描述符格式:
40 |
41 | ```
42 | MethodDescriptor = ({parameterDescriptor})ReturnDescriptor
43 | ```
44 |
45 | 如main方法的方法描述符为:“([Ljava/lang/String;)V”。
46 |
47 | 如果方法无返回值,则返回值描述符为:“V”。
48 |
49 | ---
50 |
51 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_02.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
52 |
53 |
--------------------------------------------------------------------------------
/chapter03/03.md:
--------------------------------------------------------------------------------
1 | # 读写局部变量表与操作数栈
2 |
3 | 读写局部变量表与操作数栈就是将局部变量push进操作数栈与将操作数栈的栈顶元素存储到局部变量表的操作。
4 |
5 | 将局部变量表中的元素放入操作数栈只能放入栈顶,而将操作数栈的栈顶元素存到局部变量表是可以指定存到局部变量表的位置的,这个过程其实就是给局部变量赋值。
6 |
7 | 与汇编语言有相似之处就是字节码指令不能直接将局部变量表的某个元素赋值给局部变量表的另一个元素,必须通过操作数栈完成。这也是为什么说字节码指令集是基于栈的指令集。
8 |
9 | 局部变量表的大小与操作数栈的深度是在Java代码编译成class字节码文件时就已经确定,使用javap -v命令可以查看当前class文件中每个方法的操作数栈深度与局部变量表大小,如下图所示。
10 |
11 | 
12 |
13 | 以一个给局部变量赋值的例子理解读写操作数栈与局部变量表,代码如下。
14 |
15 | ```java
16 | public static void main(String[] args) {
17 | int a=10,b=20;
18 | int c=b;
19 | b=a;
20 | }
21 | ```
22 |
23 | 使用javap命令输出这段代码的字节码如下。
24 |
25 | ```java
26 | public static void main(java.lang.String[]);
27 | Code:
28 | stack=1, locals=4, args_size=1
29 | 0: bipush 10
30 | 2: istore_1
31 | 3: bipush 20
32 | 5: istore_2
33 | 6: iload_2
34 | 7: istore_3
35 | 8: iload_1
36 | 9: istore_2
37 | 10: return
38 | ```
39 |
40 | 结果显示,局部变量表的大小为4,操作数栈的大小是1。局部变量表的每个Slot分别用于存储main方法中类型为String数组的参数的引用,以及变量a、b、c的值。
41 |
42 | 为什么局部变量表的大小为4,操作数栈的大小只是1呢?我们带着这个疑问分析这些字节码指令的执行过程。
43 |
44 | 通过javap查看字节码我们发现,在字节码指令的前面都会标有数字,这些数字是每条指令在Code属性中code[]数组的索引,也可称为下标或者偏移量。把这些指令的索引连在一起看,发现不是连续的,这是因为有些指令需要操作数,在需要操作数的指令后面会存储该指令执行所需的操作数,所以指令前面的数字不是连续的。
45 |
46 | 现在我们分析案例中字节码指令的执行过程。
47 |
48 | 1、偏移量为0的指令为bipush指令,该指令是将一个立即数10放入操作数栈顶。该指令执行完后,操作数栈与局部变量表的变化如下图所示。
49 |
50 | 
51 |
52 | 2、偏移量为2的指令是istore_1,该指令是将当前操作数栈顶的元素存储到局部变量表索引为1的Slot(第二个Slot)。该指令执行完成后,局部变量表索引为1的Slot存储整数10(即a=10),操作数栈顶的元素已经出栈,此时操作数栈为空,如下图所示。
53 |
54 | 
55 |
56 | 3、偏移量为3的字节码指令为bipush指令,该指令的作用是将立即数20放入操作数栈顶。该指令执行完成后,局部变量a的值还是10,操作数栈顶存储立即数20,如下图所示。
57 |
58 | 
59 |
60 | 4、偏移量为5的字节码指令为istore_2,该指令不需要操作数,作用是将当前操作数栈的栈顶元素存储到局部变量表索引为2的Slot。该指令执行完成后,a=10,b=20,操作数栈顶的元素出栈,操作数栈为空,如下图所示。
61 |
62 | 
63 |
64 | 5、偏移量为6的字节码指令为aload_2,该指令不需要操作数,作用是将局部变量表索引为2的元素放入操作数栈的栈顶。该指令执行完成后,a=10,b=20,操作数栈的栈顶存储整数20,如下图所示。
65 |
66 | 
67 |
68 | 6、偏移量为7的字节码指令为istore_3,该指令的作用是将当前操作数栈的栈顶元素存储到局部变量表索引为3的Slot。偏移量为6和7的两条指令完成将局部变量b赋值给局部变量c。该指令执行完成后,a=10,b=20,c=20,操作数栈顶元素出栈,操作数栈为空,如下图所示。
69 |
70 | 
71 |
72 | 7、偏移量为8和9的两条字节码指令分别为iload_1和istore_2,这两条字节码指令的作用是完成局部变量a赋值给局部变量b的操作,这两条指令执行完成后,局部变量与操作数栈的变化如下图所示。
73 |
74 | 
75 |
76 | 从整个方法的字节码指令执行过程来看,该方法执行所需要占用操作数栈的Slot最多只有一个,因此该方法的操作数栈的大小被编译器设置为1,不浪费任何空间。而方法参数args和方法体内声明的局部变量a、b、c它们的作用域是整个方法,因此需要为args、a、b、c都分配一个局部变量槽位,局部变量表的大小被编译器设置为4。
77 |
78 | 我们通过这个例子了解了局部变量表和操作数栈的读写,其中iload_xx指令就是将局部变量表的元素放入栈顶,istore_xx指令就是将当前操作数栈的栈顶元素存储到局部变量表。xx是局部变量表的索引,局部变量表是一个数组,需要通过索引访问数组中的元素。iload_xx和istore_xx对应的字节码指令如下表所示。
79 |
80 | | ***指令*** | ***指令码*** | ***说明*** |
81 | | ---------- | ------------ | ---------------------------------------- |
82 | | iload_0 | 0x1A | 将局部变量0作为int类型值放入操作数栈 |
83 | | iload_1 | 0x1B | 将局部变量1作为int类型值放入操作数栈 |
84 | | iload_2 | 0X1C | 将局部变量2作为int类型值放入操作数栈 |
85 | | iload_3 | 0X1D | 将局部变量3作为int类型值放入操作数栈 |
86 | | istore_0 | 0x3B | 将栈顶元素作为int类型值保存到局部变量0中 |
87 | | istore_1 | 0x3C | 将栈顶元素作为int类型值保存到局部变量1中 |
88 | | istore_2 | 0x3D | 将栈顶元素作为int类型值保存到局部变量2中 |
89 | | istore_3 | 0x3E | 将栈顶元素作为int类型值保存到局部变量3中 |
90 |
91 | iload_xx和istore_xx只能访问局部变量表索引为0到3的元素,那假如局部变量表的长度超过4呢,没有iload_4指令?是的,没有iload_4指令,只能使用iload和istore指令。
92 |
93 | 其实不管访问局部变量表的哪个位置,都可以通过iload和istore指令访问,那为什么还要iload_xx和istore_xx指令呢。因为iload和istore指令需要操作数,而iload_xx和istore_xx不需要操作数,在编译后能减少Code属性的code[]字节数组的大小,而且大多数方法都不会超过3个参数。因为非静态方法的局部变量表的下标0用于保存this引用,所以是4减1个参数。
94 |
95 | 例子中的iload_xx指令和istore_xx指令只能操作Java中int类型的变量,与之对应的还有操作float类型的fload_xx和fstore_xx指令,操作long类型的lload_xx和lstore_xx指令,操作double类型的dload_xx和dstore_xx指令,以及操作引用类型的aload_xx和astore_xx指令,还有fload、lload、dload、aload指令。
96 |
97 | 各类型的加载指令
98 |
99 | | ***指令*** | ***指令码*** | ***操作数*** | ***说明*** |
100 | | ---------- | ------------ | ----------------------- | ------------------------------------------------------------ |
101 | | iload | 0x15 | index,一个字节,0~255 | 将局部变量表index位置的元素作为int类型值放入操作数栈 |
102 | | fload | 0x17 | index,一个字节,0~255 | 将局部变量表index位置的元素作为fload类型值放入操作数栈 |
103 | | lload | 0x16 | index,一个字节,0~255 | 将局部变量表index位置的元素作为long类型值放入操作数栈 |
104 | | dload | 0x18 | index,一个字节,0~255 | 将局部变量表index位置的元素作为double类型值放入操作数栈 |
105 | | aload | 0x19 | index,一个字节,0~255 | 将局部变量表index位置的元素作为引用类型放入操作数栈 |
106 | | saload | 0x35 | index,一个字节,0~255 | 将局部变量表index位置的元素作为short类型值放入操作数栈 |
107 | | caload | 0x34 | index,一个字节,0~255 | 将局部变量表index位置的元素作为char类型值放入操作数栈 |
108 | | baload | 0x33 | index,一个字节,0~255 | 将局部变量表index位置的元素作为boolean类型值放入操作数栈 |
109 | | aaload | 0x32 | | 将局部变量表index位置的元素作为引用类型的数组引用放入操作数栈 |
110 |
111 | 各类型的存储指令
112 |
113 | | ***指令*** | ***指令码*** | ***操作数*** | ***说明*** |
114 | | ---------- | ------------ | ----------------------- | ------------------------------------------------------------ |
115 | | istore | 0x36 | index,一个字节,0~255 | 将操作数栈栈顶的元素作为int类型放入局部变量表index位置 |
116 | | fstore | 0x38 | index,一个字节,0~255 | 将操作数栈栈顶的元素作为float类型放入局部变量表index位置 |
117 | | lstore | 0x37 | index,一个字节,0~255 | 将操作数栈栈顶的元素作为long类型放入局部变量表index位置 |
118 | | dstore | 0x39 | index,一个字节,0~255 | 将操作数栈栈顶的元素作为double类型放入局部变量表index位置 |
119 | | astore | 0x3a | index,一个字节,0~255 | 将操作数栈栈顶的元素作为引用类型放入局部变量表index位置 |
120 | | sastore | 0x56 | index,一个字节,0~255 | 将操作数栈栈顶的元素作为short类型放入局部变量表index位置 |
121 | | castore | 0x55 | index,一个字节,0~255 | 将操作数栈栈顶的元素作为char类型放入局部变量表index位置 |
122 | | bastore | 0x54 | index,一个字节,0~255 | 将操作数栈栈顶的元素作为boolean类型放入局部变量表index位置 |
123 | | aastore | 0x53 | | 将操作数栈栈顶的元素作为引用类型的数组引用放入局部变量表index位置 |
124 |
125 | 案例中还用到了bipush指令,bipush用于将一个int型的立即数放入操作数栈的栈顶,该指令属于操作常量与立即数入栈一类的指令。除bipush之外还有将null放入操作数栈栈顶的iconst_null指令、将常量池中的常量值放入操作数栈顶的指令ldc。还有iconst_xx指令,xx可取值为-1到5,作用是将-1~5的立即数放入操作数栈顶。还有fconst_xx、dconst_xx、lconst_xx,xx代表0或1,这些指令分别是将立即数1、2作为浮点数或者双精度浮点数、长整型放入操作数栈顶,不过这几条指令不常用。
126 |
127 | 在使用将立即数放入操作数栈栈顶的这类指令时,如果立即数大于等于-1且小于等于5,可使用对应的iconst_xx指令,如果立即数超过5,只能使用bipush指令。这也是很多人第一次接触字节码指令时很是不理解的,为什么int a=3与int a=10反编译后字节码指令会不同的原因。
128 |
129 | 在分析案例过程中,我们发现字节码指令都是给局部变量表这个数组的元素赋值,看不出来是给变量a、b还是c赋值,这是因为Java虚拟机执行字节码指令并不关心局部变量表索引为1的元素在源码中叫什么名字。那我们怎么知道某个位置存储的是局部变量a、b、还是c呢?这就需要通过查看LocalVariableTable属性了。
130 |
131 | 使用javap命令输出此例子的LocalVariableTable属性如下。
132 |
133 | ```java
134 | LocalVariableTable:
135 | Start Length Slot Name Signature
136 | 0 11 0 args [Ljava/lang/String;
137 | 3 8 1 a I
138 | 6 5 2 b I
139 | 8 3 3 c I
140 | ```
141 |
142 | 第一行:局部变量的作用范围为[0,11) [^1],使用局部变量表中的第一个Slot存储,该局部变量的名称为“args”,变量的类型签名为“[Ljava/lang/String”;
143 |
144 | 第二行:局部变量的作用范围为[3,11),使用局部变量表中的第二个Slot存储,该局部变量的名称为“a”,类型签名为“ I”;
145 |
146 | 第三行:局部变量的作用范围为[6,11),使用局部变量表中的第三个Slot存储,该局部变量的名称为“b”,类型签名为“ I”;
147 |
148 | 第四行:局部变量的作用范围为[8,11),使用局部变量表中的第四个Slot存储,该局部变量的名称为“c”,类型签名为“ I”。
149 |
150 | ---
151 |
152 | [^1]: 表示在偏移量为0至偏移量为11的字节码指令范围内,指定的Slot存储的变量的变量名为“args”,也是限定局部变量“args”的作用范围。
153 |
154 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_03.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
155 |
156 |
--------------------------------------------------------------------------------
/chapter03/05.md:
--------------------------------------------------------------------------------
1 | # 访问静态字段与静态方法
2 |
3 | 与非静态方法的调用和非静态字段的访问不同,获取静态字段、修改静态字段、调用静态方法不需要一个该类型的对象引用作为隐式参数,且静态方法的局部变量表不会存储this引用。
4 |
5 | 静态字段的初始赋值由编译器编译后在类初始化方法中生成赋值的字节码指令,而被声明为final的静态字段初始赋值则在类加载的准备阶段赋值。
6 |
7 | 读写静态字段的字节码指令是getstatic与putstatic,这两条指令都要求一个操作数,操作数的值为常量池中某个CONSTANT_Fieldref_info常量的索引。getstatic指令的操作码为0xB2,putstatic指令的操作码为0xB3。
8 |
9 | 读写静态字段的案例代码如下。
10 |
11 | ```java
12 | public class StaticFieldMain {
13 | static String name;
14 |
15 | public static void main(String[] args) {
16 | name = "wujiuye";
17 | System.out.println(name);
18 | }
19 | }
20 | ```
21 |
22 | 使用javap命令输出这段代码的字节码如下。
23 |
24 | ```java
25 | public static void main(java.lang.String[]);
26 | descriptor: ([Ljava/lang/String;)V
27 | Code:
28 | 0: ldc #2 // String wujiuye
29 | 2: putstatic #3 // Field name:Ljava/lang/String;
30 | 5: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
31 | 8: getstatic #3 // Field name:Ljava/lang/String;
32 | 11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33 | 14: return
34 | ```
35 |
36 | 偏移量为0和2的字节码指令完成为静态字段name赋值,先使用ldc字节码指令将putstatic指令所需要的参数放入操作数栈顶,putstatic指令将栈顶的元素赋值给类的静态字段。
37 |
38 | 调用静态方法的案例代码如下。
39 |
40 | ```java
41 | public class StaticMethodMain {
42 | static void show(String msg){
43 | System.out.println(msg);
44 | }
45 | public static void main(String[] args) {
46 | StaticMethodMain.show("hello word!");
47 | }
48 | }
49 | ```
50 |
51 | 例子中,在main方法调用show静态方法,调用show方法需要传递一个参数,在show方法中打印main方法传递的参数。对应的字节码如下。
52 |
53 | ```java
54 | static void show(java.lang.String);
55 | descriptor: (Ljava/lang/String;)V
56 | Code:
57 | 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
58 | 3: aload_0
59 | 4: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60 | 7: return
61 |
62 | public static void main(java.lang.String[]);
63 | descriptor: ([Ljava/lang/String;)V
64 | Code:
65 | 0: ldc #4 // String hello word!
66 | 2: invokestatic #5 // Method show:(Ljava/lang/String;)V
67 | 5: return
68 | ```
69 |
70 | main方法中,偏移量为0和2的字节码指令完成调用show方法,ldc指令将调用show方法所需的参数放入操作数栈的栈顶。方法需要多少个参数就将多少个参数放入操作数栈顶,如果传null则使用aconst_null指令,aconst_null指令的操作码为0x01。调用静态方法的指令是invokestatic,指令的操作码为0xB8,该指令需要一个操作数,操作数的值必须是常量池中某个CONSTANT_Methodref_info常量的索引。在show方法中,偏移量为3的aload_0指令获取到的局部变量不再是this,而是方法的第一个参数。
71 |
72 | ---
73 |
74 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_05.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
75 |
76 |
--------------------------------------------------------------------------------
/chapter03/06.md:
--------------------------------------------------------------------------------
1 | # 调用方法的四条指令
2 |
3 | 在Java字节码指令集中有四条调用方法的指令,严格来说是五条,在JDK1.7中加入了invokedynamic指令。常用的四条方法调用指令如下表所示。
4 |
5 | | ***指令的操作码*** | ***指令的助记符*** | ***操作数*** | ***描述*** |
6 | | ------------------ | ------------------ | --------------------- | ---------------------------------- |
7 | | 0xB7 | invokespecial | index,两个字节 | 调用私有方法、父类方法、方法 |
8 | | 0xB6 | invokevirtual | index,两个字节 | 调用虚方法 |
9 | | 0xB8 | invokestatic | index,两个字节 | 调用静态方法 |
10 | | 0xB9 | invokeinterface | index,两个字节count0 | 调用接口方法 |
11 |
12 | 这四条方法调用指令都需要一个执向常量池中某个CONSTANT_Methodref_info常量的操作数,即告诉jvm,该指令调用的是哪个类的哪个方法。除invokestatic指令外,其余指令都至少需要一个参数,这个参数就是隐式参数this。
13 |
14 | 其中invokestatic指令用于调用一个静态方法,只要调用的方法是静态方法就必须要使用这条指令。invokeinterface指令用于调用接口方法,运行时再根据对象的类型找出一个实现该接口方法的适合方法进行调用。invokespecial指令用于调用实例初始化方法、私有方法和父类的子类可访问的方法。invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派。
15 |
16 | ---
17 |
18 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_06.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
19 |
20 |
--------------------------------------------------------------------------------
/chapter03/07.md:
--------------------------------------------------------------------------------
1 | # 不同类型返回值对应的指令
2 |
3 | 与读写局部变量表和操作数栈一样,方法返回指令也对应有多条指令,每种基本数据类型对应一条指令,引用类型对应areturn指令,如下表所示。
4 |
5 | | ***指令的操作码*** | ***指令的助记符*** | ***操作数*** | ***描述*** |
6 | | ------------------ | ------------------ | ------------ | ---------------- |
7 | | 0xAC | ireturn | | 返回int类型值 |
8 | | 0xAD | lreturn | | 返回long类型值 |
9 | | 0xAE | freturn | | 返回float类型值 |
10 | | 0xAF | dreturn | | 返回double类型值 |
11 | | 0xB0 | areturn | | 返回引用类型值 |
12 | | 0xB1 | return | | 无返回值返回 |
13 |
14 | return指令用于无返回值方法,在java代码中,void方法我们可能不会写return,但编译器会自动加上return指令。以返回值为int、long基本数据类型为例,对应java代码如下。
15 |
16 | ```java
17 | public static int getInt(){
18 | return 1000000000;
19 | }
20 | public static long getLong(){
21 | return 1000000000000000000L;
22 | }
23 | ```
24 |
25 | 使用javap命令输出这两个方法的字节码如下。
26 |
27 | ```java
28 | public static int getInt();
29 | descriptor: ()I
30 | Code:
31 | 0: ldc #2 // int 1000000000
32 | 2: ireturn
33 |
34 | public static long getLong();
35 | descriptor: ()J
36 | Code:
37 | 0: ldc2_w #3 // long 1000000000000000000l
38 | 3: lreturn
39 | ```
40 |
41 | 验证返回值类型为引用类型时使用的返回指令为areturn,代码如下。
42 |
43 | ```java
44 | public static User getObject(){
45 | return new User();
46 | }
47 | public static int[] getArray(){
48 | int[] array = new int[]{};
49 | return array;
50 | }
51 | ```
52 |
53 | 使用javap查看编译后的字节码如下。
54 |
55 | ```java
56 | public static com.wujiuye.asmbytecode.book.third.model.User getObject();
57 | descriptor: ()Lcom/wujiuye/asmbytecode/book/third/model/User;
58 | Code:
59 | 0: new #5 // class com/wujiuye/asmbytecode/book/third/model/User
60 | 3: dup
61 | 4: invokespecial #6 // Method com/wujiuye/asmbytecode/book/third/model/User."":()V
62 | 7: areturn
63 |
64 | public static int[] getArray();
65 | descriptor: ()[I
66 | Code:
67 | 0: iconst_0
68 | 1: newarray int
69 | 3: astore_0
70 | 4: aload_0
71 | 5: areturn
72 | ```
73 |
74 | 从该例子可以看出,无论是数组还是对象,都是使用areturn指令。
75 |
76 | ---
77 |
78 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_07.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
79 |
80 |
--------------------------------------------------------------------------------
/chapter03/08.md:
--------------------------------------------------------------------------------
1 | # 创建数组与访问数组元素
2 |
3 | 数组的创建和访问也是我们编程中经常用到的。在java中,如果访问一个数组的索引超过数组的大小,会被虚拟机检测到,并抛出数组越界异常,这是因为在虚拟机执行访问数组的指令时,数组的大小已经是确定的,可以在执行指令之前检测索引是否超过数组大小。我们分基本数据类型数组、引用类型数组两种不同的数组,分别看下如何在字节码层面实现创建数组与访问数组元素。
4 |
5 | ## 基本数据类型数组
6 |
7 | 基本数据类型的数组创建与访问案例代码如下。
8 |
9 | ```java
10 | public static void baseArray() {
11 | int[] array = new int[2];
12 | array[1] = 100;
13 | int a = array[1];
14 | }
15 | ```
16 |
17 | 使用javap命令输出baseArray方法的字节码如下。
18 |
19 | ```java
20 | public static void baseArray();
21 | Code:
22 | 0: iconst_2
23 | 1: newarray int
24 | 3: astore_0
25 | 4: aload_0
26 | 5: iconst_1
27 | 6: bipush 100
28 | 8: iastore
29 | 9: aload_0
30 | 10: iconst_1
31 | 11: iaload
32 | 12: istore_1
33 | 13: return
34 | ```
35 |
36 | 偏移量为0和1的字节码指令完成数组的创建,iconst_2指令设置数组的大小为2,将立即数2放入操作数栈顶,newarray指令需要一个操作数,操作数占一个字节,如创建int类型的数组则操作数的值为10,创建long类型的数组则操作数为11等。
37 |
38 | 偏移量为4、5、6、8的这四条指令分别是将数组引用放入操作数栈顶、将访问数组的索引1放入操作数栈顶、将立即数100放入栈顶、最后iastore指令将栈顶int类型值100保存到int类型数组的索引为1的位置。偏移量为9、10、11这三条指令是读取int类型数组索引为1的元素放入操作数栈的栈顶,iaload指令将int类型数组指定索引位置处的元素放入操作数栈的栈顶。
39 |
40 | iaload指令和iastore指令都不需要操作数。iastore指令执行之前,要求当前操作数栈必须存在int类型数组的引用、访问数组的索引、将要赋给数组元素的值,这三项是按顺序入栈的。iaload指令执行之前,要求当前操作数栈必须存在int类型数组的引用、访问数组的索引,按顺序入栈,返回值存放在操作数栈的栈顶。
41 |
42 | 访问基本数据类型数组的指令还有,对应long类型的laload和lastore指令,对应float类型的faload和fastore指令,对应double类型的daload和dastore指令,对应char类型的caload和castore指令,对应short类型的saload和sastore指令。
43 |
44 | | ***指令的操作码*** | ***指令的助记符*** | ***操作数*** | ***描述*** |
45 | | ------------------ | ------------------ | ------------ | -------------------------------------------------------- |
46 | | 0x2E | iaload | | 将int类型数组中指定索引的元素放入操作数栈顶 |
47 | | 0x2F | laload | | 将long类型数组中指定索引的元素放入操作数栈顶 |
48 | | 0x30 | faload | | 将float类型数组中指定索引的元素放入操作数栈顶 |
49 | | 0x31 | daload | | 将double类型数组中指定索引的元素放入操作数栈顶 |
50 | | 0x34 | caload | | 将char类型数组中指定索引的元素放入操作数栈顶 |
51 | | 0x35 | saload | | 将short类型数组中指定索引的元素放入操作数栈顶 |
52 | | 0x33 | baload | | 将boolean类型数组中指定索引的元素放入操作数栈顶 |
53 | | 0x4F | iastore | | 将栈顶int类型值保存到指定int类型数组的指定索引处 |
54 | | 0x50 | lastore | | 将栈顶long类型值保存到指定long类型数组的指定索引处 |
55 | | 0x51 | fastore | | 将栈顶float类型值保存到指定int类型数组的指定索引处 |
56 | | 0x52 | dastore | | 将栈顶double类型值保存到指定double类型数组的指定索引处 |
57 | | 0x55 | castore | | 将栈顶char类型值保存到指定char类型数组的指定索引处 |
58 | | 0x56 | sastore | | 将栈顶short类型值保存到指定short类型数组的指定索引处 |
59 | | 0x54 | bastore | | 将栈顶boolean类型值保存到指定boolean类型数组的指定索引处 |
60 |
61 | ## 引用类型数组
62 |
63 | 引用类型数组的创建与访问案例代码如下。
64 |
65 | ```java
66 | public static void objArray(){
67 | User[] users = new User[2];
68 | users[0] = new User();
69 | users[1] = users[0];
70 | }
71 | ```
72 |
73 | 使用javap命令输出objArray方法的字节码如下。
74 |
75 | ```java
76 | public static void objArray();
77 | Code:
78 | 0: iconst_2
79 | 1: anewarray #2 // class com/wujiuye/asmbytecode/book/third/model/User
80 | 4: astore_0
81 | 5: aload_0
82 | 6: iconst_0
83 | 7: new #2 // class com/wujiuye/asmbytecode/book/third/model/User
84 | 10: dup
85 | 11: invokespecial #3 // Method com/wujiuye/asmbytecode/book/third/model/User."":()V
86 | 14: aastore
87 | 15: aload_0
88 | 16: iconst_1
89 | 17: aload_0
90 | 18: iconst_0
91 | 19: aaload
92 | 20: aastore
93 | 21: return
94 | ```
95 |
96 | 与访问基本数据类型数组的逻辑差不多,创建数组使用的是anewarray指令,获取数组指定索引位置处的元素使用aaload指令,将对象的引用写到数组指定索引位置处使用aastore指令。
97 |
98 | 引用类型数组访问指令如下表所示。
99 |
100 | | ***指令的操作码*** | ***指令的助记符*** | ***操作数*** | ***描述*** |
101 | | ------------------ | ------------------ | ------------ | -------------------------------------------------- |
102 | | 0x2E | aaload | | 将引用类型数组中指定索引的元素放入操作数栈顶 |
103 | | 0x53 | aastore | | 将栈顶引用类型值保存到引用类型数组的指定索引位置处 |
104 |
105 | 创建数组的指令如下表所示。
106 |
107 | | ***指令的操作码*** | ***指令的助记符*** | ***操作数*** | ***描述*** |
108 | | ------------------ | ------------------ | ------------ | ---------------------- |
109 | | 0xBC | newarray | | 创建基本数据类型的数组 |
110 | | 0x53 | anewarray | | 创建引用类型的数组 |
111 |
112 | 访问数组的字节码指令还有获取数组一维长度的arrayleng指令,创建多维度的数组指令multinewarray指令。
113 |
114 | ---
115 |
116 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_08.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
117 |
118 |
--------------------------------------------------------------------------------
/chapter03/09.md:
--------------------------------------------------------------------------------
1 | # 条件分支语句的实现
2 |
3 | Java语言提供的条件分支语句包含if语句、switch语句、三目运算符,这些条件语句是如何通过字节码实现的呢?
4 |
5 | ## if语句
6 |
7 | 使用Java语言编写的if语句使用案例代码如下。
8 |
9 | ```java
10 | public int ifFunc(int type) {
11 | if (type == 1) {
12 | return 100;
13 | } else if (type == 2) {
14 | return 1000;
15 | } else {
16 | return 0;
17 | }
18 | }
19 | ```
20 |
21 | 使用javap命令输出ifFunc方法的字节码如下。
22 |
23 | ```java
24 | public int ifFunc(int);
25 | Code:
26 | 0: iload_1
27 | 1: iconst_1
28 | 2: if_icmpne 8
29 | 5: bipush 100
30 | 7: ireturn
31 | 8: iload_1
32 | 9: iconst_2
33 | 10: if_icmpne 17
34 | 13: sipush 1000
35 | 16: ireturn
36 | 17: iconst_0
37 | 18: ireturn
38 | ```
39 |
40 | 偏移量为0、1、2 三条字节码指令完成第一个if语句的判断。iload_1将参数type的值放入操作数栈顶,由于是非静态方法,所示局部变量表索引为0的Slot存储的是this引用,因此局部变量表索引为1的Slot存储的才是方法的第一个参数。iconst_1指令将立即数1放入操作数栈顶。if_icmpne指令完成操作数栈顶两个整数的比较,该指令的操作码为0xA0,指令执行需要一个操作数,操作数是当前方法某条字节码指令的偏移量。当栈顶的两个int类型的元素不相等时,跳转到操作数指向的字节码指令。
41 |
42 | if_icmpne字节码指令是判断两个值不相等才跳转,这与java代码刚好相反。在java代码中,if左右两个元素相等才执行if体内的代码,而编译后字节码指令按if与else if、else的编写顺序生成,当if 左右两个元素相等时继续往下执行便是对应java语言中的if语句的代码块,因此字节码层面会看到相反的条件比较跳转。
43 |
44 | 偏移量为8、9、10的三条字节码指令也是完成比较跳转的操作,最后一个else从偏移量为17的字节码指令开始,如果else代码块中没有返回指令,那么会继续往下执行。
45 |
46 | 如果第一个if中没有返回指令呢?如:
47 |
48 | ```java
49 | public int ifFunc2(int type) {
50 | if (type == 1) {
51 | type = 2;
52 | }else {
53 | type = 3;
54 | }
55 | return type;
56 | }
57 | ```
58 |
59 | 使用javap命令输出ifFunc2方法的字节码如下。
60 |
61 | ```java
62 | public int ifFunc2(int);
63 | Code:
64 | 0: iload_1
65 | 1: iconst_1
66 | 2: if_icmpne 10
67 | 5: iconst_2
68 | 6: istore_1
69 | 7: goto 12
70 | 10: iconst_3
71 | 11: istore_1
72 | 12: iload_1
73 | 13: ireturn
74 | ```
75 |
76 | 如字节码所示,编译器在if_icmpne指令后面为局部变量type赋值后,使用一条goto指令跳转到else结束的后面的第一条字节码指令。
77 |
78 | 所以,当if或者else if的代码块中没有return指令时,编译器会为其添加一条goto指令用于跳出if条件分支语句。goto指令是无条件跳转指令,操作码为0xA7,操作数是当前方法的某条字节码指令的偏移量,本例中,goto指令的操作码是12,表示跳转到偏移量为12的字节码指令,偏移量为12的字节码指令是iload_1,所以goto指令之后将会指向该指令。
79 |
80 | if_icmpne指令用于两个int类型值比较,不相等才跳转。更多比较跳转指令如下表所示。
81 |
82 | | ***指令的操作码*** | ***指令的助记符*** | ***操作数*** | ***描述*** |
83 | | ------------------ | ------------------ | ----------------------- | ------------------------------------- |
84 | | 0x9F | if_icmpeq | index,字节码指令的下标 | 如果栈顶两个int类型值相等则跳转 |
85 | | 0xA0 | if_icmpne | index,字节码指令的下标 | 如果栈顶两个int类型值不相等则跳转 |
86 | | 0xA1 | if_icmplt | index,字节码指令的下标 | 如果栈顶两int类型值前小于后则跳转 |
87 | | 0xA4 | if_icmple | index,字节码指令的下标 | 如果栈顶两int类型值前小于等于后则跳转 |
88 | | 0xA3 | if_icmpgt | index,字节码指令的下标 | 如果栈顶两int类型值前大于后则跳转 |
89 | | 0xA2 | if_icmpge | index,字节码指令的下标 | 如果栈顶两int类型值前大于等于后则跳转 |
90 | | 0xC6 | ifnull | index,字节码指令的下标 | 如果栈顶引用值为null则跳转 |
91 | | 0xC7 | ifnonnull | index,字节码指令的下标 | 如果栈顶引用值不为null则跳转 |
92 | | 0xA5 | if_acmpeq | index,字节码指令的下标 | 如果栈顶两引用类型值相等则跳转 |
93 | | 0xA6 | if_acmpne | index,字节码指令的下标 | 如果栈顶两引用类型不相等则跳转 |
94 |
95 | 与0比较的跳转指令如下表所示。
96 |
97 | | ***指令的操作码*** | ***指令的助记符*** | ***操作数*** | ***描述*** |
98 | | ------------------ | ------------------ | ----------------------- | -------------------------------- |
99 | | 0x99 | ifeq | index,字节码指令的下标 | 如果栈顶int类型值等于0则跳转 |
100 | | 0x9A | ifne | index,字节码指令的下标 | 如果栈顶int类型值不为0则跳转 |
101 | | 0x9B | iflt | index,字节码指令的下标 | 如果栈顶int类型值小于0则跳转 |
102 | | 0x9E | ifle | index,字节码指令的下标 | 如果栈顶int类型值小于等于0则跳转 |
103 | | 0x9D | ifgt | index,字节码指令的下标 | 如果栈顶int类型值大于0则跳转 |
104 | | 0x9C | ifge | index,字节码指令的下标 | 如果栈顶int类型值大于等于0则跳转 |
105 |
106 | ## switch语句
107 |
108 | 使用Java语言编写的switch语句使用案例代码如下。
109 |
110 | ```java
111 | public int switchFunc(int stat) {
112 | int a = 0;
113 | switch (stat) {
114 | case 5:
115 | a = 0;
116 | break;
117 | case 6:
118 | case 8:
119 | a = 1;
120 | break;
121 | }
122 | return a;
123 | }
124 | ```
125 |
126 | 使用javap命令输出switchFunc方法的字节码如下。
127 |
128 | ```java
129 | public int switchFunc(int);
130 | Code:
131 | 0: iconst_0
132 | 1: istore_2
133 | 2: iload_1
134 | 3: tableswitch { // 5 to 8
135 | 5: 32
136 | 6: 37
137 | 7: 39
138 | 8: 37
139 | default: 39
140 | }
141 | 32: iconst_0
142 | 33: istore_2
143 | 34: goto 39
144 | 37: iconst_1
145 | 38: istore_2
146 | 39: iload_2
147 | 40: ireturn
148 | ```
149 |
150 | 与if语句一样的是,switch代码块中的每个case代码块都是按顺序编译生成字节码的,switch代码块中的所有字节码都在tableswitch这条指令的后面。
151 |
152 | tableswitch指令的操作码为0xAA,该指令的操作数是不定长的,每个操作数的长度为四个字节,编译器会为case区间(本例中,case最小值为5,最大值为8,区间为[5,8])的每一个数字都生成一个case语句,就是添加一个操作数,操作数存放下一条字节码指令的相对偏移量,注意,是相对偏移量。以上面例子说明,tableswitch指令对应的字节码为:
153 |
154 | ```java
155 | AA | 00 00 00 24 | 00 00 00 05 | 00 00 00 08 |
156 | 00 00 00 1D | 00 00 00 22 | 00 00 00 24 | 00 00 00 22
157 | ```
158 |
159 | 第一个字节0xAA是tableswitch指令的操作码,后面每四个字节为一个操作数。前面四个字节0x00000024转为10进制是36,由于tableswitch指令的偏移量为3,因此该操作数表示匹配default时跳转到偏移量为39的字节码指令。紧随其后的是0x00000005与0x00000008,这两个数代表表格的区间,从5到8,也就是case 5到case 8,虽然我们代码中没有case 7,编译器还是为我们生成了。后面的0x0000001d、0x00000022、0x00000024、0x00000022分别+3得到的结果就是case 5到8分别跳转到的目标字节码指令的绝对偏移量。
160 |
161 | 从前面的例子我们可以看出,tableswitch指令生成的字节码占用的空间很大,而且当case的值不连续时,还会生成一些无用的映射。如果case的每个值都不连续呢?如:
162 |
163 | ```java
164 | public int switch2Func(int stat) {
165 | int a = 0;
166 | switch (stat) {
167 | case 1:
168 | a = 0;
169 | break;
170 | case 100:
171 | a = 1;
172 | break;
173 | }
174 | return a;
175 | }
176 | ```
177 |
178 | 假设,编译器将代码清单3-43的switch语句生成tableswitch指令,那么这条指令将浪费掉4乘以98的字节空间,如果再加个case 1000,那么浪费的空间更大。显然,这种情况下再使用tableswitch指令是不可取的。
179 |
180 | 使用javap输出switch2Func方法的字节码如下。
181 |
182 | ```java
183 | public int switch2Func(int);
184 | Code:
185 | 0: iconst_0
186 | 1: istore_2
187 | 2: iload_1
188 | 3: lookupswitch { // 2
189 | 1: 28
190 | 100: 33
191 | default: 35
192 | }
193 | 28: iconst_0
194 | 29: istore_2
195 | 30: goto 35
196 | 33: iconst_1
197 | 34: istore_2
198 | 35: iload_2
199 | 36: ireturn
200 | ```
201 |
202 | 正如你所看到的,编译器使用lookupswitch指令替代了tableswitch指令。lookupswitch指令的操作码为0xAB,与tableswitch指令一样,该指令的操作数也是不定长的,每个操作数的长度为四个字节,操作数存放的也是下一条字节码指令的相对偏移量,注意,还是相对偏移量。以上面例子说明,lookupswitch指令对应的字节码为。
203 |
204 | ```java
205 | AB | 00 00 00 20 | 00 00 00 02 | 00 00 00 01
206 | 00 00 00 19 | 00 00 00 64 | 00 00 00 1E
207 | ```
208 |
209 | 第一个字节0xAB是lookupswitch指令的操作码,接着后面四个字节也是匹配default时跳转的目标指令相对当前指令的偏移量,紧随其后四个字节0x00000002代表后面跟随多少个条件映射,每八个字节为一个条件映射,前四个字节为匹配条件,后四个字节为条件匹配时跳转的目标字节码指令的相对偏移量。0x00000001表示当当前操作数栈栈顶的值为1时,跳转到相对偏移量为0x00000019的字节码指令,0x00000019转为10进制是25,加上当前lookupswitch指令的偏移量3等于28;0x00000064转为十进制为100,0x0000001E转为十进制加上3等于33。
210 |
211 | ## 三目运算符
212 |
213 | 三目运算符也叫三元运算符,这是由三个操作数组成的运算符。
214 |
215 | 使用三目运算符的案例代码如下。
216 |
217 | ```java
218 | public int syFunc(boolean sex) {
219 | return sex ? 1 : 0;
220 | }
221 | ```
222 |
223 | 使用javap命令输出syFunc方法的字节码如下。
224 |
225 | ```java
226 | public int syFunc(boolean);
227 | Code:
228 | 0: iload_1
229 | 1: ifeq 8
230 | 4: iconst_1
231 | 5: goto 9
232 | 8: iconst_0
233 | 9: ireturn
234 | ```
235 |
236 | 由于方法参数sex是boolean类型,因此使用sex作为条件表达式编译后会使用ifeq指令实现跳转,即与0比较。当前操作数栈顶元素的值等于0则跳转,不等于0继续往下执行。
237 |
238 | 三目运算符的表达式为:<表达式1> ? <表达式2> :<表达式3>。因此三目运算符也支持多层嵌套,但实际开发中不建议这么做,因为会导致代码能以理解。
239 |
240 | ---
241 |
242 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_09.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
243 |
--------------------------------------------------------------------------------
/chapter03/10.md:
--------------------------------------------------------------------------------
1 | # 循环语句的实现
2 |
3 | Java语言提供的循环语句包括for、while和do-while,由于do-while不常用,因此本章不做介绍。Java循环语句的底层字节码实现实际上与条件分支语句的实现差不多,都是通过条件跳转指令完成。
4 |
5 | ## while循环
6 |
7 | 我们通过一个简单的while循环例子,了解while循环在字节码层面的实现,案例代码如下。
8 |
9 | ```java
10 | public void whileDemo() {
11 | int count = 10;
12 | while (count > 0) {
13 | count--;
14 | }
15 | }
16 | ```
17 |
18 | 使用javap命令输出whileDemo方法的字节码如下。
19 |
20 | ```java
21 | public void whileDemo();
22 | Code:
23 | 0: bipush 10
24 | 2: istore_1
25 | 3: iload_1
26 | 4: ifle 13
27 | 7: iinc 1, -1
28 | 10: goto 3
29 | 13: return
30 | ```
31 |
32 | 偏移量为0的字节码指令为bipush,该指令将立即数10放到操作数栈顶,接着使用istore_1指令将操作数栈栈顶的10存储到局部变量表索引为1的Slot,也就是给局部变量count赋值。虽然只有一个局部变量,但因为索引为0的Slot用来存储 this引用了,所以局部变量count存储在局部变量表的索引为1的Slot。
33 |
34 | 偏移量为3到10的字节码指令实现while循环。iload_1将局部变量count的值放到操作数栈栈顶,接着使用ifle条件跳转指令判断栈顶的元素是否小于等于0,如果小于等于0则跳转到偏移量为13的字节码指令,也就是结束while循环。ifle后面跟的是while循环体中的代码,iinc指令是将局部变量count减1。while循环体结束处会加上一条goto指令,goto指令是无条件跳转指令,本例中用于跳转到偏移量为3的字节码指令,直到ifle指令的条件成立才跳转到return指令结束循环。
35 |
36 | ## for循环
37 |
38 | for循环语句的一般表达式为:
39 |
40 | for(单次表达式;条件表达式;末尾循环体){
41 |
42 | 中间循环体;
43 |
44 | }
45 |
46 | 我们通过一个简单的for循环例子,了解for循环在字节码层面的实现,案例代码如下。
47 |
48 | ```java
49 | public int forDemo() {
50 | int count = 0;
51 | for (int i = 1; i <= 10; i++) {
52 | count += i;
53 | }
54 | return count;
55 | }
56 | ```
57 |
58 | 使用javap命令输出forDemo方法的字节码如下。
59 |
60 | ```java
61 | public int forDemo();
62 | Code:
63 | 0: iconst_0
64 | 1: istore_1
65 | 2: iconst_1
66 | 3: istore_2
67 | 4: iload_2
68 | 5: bipush 10
69 | 7: if_icmpgt 20
70 | 10: iload_1
71 | 11: iload_2
72 | 12: iadd
73 | 13: istore_1
74 | 14: iinc 2, 1
75 | 17: goto 4
76 | 20: iload_1
77 | 21: ireturn
78 | ```
79 |
80 | 其中,偏移量为0、1的两条字节码指令实现为局部变量count赋值为0;偏移量为2、3的两条字节码指令实现为局部变量i赋值;偏移量为4、5、7的字节码指令判断局部变量i是否大于10,条件成立则跳转到偏移量为20的字节码指令执行;偏移量为10、11、12、13这四条字节码指令为局部变量count的值加1;偏移量为13的字节码指令给局部变量i的值加1;偏移量为17的字节码指令告诉虚拟机下一条指令的偏移量为4,即跳转到偏移量为4的字节码指令,而偏移量为4开始的连续3条指令就是判断局部变量i是否大于10的,这便是for循环的实现。
81 |
82 | 我们常用的for循环还有一种,就是forEach。forEach语句是简化版的for语句,实际上是通过编译器实现的。forEach语句的格式为:
83 |
84 | for(元素类型 元素变量: 数组或集合对象) {
85 |
86 | }
87 |
88 | 以使用forEach语句遍历一个集合为例,从字节码层面看forEach的实现,案例代码如下。
89 |
90 | ```java
91 | public void forEachDemo(List list) {
92 | for (String str : list) {
93 | System.out.println(str);
94 | }
95 | }
96 | ```
97 |
98 | 使用javap命令输出forEachDemo方法的字节码如下。
99 |
100 | ```java
101 | public void forEachDemo(java.util.List);
102 | Code:
103 | 0: aload_1
104 | 1: invokeinterface #2, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
105 | 6: astore_2
106 | 7: aload_2
107 | 8: invokeinterface #3, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
108 | 13: ifeq 36
109 | 16: aload_2
110 | 17: invokeinterface #4, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
111 | 22: checkcast #5 // class java/lang/String
112 | 25: astore_3
113 | 26: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
114 | 29: aload_3
115 | 30: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
116 | 33: goto 7
117 | 36: return
118 | ```
119 |
120 | 从编译后的字节码可以看出,使用forEach语句遍历一个集合,实际上是通过迭代器Iterator加while循环实现的。偏移量为0和1的字节码指令是调用局部变量list的iterator方法获取该集合的迭代器;偏移量为7和8的字节码指令调用迭代器的hasNext方法;接着偏移量为13的字节码指令ifeq判断hasNext方法的返回值是否等于0,如果hasNext方法返回false那么等式就成立,等式成立则跳转到偏移量为36的字节码指令,循环结束。
121 |
122 | forEach语句中定义的局部变量str,是在循环体中通过调用迭代器Iterator的next方法为其赋值,为str赋值的字节码指令一定是放在我们编写的forEach循环体内的代码编译后的字节码指令之前。
123 |
124 | ---
125 |
126 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_10.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
127 |
128 |
--------------------------------------------------------------------------------
/chapter03/11.md:
--------------------------------------------------------------------------------
1 | # 异常处理的实现
2 |
3 | 在Java代码中,我们可通过try-catch-finally块对异常进行捕获或处理。其中catch块可以有零个或多个,finally块可有可无。如果catch有多个,而第一个catch的异常的类型是后面catch的异常的类型的父类,那么后面的catch块不会起作用。那么我们如何在字节码层面实现try-catch-finally块呢?
4 |
5 | ## try-catch
6 |
7 | 我们来看一个简单的try-catch使用例子,代码如下。
8 |
9 | ```java
10 | public int tryCatchDemo() {
11 | try {
12 | int n = 100;
13 | int m = 0;
14 | return n / m;
15 | } catch (ArithmeticException e) {
16 | return -1;
17 | }
18 | }
19 | ```
20 |
21 | 使用javap命令输出tryCatchDemo方法的字节码以及异常表信息如下。
22 |
23 | ```java
24 | public int tryCatchDemo();
25 | Code:
26 | stack=2, locals=3, args_size=1
27 | 0: bipush 100
28 | 2: istore_1
29 | 3: iconst_0
30 | 4: istore_2
31 | 5: iload_1
32 | 6: iload_2
33 | 7: idiv
34 | 8: ireturn
35 | 9: astore_1
36 | 10: iconst_m1
37 | 11: ireturn
38 | Exception table:
39 | from to target type
40 | 0 8 9 Class java/lang/ArithmeticException
41 | ```
42 |
43 | 异常表存储在Code属性中,异常表每项元素的结构见第二章。tryCatchDemo方法的异常表只有一项,该项的from、to、target存储的是方法字节码指令的偏移量,从from到to的字节码对应try代码块中的代码,target指向的字节码指令是catch代码块的开始,type是该catch块捕获的异常。也就是说,在执行偏移量为0到7的字节码指令时,如果抛出类型为ArithmeticException的异常,那么虚拟机将执行偏移量为9开始的字节码指令。
44 |
45 | 在本例中,如果try代码块中抛出的不是ArithmeticException异常,虚拟机将结束当前方法的执行,将异常往上抛出。如果直到当前线程的第一个方法都没有遇到catch代码块处理这个异常,那么当前线程将会异常结束,线程被虚拟机销毁。
46 |
47 | ## try-catch-finally
48 |
49 | final语意是如何实现的,为什么finally代码块的代码总能被执行到?我们来看一个例子:
50 |
51 | ```java
52 | public int tryCatchFinalDemo() {
53 | try {
54 | int n = 100;
55 | int m = 0;
56 | return n / m;
57 | } catch (ArithmeticException e) {
58 | return -1;
59 | } finally {
60 | System.out.println("finally");
61 | }
62 | }
63 | ```
64 |
65 | 使用javap命令输出tryCatchFinalDemo方法的字节码以及异常表信息如下。
66 |
67 | ```java
68 | public int tryCatchFinalDemo();
69 | Code:
70 | stack=2, locals=5, args_size=1
71 | 0: bipush 100
72 | 2: istore_1
73 | 3: iconst_0
74 | 4: istore_2
75 | 5: iload_1
76 | 6: iload_2
77 | 7: idiv
78 | 8: istore_3
79 | 9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
80 | 12: ldc #4 // String finally
81 | 14: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
82 | 17: iload_3
83 | 18: ireturn
84 | 19: astore_1
85 | 20: iconst_m1
86 | 21: istore_2
87 | 22: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
88 | 25: ldc #4 // String finally
89 | 27: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
90 | 30: iload_2
91 | 31: ireturn
92 | 32: astore 4
93 | 34: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
94 | 37: ldc #4 // String finally
95 | 39: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
96 | 42: aload 4
97 | 44: athrow
98 | Exception table:
99 | from to target type
100 | 0 9 19 Class java/lang/ArithmeticException
101 | 0 9 32 any
102 | 19 22 32 any
103 | 32 34 32 any
104 | ```
105 |
106 | 先看异常表。异常表的第一项对应tryCatchFinalDemo方法中的catch,当偏移量为0到9(不包括9)的字节码指令在执行过程中抛出异常时,如果异常类型为ArithmeticException则跳转到偏移量为19的字节码指令,也就是执行catch块。但后面的3项又是什么呢?
107 |
108 | 对照tryCatchFinalDemo方法编译后的字节码指令看。偏移量为0到9的字节码对应try代码块中的Java代码,而19到22对应catch块中的Java代码,32到42的字节码指令对应finally块中的Java代码。偏移量为32的字节码指令是将异常存储到局部变量表索引为4的Slot,这是因为在执行finally块中的代码之前需要将当前异常保存,以便于在执行完finally块中的代码之后,将异常还原到操作数栈的栈顶。抛出异常的字节码指令为athrow,该指令的操作码为0xBF。
109 |
110 | 根据异常表的理解,编译器为实现finally语意,在异常表中多生成了三个异常项,捕获的类型为any,即不管任何类型的受检异常,都会执行到target处的字节码。
111 |
112 | 总的理解就是,当try代码块中发生异常时,如果异常类型是ArithmeticException,则跳转到偏移量为19的字节码指令,如果异常类型不是ArithmeticException,则会匹配到异常表的第二项,跳动到偏移量为32的字节码指令,也就是执行finally块的代码。异常表的第三项,如果偏移量为19到22的字节码指令在执行过程中抛出异常,不管任何受检异常都跳转到finally块执行,偏移量为19到22的字节码指令对应catch块的代码。
113 |
114 | 从这个例子中可以看出,编译器除了为try代码块或者每个catch代码块都添加一个异常项用于捕获任意受检异常跳转到finally代码块执行之外,还把finally代码块的代码复制到try代码块的尾部,以及 catch代码块的尾部。以此确保任何情况下finally代码块中的代码都会被执行。
115 |
116 | ## try-with-resource语法糖
117 |
118 | 在JDK1.7之前,为确保访问的资源被关闭,我们需要为资源的访问代码块添加try-finally确保任何情况下资源都能被关闭,但由于关闭资源的close方法也可能抛出异常,因此也需要在finally代码块中嵌套try-catch代码块,这样写出来的代码显得非常的乱。
119 |
120 | JDK1.7推出了try-with-resource语法糖[^1],帮助资源自动释放,不需要在finally块中显示的调用资源的close方法关闭资源,由编译器自动生成。
121 |
122 | try-with-resource语法糖使用案例代码如下。
123 |
124 | ```java
125 | public void tryWithResource() {
126 | try (InputStream in = new FileInputStream("/tmp/xxx.xlsx")) {
127 | // 读取文件
128 | } catch (Exception e) {
129 | }
130 | }
131 | ```
132 |
133 | 使用javap输出这段代码的字节码如下。
134 |
135 | ```java
136 | public void tryWithResource();
137 | Code:
138 | stack=3, locals=4, args_size=1
139 | // 创建FileInputStream,局部变量表索引为1的Slot存储FileInputStream对象
140 | 0: new #6 // class java/io/FileInputStream
141 | 3: dup
142 | 4: ldc #7 // String /tmp/xxx.xlsx
143 | 6: invokespecial #8 // Method java/io/FileInputStream."":(Ljava/lang/String;)V
144 | 9: astore_1
145 | //
146 | 10: aconst_null
147 | 11: astore_2
148 | 12: aload_1
149 | 13: ifnull 40
150 | 16: aload_2
151 | 17: ifnull 36
152 | // 如果局部变量in不为null,且try块抛出的异常不为null,调用close方法
153 | 20: aload_1
154 | 21: invokevirtual #9 // Method java/io/InputStream.close:()V
155 | 24: goto 40
156 | // 调用addSuppressed方法将close方法抛出的异常添加到try代码块抛出的异常
157 | 27: astore_3
158 | 28: aload_2
159 | 29: aload_3
160 | 30: invokevirtual #11 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
161 | 33: goto 40
162 | // 调用close方法
163 | 36: aload_1
164 | 37: invokevirtual #9 // Method java/io/InputStream.close:()V
165 | 40: goto 44
166 | 43: astore_1
167 | 44: return
168 | Exception table:
169 | from to target type
170 | 20 24 27 Class java/lang/Throwable
171 | 0 40 43 Class java/lang/Exception
172 | ```
173 |
174 | 从tryWithResource方法编译后的字节码可以看出,编译器为try括号内打开的输入流InputStream,在try块的尾部添加了关闭输入流的相关代码。自动添加的字节码指令实现:判断局部变量in是否为空,如果不为空则调用局部变量in的close方法,并且为调用close方法的字节码指令也添加了try-catch块。
175 |
176 | ---
177 |
178 | [^1]: Oracle官方文档:https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
179 |
180 | 发布于:2021 年 08 月 21 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter03_11.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
181 |
182 |
--------------------------------------------------------------------------------
/chapter03/12.md:
--------------------------------------------------------------------------------
1 | # 本章小结
2 |
3 | 本章通过分析Java代码编译后生成的字节码指令,了解Java虚拟机字节码指令,了解字节码指令的执行过程。在分析for循环语句底层字节码实现时,我们还分析了forEach语句的实现,以及分析异常处理时,也分析了try-with-resource语法糖的实现,基本覆盖了常用的Java语法。
4 |
5 | 整章内容我们都是通过编写java代码案例,再通过javap命令查看对应的字节码指令。javap命令也将会是我们以后在进行字节码相关工作中常用的命令,这有助于我们在遇到一些不懂如何编写字节码指令实现的复杂语法时,可参考使用java编写实现后再使用javap命令查看的结果。
6 |
7 |
--------------------------------------------------------------------------------
/chapter03/README.md:
--------------------------------------------------------------------------------
1 | # 字节码指令集
2 |
3 | Java虚拟机的指令是由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零个或多个代表此操作所需参数(称为操作数,Operand)而构成[^1]。 虽然一条虚拟机指令的操作码只用一个字节存储,Java虚拟机所能支持的指令最多只能有256条,但虚拟机指令很少会随着虚拟机版本的更新而增加新的指令,即便Java版本更新很快。
4 |
5 | 相信通过前面第二章的学习,我们都已经在枯燥的学习中掌握了class文件结构,相比于学习 class文件结构,本章介绍的字节码指令会增添几分乐趣。在了解每条字节码指令后,通过javap查看Java代码编译后的字节码就能了解一些语法糖的本质实现。本章内容安排如下:
6 |
7 | * [从Hello Word出发](01.md)
8 | * [字段与方法描述符](02.md)
9 | * [读写局部变量表与操作数栈](03.md)
10 | * [基于对象的操作](04.md)
11 | * [访问静态字段与静态方法](05.md)
12 | * [调用方法的四条指令](06.md)
13 | * [不同类型返回值对应的指令](07.md)
14 | * [创建数组与访问数组元素](08.md)
15 | * [条件分支语句的实现](09.md)
16 | * [循环语句的实现](10.md)
17 | * [异常处理的实现](11.md)
18 | * [本章小结](12.md)
19 |
20 | ---
21 |
22 | [^1]: 《Java虚拟机规范》Java SE 8版字 节码指令集简介
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/chapter03/images/chapter03_01_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_01_01.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_01_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_01_02.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_01_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_01_03.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_01.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_02.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_03.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_04.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_05.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_06.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_07.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_03_08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_03_08.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_01.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_02.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_03.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_04.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_05.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_06.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_07.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_08.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_09.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_10.png
--------------------------------------------------------------------------------
/chapter03/images/chapter03_04_11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter03/images/chapter03_04_11.png
--------------------------------------------------------------------------------
/chapter04/01.md:
--------------------------------------------------------------------------------
1 | # 动态加载类的两种方式
2 |
3 | Java基础类库给我们提供了两种动态加载类的方式,一种是使用Class的静态方法forName,另一种是使用ClassLoader的loadClass方法。
4 |
5 | Class的静态方法forName加载类与ClassLoader的loadClass方法有所区别,下面我们分别使用这两种方式加载一个含有静态代码块的类:ClassLoaderTest。
6 |
7 | ```java
8 | public class ClassLoaderTest {
9 | static {
10 | System.out.println("my name is ClassLoaderTest!");
11 | }
12 | }
13 | ```
14 |
15 | 使用ClassLoader加载类:
16 |
17 | ```java
18 | public static void loadClass1() throws ClassNotFoundException {
19 | Class> classLoaderTestClass = ClassLoaderMain.class.getClassLoader()
20 | .loadClass("com.wujiuye.asmbytecode.book.fourth.ClassLoaderTest");
21 | System.out.println(classLoaderTestClass.getName());
22 | }
23 | ```
24 |
25 | 程序输出如下:
26 |
27 | 
28 |
29 | 使用Class.forName加载类:
30 |
31 | ```java
32 | public static void loadClass2() throws ClassNotFoundException {
33 | Class> classLoaderTestClass =
34 | Class.forName("com.wujiuye.asmbytecode.book.fourth.ClassLoaderTest");
35 | System.out.println(classLoaderTestClass.getName());
36 | }
37 | ```
38 |
39 | 程序输出如下:
40 |
41 | 
42 |
43 | 对比使用两种类加载方式的结果,很明显的区别就是使用Class静态方法forName加载ClassLoaderTest时,ClassLoaderTest的静态代码块被调用了,而静态代码块是被编译器编译后放入类的初始化方法``中的,也就是说,使用Class静态方法forName方法加载类会触发该类初始化,而ClassLoader的loadClass方法则不会。
44 |
45 | 要了解这两个加载类的方式有什么区别,我们可以从虚拟机的源码中寻找答案,那么如何找到对应的源码呢?
46 |
47 | 以opendjdk1.8[^1]为例,在源码根目录下,hotspot目录存放的是hotspot虚拟机的源码,jdk目录存放java开发工具包的源码,其中jdk/src/share目录下的classes目录是存放java基础类库的源码,native目录则是java基础类库中的native方法的实现,源码目录结构如下。
48 |
49 | ```txt
50 | openjdk
51 | -- hotspot
52 | -- jdk
53 | ------ src
54 | ------ share
55 | ---------- classes
56 | ---------- native
57 | ```
58 |
59 | 如果我们阅读Java源码就会发现,Class的静态方法forName与ClassLoader的loadClass方法最后都会调用一个native方法,想要查找Java基础类库中某个native方法的c实现,可到jdk/src/share/native的同包名目录下找到相同文件名的c代码文件,在c代码文件中找到对应的方法。
60 |
61 | 通过查看Java源码我们知道,ClassLoader的loadClass方法最终都会调用到defineClass0、defineClass1、defineClass2这几个native方法其中的一个。在本例中使用只有一个参数的loadClass方法,因此最后调用的是defineClass1方法,对应的c代码如下。
62 |
63 | ```c
64 | JNIEXPORT jclass JNICALL
65 | Java_java_lang_ClassLoader_defineClass1(JNIEnv *env, jobject loader,jstring name,
66 | jbyteArray data,jint offset,jint length,jobject pd,
67 | jstring source)
68 | {
69 | jbyte *body;
70 | char *utfName;
71 | jclass result = 0;
72 | char* utfSource;
73 | ......
74 | body = (jbyte *)malloc(length);
75 | ......
76 | result = JVM_DefineClassWithSource(env, utfName, loader, body, length, pd, utfSource);
77 | .....
78 | return result;
79 | }
80 | ```
81 |
82 | 这只是截取了其中一部分源代码。无论是defineClass0、defineClass1、defineClass2这几个方法其中的哪一个,最后都会调用虚拟机的对外接口:JVM_DefineClassWithSource。
83 |
84 | 我们可以到hotspot源码中查看该方法的实现,hotspot/src/share/vm/prims目录存放HotSpot虚拟机的对外接口,包括部分标准库的native部分和JVMTI实现。JVM_DefineClassWithSource等native方法中调用的方法可在hotspot源码的prims包下的jvm.cpp文件中找到。
85 |
86 | JVM_DefineClassWithSource源码如下。
87 |
88 | ```c++
89 | JVM_ENTRY(jclass, JVM_DefineClassWithSource(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source))
90 | JVMWrapper2("JVM_DefineClassWithSource %s", name);
91 | return jvm_define_class_common(env, name, loader, buf, len, pd, source, true, THREAD);
92 | JVM_END
93 | ```
94 |
95 | 该方法直接调用了jvm_define_class_common方法完成类的加载,jvm_define_class_common的部分源码如下。
96 |
97 | ```c++
98 | static jclass jvm_define_class_common(JNIEnv *env, const char *name,
99 | jobject loader, const jbyte *buf,
100 | jsize len, jobject pd, const char *source,
101 | jboolean verify, TRAPS) {
102 | ......
103 | // 1
104 | assert(THREAD->is_Java_thread(), "must be a JavaThread");
105 | JavaThread* jt = (JavaThread*) THREAD;
106 | ......
107 | // 2
108 | TempNewSymbol class_name = NULL;
109 | if (name != NULL) {
110 | const int str_len = (int)strlen(name);
111 | if (str_len > Symbol::max_length()) {
112 | THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name);
113 | }
114 | class_name = SymbolTable::new_symbol(name, str_len, CHECK_NULL);
115 | }
116 | ........
117 | // 3
118 | Klass* k = SystemDictionary::resolve_from_stream(class_name, class_loader,
119 | protection_domain, &st,
120 | verify != 0,
121 | CHECK_NULL);
122 | .......
123 | return (jclass) JNIHandles::make_local(env, k->java_mirror());
124 | }
125 | ```
126 |
127 | * 1) 确保加载类的线程是Java线程;
128 | * 2) 验证类名的长度,如果类名不为空,则类名不能超出最大长度,否则不被允许放入常量池,抛出NoClassDefFoundError;
129 | * 3) 调用SystemDictionary类的resolve_from_stream方法解析class文件字节流,该方法返回一个InstanceKlass指针,这便是hotspot虚拟机将class文件字节流解析完成后生成的c++对象,该对象存储在方法区中。
130 |
131 | 现在我们分析Class的静态方法forName的底层实现。该方法有两个重载方法,在不传入类加载器时,默认使用的类加载我们可以通过调用加载后的Class对象的getClassLoader方法拿到,默认是AppClassLoader。forName方法源码如下。
132 |
133 | ```c++
134 | public static Class> forName(String className) throws ClassNotFoundException {
135 | Class> caller = Reflection.getCallerClass();
136 | return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
137 | }
138 | private static native Class> forName0(String name, boolean initialize,
139 | ClassLoader loader,Class> caller) throws ClassNotFoundException;
140 | ```
141 |
142 | 该方法会调用Class的forName0这个native方法。forName0方法有一个bool类型的参数initialize,forName方法中调用forName0方法默认传递该参数的值为true,也就是告诉虚拟机,在加载类完成之后需要对类进行初始化。forName0的c代码部分源码如下。
143 |
144 | ```c++
145 | JNIEXPORT jclass JNICALL
146 | Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
147 | jboolean initialize, jobject loader, jclass caller)
148 | {
149 | char *clname;
150 | jclass cls = 0;
151 | char buf[128];
152 | .....
153 | cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller);
154 | .....
155 | return cls;
156 | }
157 | ```
158 |
159 | JVM_FindClassFromCaller的源码可在jvm.cpp文件中找到,我们跳过JVM_FindClassFromCaller,因为该方法是通过调用find_class_from_class_loader方法继续完成类加载的。
160 |
161 | ```c++
162 | jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init,
163 | Handle loader, Handle protection_domain,
164 | jboolean throwError, TRAPS) {
165 | Klass* klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain,
166 | throwError != 0, CHECK_NULL);
167 | KlassHandle klass_handle(THREAD, klass);
168 | // 检查是否应该初始化类
169 | if (init && klass_handle->oop_is_instance()) {
170 | klass_handle->initialize(CHECK_NULL);
171 | }
172 | return (jclass) JNIHandles::make_local(env, klass_handle->java_mirror());
173 | }
174 | ```
175 |
176 | 从find_class_from_class_loader方法中就可以看出,使用Class的静态方法forName加载类会触发类的初始化方法被调用是因为在类加载完成后调用了类的初始化方法。而类的加载部分是调用SystemDictionary的resolve_or_fail方法完成的,在类没有被加载过的情况下,最后都是通过调用类加载器的loadClass方法完成类的加载。
177 |
178 | ---
179 |
180 | [^1]: Openjdk1.8下载地址:https://github.com/unofficial-openjdk/openjdk/tree/jdk8u/jdk8u
181 |
182 | 发布于:2021 年 07 月 03 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter04_01.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
183 |
184 |
--------------------------------------------------------------------------------
/chapter04/03.md:
--------------------------------------------------------------------------------
1 | # 双亲委派模型
2 |
3 | Java基础类库给我们提供了几种类加载器,这几种类加载器的关系如图4.5所示。
4 |
5 | 
6 |
7 | BootstrapClassLoader:负责加载JDK提供的基础类库,基础类库编译后的rt.jar包位于 $JAVA_HOME/lib目录下。该类加载器由C代码实现,也叫启动类加载器。
8 |
9 | ExtClassLoader:负责加载 JVM 扩展类,比如内置的 js 引擎、xml 解析器等等,这些库名通常以javax开头,它们的jar包位于 $JAVA_HOME/lib/ext目录。
10 |
11 | AppClassLoader:应用程序加载器,负责加载classpath 路径中的jar包和目录,我们编写的代码以及使用的第三方jar包通常都是由它来加载。
12 |
13 | 这几种类加载器的关系并非继承关系,而是通过持有一个父加载器的引用,在外部调用loadClass方法时,先委托父加载器去加载,在父加载器抛出ClassNotFoundException异常时,再由自己去加载,如果还是加载不到类,再往上抛出ClassNotFoundException异常,这就是双亲委派模型。双亲委派的实现如下源码所示。
14 |
15 | ```java
16 | protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{
17 | synchronized (getClassLoadingLock(name)) {
18 | ......
19 | try {
20 | if (parent != null) {
21 | c = parent.loadClass(name, false);
22 | } else {
23 | c = findBootstrapClassOrNull(name);
24 | }
25 | } catch (ClassNotFoundException e) {
26 | // ClassNotFoundException thrown if class not found
27 | // from the non-null parent class loader
28 | }
29 | if (c == null) {
30 | // If still not found, then invoke findClass in order
31 | // to find the class.
32 | long t1 = System.nanoTime();
33 | c = findClass(name);
34 | ......
35 | }
36 | ......
37 | return c;
38 | }
39 | }
40 | ```
41 |
42 | 如果想要打破双亲委派模型,只需要在自实现的类型加载器中重写该方法,去掉try-catch部分的逻辑即可。
43 |
44 | ---
45 |
46 | 发布于:2021 年 07 月 03 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter04_03.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
--------------------------------------------------------------------------------
/chapter04/04.md:
--------------------------------------------------------------------------------
1 | # 自定义类加载器
2 |
3 | Java还提供了一种支持通过网络下载方式加载class的类加载器URLClassLoader,也支持传本地路径,但不能直接传入字节数组加载。而其它类加载器只能通过类名去加载。
4 |
5 | 由于动态编写字节码生成class或者改写class是在内存中完成的,如果要使用Java提供的几种类加载器去加载,我们需要先将字节码输出到文件并将文件存放在classpath的目录下,因此我们可以自己实现一个类加载器支持直接传入字节数组加载,省略掉这一步骤。
6 |
7 | ClassLoader 类有三个重要的方法,分别是 loadClass、findClass 和 defineClass。loadClass方法是加载目标类的入口,首先查找当前ClassLoader以及父加载器是否已经加载了目标类,如果都没有加载,且父加载器加载不到这个类,就会调用 findClass 让自己来加载目标类。ClassLoader 的 findClass方法是需要子类重写的,在该方法中实现获取目标类的字节码,拿到类的字节码之后再调用 defineClass方法将字节码转换成Class对象。
8 |
9 | 自定义类加载器需要继承ClassLoader,并重写findClass方法,在findClass方法中根据类名取得该类的字节码,再调用父类的defineClass方法完成类的加载。需要注意的是,findClass方法传递进行的类名是以符号“.”拼接的类名,不是“/”。使用“/”符号替代“ .”符号的类名称为类的内部名称或内部类名。
10 |
11 | 自定义类加载器ByteCodeClassLoader的实现如下代码所示。
12 |
13 | ```java
14 | public class ByteCodeClassLoader extends ClassLoader {
15 |
16 | // 类名-> 字节码持有者
17 | private final Map classes = new HashMap<>();
18 |
19 | public ByteCodeClassLoader(final ClassLoader parentClassLoader) {
20 | super(parentClassLoader);
21 | }
22 |
23 | @Override
24 | protected Class> findClass(final String name) throws ClassNotFoundException {
25 | ByteCodeHolder holder= classes.get(name);
26 | if (holder != null) {
27 | byte[] bytes = holder.getByteCode();
28 | classes.remove(name);
29 | return defineClass(name, bytes, 0, bytes.length);
30 | }
31 | return super.findClass(name);
32 | }
33 |
34 | public void add(final String name, final ByteCodeHolder holder) {
35 | classes.put(name, holder);
36 | }
37 | }
38 | ```
39 |
40 | ByteCodeClassLoader使用HashMap缓存类名与其字节码的映射,外部需要先通过调用add方法将需要加载的类添加到Map中,才能调用loadClass方法加载类。在findClass方法中,完成类的加载后将调用add方法添加的数据移除。
41 |
42 | ---
43 |
44 | 发布于:2021 年 07 月 03 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter04_04.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
45 |
46 |
--------------------------------------------------------------------------------
/chapter04/05.md:
--------------------------------------------------------------------------------
1 | # 本章小结
2 |
3 | 本章我们通过分析hotspot源码深入理解了hotspot虚拟机类加载的几个阶段:加载、链接(验证、准备、解析)、初始化,在hotspot虚拟机中,类加载的几个阶段并非严格按照顺序执行的,如部分验证工作是在加载阶段完成的,部分验证工作又是在解析阶段完成的,而准备阶段又是在加载阶段之后执行的,类的链接是在类初始化时才触发的,在类初始化之前完成链接,并对类的字节码进行验证,解析阶段则是在符号引用将要被使用前才去解析。
4 |
5 | 最后我们也了解了几种类加载器,以及了解双亲委派模型是如何实现的,如何打破双亲委派模型,以及实现能直接将内存中的class字节码转为Class的自定义类加载器。
6 |
7 | ---
8 |
9 | 发布于:2021 年 07 月 03 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter04_05.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
10 |
11 |
--------------------------------------------------------------------------------
/chapter04/README.md:
--------------------------------------------------------------------------------
1 | # 深入理解类加载
2 |
3 | 了解类加载过程有助于我们在字节码实战过程中快速定位各种错误导致类加载异常问题,而理解类加载器双亲委派模型也是理解类加载器不可或缺的知识点,有助于我们实现自定义类加载器,以便使用自定义类加载器加载动态改写过的class字节码。
4 |
5 | 本章内容安排如下:
6 |
7 | * [动态加载类的两种方式](01.md)
8 | * [类加载过程](02.md)
9 | * [双亲委派模型](03.md)
10 | * [自定义类加载器](04.md)
11 | * [本章小结](chapter04/05.md)
12 |
13 |
--------------------------------------------------------------------------------
/chapter04/images/chapter04-01-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-01-01.png
--------------------------------------------------------------------------------
/chapter04/images/chapter04-01-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-01-02.png
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-01.jpg
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-02.png
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-03.png
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-04.png
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-05.jpg
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-06.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-06.jpg
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-07.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-07.jpg
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-08.png
--------------------------------------------------------------------------------
/chapter04/images/chapter04-02-09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-02-09.png
--------------------------------------------------------------------------------
/chapter04/images/chapter04-03-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter04/images/chapter04-03-01.jpg
--------------------------------------------------------------------------------
/chapter05/01.md:
--------------------------------------------------------------------------------
1 | # 框架简介
2 |
3 | ASM是一个非常小且快速的java字节码操作框架。除ASM外,javassist也是一个非常热门的字节码操作框架,很多人也喜欢拿这两个框架进行比较。javassist的主要优点是简单,使用javassist不需要了解class文件结构,也不需要了解字节码指令,就能动态改变类的结构或生成类,但这同时也是缺点,这种简单带来了局限性,也导致性能降低。而ASM恰好与之相反,使用ASM需要了解底层,对使用者有一定的门槛,但ASM没有局限,我们完全可以使用ASM编写任意一个能用Java代码编写的类。
4 |
5 | 由于类只动态生成或改写一次,因此性能可能并不是我们考虑的最大因素,但ASM能实现的功能更多,可玩性更强,ASM使用访问者模式并提供非常丰富的API简化我们的开发。
6 |
7 | ---
8 |
9 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_01.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
10 |
11 |
--------------------------------------------------------------------------------
/chapter05/02.md:
--------------------------------------------------------------------------------
1 | # 访问者模式在ASM框架中的应用
2 |
3 | 访问者模式是23种设计模式中的一种,使用频率不高,通常用于操作一些拥有固定结构的数据,避免操作数据的过程中由于不注意而导致数据结构发生改变。如改写SQL的JsqlParser框架,以及本书介绍的ASM框架。
4 |
5 | 访问者模式的定义是:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
6 |
7 | ASM框架使用访问者模式封装了class文件结构的各项元素的操作。我们将通过实现一个简单版的ASM框架学习访问者模式在ASM框架中的应用,也以此来理解访问者模式。
8 |
9 | 首先定义类访问者接口ClassVisitor,代码如下。
10 |
11 | ```java
12 | public interface ClassVisitor {
13 | // 设置class文件结构的版本号、类的访问标志、类名
14 | void visit(int version, String access, String className);
15 | // 为类添加一个字段
16 | FieldVisitor visitField(String access, String name, String descriptor);
17 | // 为类添加一个方法
18 | MethodVisitor visitMethod(String access, String name, String descriptor);
19 | }
20 | ```
21 |
22 | ClassVisitor接口定义了三个方法:visit方法可设置class文件结构的版本号、类的访问标志以及类名,visitField方法可给类添加一个字段,visitMethod方法可给类添加一个方法。
23 |
24 | 由于字段元素也是一个数据结构,需要使用访问者模式封装字段结构中各项元素的操作。如通过调用字段访问者的visitAnnotation方法为字段添加一个注解。
25 |
26 | 字段访问者,FieldVisitor接口的定义代码如下。
27 |
28 | ```java
29 | public interface FieldVisitor {
30 | // 为字段添加一个注解
31 | void visitAnnotation(String annotation, boolean runtime);
32 | }
33 | ```
34 |
35 | 编写FieldVisitor接口的实现类,FieldWriter类代码如下。
36 |
37 | ```java
38 | @Getter
39 | public class FieldWriter implements FieldVisitor {
40 |
41 | private String access;
42 | private String name;
43 | private String descriptor;
44 | private List annotations;
45 |
46 | public FieldWriter(String access, String name, String descriptor) {
47 | this.access = access;
48 | this.name = name;
49 | this.descriptor = descriptor;
50 | this.annotations = new ArrayList<>();
51 | }
52 |
53 | @Override
54 | public void visitAnnotation(String annotation, boolean runtime) {
55 | this.annotations.add("注解:" + annotation + "," + runtime);
56 | }
57 |
58 | }
59 | ```
60 |
61 | 与字段结构一样,方法结构也需要使用访问者模式封装各项元素的操作。如通过调用方法访问者的visitMaxs方法设置操作数栈和局部变量表的大小。
62 |
63 | 方法访问者,MethodVisitor接口的定义如下。
64 |
65 | ```java
66 | public interface MethodVisitor {
67 | // 设置局部变量表和操作数栈的大小
68 | void visitMaxs(int maxStackSize, int maxLocalSize);
69 | }
70 | ```
71 |
72 | 编写MethodVisitor接口的实现类,MethodWriter类代码如下。
73 |
74 | ```java
75 | @Getter
76 | public class MethodWriter implements MethodVisitor {
77 |
78 | private String access;
79 | private String name;
80 | private String descriptor;
81 | private int maxStackSize;
82 | private int maxLocalSize;
83 |
84 | public MethodWriter(String access, String name, String descriptor) {
85 | this.access = access;
86 | this.name = name;
87 | this.descriptor = descriptor;
88 | }
89 |
90 | @Override
91 | public void visitMaxs(int maxStackSize, int maxLocalSize) {
92 | this.maxLocalSize = maxLocalSize;
93 | this.maxStackSize = maxStackSize;
94 | }
95 |
96 | }
97 | ```
98 |
99 | 在class文件结构中,字段表可以有零个或多个字段,方法表可以有一个或多个方法,因此我们需要使用数组存储字段表和方法表。而由于方法表和字段表中的每个方法或每个字段也都是一个数据结构,因此字段表和方法表我们存储的是字段的访问者和方法的访问者。
100 |
101 | 编写ClassVisitor接口的实现类,ClassWriter类代码如下。
102 |
103 | ```java
104 | @Getter
105 | public class ClassWriter implements ClassVisitor {
106 |
107 | private int version;
108 | private String className;
109 | private String access;
110 | // 存储的是字段访问者
111 | private List fieldWriters = new ArrayList<>();
112 | // 存储的是方法访问者
113 | private List methodWriters = new ArrayList<>();
114 |
115 | @Override
116 | public void visit(int version, String access, String className) {
117 | this.version = version;
118 | this.className = className;
119 | this.access = access;
120 | }
121 |
122 | @Override
123 | public FieldVisitor visitField(String access, String name, String descriptor) {
124 | FieldWriter fieldWriter = new FieldWriter(access, name, descriptor);
125 | fieldWriters.add(fieldWriter);
126 | return fieldWriter;
127 | }
128 |
129 | @Override
130 | public MethodVisitor visitMethod(String access, String name, String descriptor) {
131 | MethodWriter methodWriter = new MethodWriter(access, name, descriptor);
132 | methodWriters.add(methodWriter);
133 | return methodWriter;
134 | }
135 |
136 | }
137 | ```
138 |
139 | visitField方法先为类添加一个字段元素,创建字段的访问者(FieldVisitor),并将字段访问者添加到字段表,最后返回该字段访问者。visitMethod方法先为类添加一个方法元素,创建方法的访问者(MethodVisitor),并将访问者添加到方法表,最后返回该方法访问者。
140 |
141 | 在ASM框架中,可调用ClassWriter实例的toByteArray方法获取生成的类的class字节数组。我们可以模拟实现toByteArray方法,在ClassWriter类添加showClass方法,用于输出类的class文件结构的各项信息,代码如下。
142 |
143 | ```java
144 | public void showClass() {
145 | System.out.println("版本号:" + getVersion());
146 | System.out.println("访问标志:" + getAccess());
147 | System.out.println("类名:" + getClassName());
148 | // 遍历字段
149 | for (FieldWriter fieldWriter : fieldWriters) {
150 | System.out.print(fieldWriter.getAccess()
151 | + " " + fieldWriter.getDescriptor()
152 | + " " + fieldWriter.getName()+ " ");
153 | for (String annotation : fieldWriter.getAnnotations()) {
154 | System.out.println(annotation + " ");
155 | }
156 | }
157 | // 遍历方法
158 | for (MethodWriter methodWriter : methodWriters) {
159 | System.out.println(methodWriter.getAccess()
160 | + " " + methodWriter.getName()
161 | + " " + methodWriter.getDescriptor()
162 | + " 操作数栈大小:" + methodWriter.getMaxStackSize()
163 | + " 局部变量表大小:" + methodWriter.getMaxLocalSize());
164 | }
165 | }
166 | ```
167 |
168 | 现在,我们使用自己编写的简单版ASM框架生成一个类,为该类添加一个字段并为该字段添加一个注解,然后为该类添加一个方法,并设置该方法的局部变量表和操作数栈的大小,代码如下。
169 |
170 | ```java
171 | public static void main(String[] args) {
172 | ClassWriter classWriter = new ClassWriter();
173 | classWriter.visit(52, "public", "com.wujiuye.User");
174 | // 添加字段
175 | FieldVisitor fieldVisitor = classWriter
176 | .visitField("private", "name", "Ljava/lang/String;");
177 | // 为字段添加注解
178 | fieldVisitor.visitAnnotation("@Getter", true);
179 | // 添加方法
180 | MethodVisitor methodVisitor = classWriter
181 | .visitMethod("public", "getName", "(Ljava/lang/String)V");
182 | // 设置局部变量表和操作数栈的大小
183 | methodVisitor.visitMaxs(1, 1);
184 | classWriter.showClass();
185 | }
186 | ```
187 |
188 | 程序输出结果如下图所示。
189 |
190 | 
191 |
192 | ---
193 |
194 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_02.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
195 |
196 |
--------------------------------------------------------------------------------
/chapter05/03.md:
--------------------------------------------------------------------------------
1 | # 在项目中使用ASM
2 |
3 | 如果项目是使用maven或者gradle构建的,那么都可以直接使用在maven仓库中搜索ASM的结果,本书使用的ASM版本为6.2。
4 |
5 | 对于maven项目,需在maven依赖配置文件pom.xml中添加如下依赖配置。
6 |
7 | ```xml
8 |
9 | org.ow2.asm
10 | asm
11 | 6.2
12 |
13 | ```
14 |
15 | 对于gradle项目,需在gradle依赖配置文件build.gradle中添加如下依赖配置。
16 |
17 | ```java
18 | compile group: 'org.ow2.asm', name: 'asm', version: '6.2'
19 | ```
20 |
21 | 如何使用ASM操作字节吗?本章将通过一些案例介绍ASM框架的一些常用API的使用。本书不会每个API都介绍到,如果读者想要学习ASM框架的更多API可查阅ASM的在线API文档[^1]。
22 |
23 | ---
24 |
25 | [^1]: ASM在线API文档:https://tool.oschina.net/apidocs/apidoc?api=asm
26 |
27 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_03.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
28 |
29 |
--------------------------------------------------------------------------------
/chapter05/04.md:
--------------------------------------------------------------------------------
1 | # 创建类并创建方法
2 |
3 | ASM使用访问者模式提供非常丰富的API简化我们的开发,如访问类的ClassVisitor、访问字段的FieldVisitor、访问方法的MethodVisitor。
4 |
5 | 在使用ASM创建一个类之前,我们先认识ASM提供的ClassWriter类,ClassWriter继承ClassVisitor,是以字节码格式生成类的类访问者。使用这个访问者可生成一个符合class文件格式的字节数组。
6 |
7 | 我们可以使用ClassWriter从头开始生成一个Class,也可以与ClassReader一起使用,以修改现有的Class生成新的Class。
8 |
9 | ClassWriter的构造方法要求传递一个参数,必须是COMPUTE_FRAMES、COMPUTE_MAXS中的0个或多个。
10 |
11 | ```java
12 | // 0个
13 | ClassWriter classWriter = new ClassWriter(0);
14 | // 一个:COMPUTE_MAXS
15 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
16 | // 一个:COMPUTE_FRAMES
17 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
18 | // 多个:COMPUTE_MAXS和COMPUTE_FRAMES
19 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
20 | ```
21 |
22 | * COMPUTE_MAXS:自动计算局部变量表和操作数栈的大小。但仍然需要调用visitMax方法,只是你可以传递任意参数,ASM会忽略这些参数,并重新计算大小。
23 | * COMPUTE_FRAMES:自动计算方法的栈映射桢。与自动计算局部变量表和操作数栈的大小一样,我们仍然需要调用visitFrame方法,但参数可以随意填。
24 |
25 | 创建一个ClassWriter对象其实就是创建一个符合class文件结构的空字节数组,但此时这个对象是没有任何意义的,我们需要调用ClassWriter对象的API为该对象填充class文件结构的各项元素。
26 |
27 | ClassWriter类的visit方法用于设置类的class文件结构版本号、类的访问标志、类的名称、类的签名、父类的名称、实现的接口。visit方法的定义如下。
28 |
29 | ```java
30 | public final void visit(final int version,final int access,final String name,
31 | final String signature,final String superName,final String[] interfaces)
32 | ```
33 |
34 | visit方法的参数说明:
35 |
36 | * version:指定类文件结构的版本号;
37 | * access:指定类的访问标志,如public、final等;
38 | * name:指定类的名称(内部类名),如“java/lang/String”;
39 | * signature:类的类型签名,如“Ljava/lang/String;”;
40 | * superName:继承的父类名称。除Object类外,所有的类都必须有父类;
41 | * interfaces:该类需要实现的接口。
42 |
43 | 在ASM框架中,每个访问者都有一个visitEnd方法,如ClassVisitor。ClassVisitor的visitEnd方法在结束时调用,用于通知访问者,类访问结束。
44 |
45 | 现在,我们就使用ClassWriter来创建一个类,一个继承至Object的类,代码如下。
46 |
47 | ```java
48 | public class UseAsmCreateClass {
49 |
50 | public static void main(String[] args) throws IOException, ClassNotFoundException {
51 | String className = "com.wujiuye.asmbytecode.book.fifth.AsmGenerateClass";
52 | String signature = "L" + className.replace(".", "/") + ";";
53 | // 字节计算局部变量表和操作数栈大小、栈映射桢
54 | ClassWriter classWriter = new ClassWriter(0);
55 | // 类名和父类名需要使用 “/”替换“.”。这个可以在常量池中找到答案
56 | classWriter.visit(Opcodes.V1_8, ACC_PUBLIC,
57 | className.replace(".", "/"),
58 | signature,
59 | Object.class.getName().replace(".", "/"),
60 | null);
61 | classWriter.visitEnd();
62 | // 获取生成的class的字节数组
63 | byte[] byteCode = classWriter.toByteArray();
64 | ByteCodeUtils.savaToFile(className, byteCode);
65 | }
66 |
67 | }
68 | ```
69 |
70 | 由于ClassWriter对象生成的是类的字节数组,因此,为验证我们所写的代码生成的class字节码是正确的,我们还需要将ClassWriter对象生成的字节数组输出到一个class文件。在案例代码中,我们使用ByteCodeUtils的savaToFile方法将class字节数组输出到文件,savaToFile方法的实现代码如下。
71 |
72 | ```java
73 | public static void savaToFile(String className, byte[] byteCode) throws IOException {
74 | File file = new File("/tmp/" + className + ".class");
75 | if ((!file.exists() || file.delete()) && file.createNewFile()) {
76 | try (FileOutputStream fos = new FileOutputStream(file)) {
77 | fos.write(byteCode);
78 | }
79 | }
80 | }
81 | ```
82 |
83 | 使用IDEA打开这个class文件,查看class文件反编译后的java代码,如下图所示。
84 |
85 | 
86 |
87 | IDEA能够将其反编译为java代码,说明该class文件的结构没有任何问题,但并不能说明这个类能使用。因为我们并没有生成类的实例初始化方法``,ASM不是编译器,不会自动帮我们生成``方法。如果现在使用ClassLoader来加载这个类并通过反射创建一个实例,那么将会得到如下图所示的错误。
88 |
89 | 
90 |
91 | 因此,在创建类之后,我们必须要为类添加实例初始化方法,也就是Java类的构造方法。并且需要在实例初始化方法中调用父类的实例初始化方法。
92 |
93 | 为类生成方法,我们需要用到ClassWriter类的visitMethod方法,visitMethod方法的定义如下。
94 |
95 | ```java
96 | public final MethodVisitor visitMethod(final int access,final String name,
97 | final String descriptor,final String signature,final String[] exceptions)
98 | ```
99 |
100 | visitMethod方法的各参数说明:
101 |
102 | * access:方法的访问标志,如public、static等;
103 | * name:方法的名称(内部类名);
104 | * descriptor:方法的描述符,如“()V”;
105 | * signature:方法签名,可以为空;
106 | * exceptions:该方法可能抛出的受检异常的类型内部名称,可以为空。
107 |
108 | 调用ClassWriter的visitMethod方法会创建一个MethodWriter,MethodWriter的构造方法中会将方法名、方法描述符、方法签名生成CONSTANT_Utf8_info常量,并添加到常量池中。如果exceptions参数不为空,会为异常表的每个异常类型生成一个CONSTANT_Class_info常量,并添加到常量池中。
109 |
110 | 使用visitMethod方法为类生成一个``方法,代码如下。
111 |
112 | ```java
113 | MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC,"","()V",null,null);
114 | methodVisitor.visitEnd();
115 | ```
116 |
117 | visitMethod方法会返回该方法的访问者:MethodVisitor实例。我们在使用完MethodVisitor 之后,与ClassVisitor一样,需要调用实例的visitEnd方法。可以说,使用ASM提供的每个访问者,都需要调用一次实例的visitEnd方法,一般的使用场景下,不调用visitEnd方法也不会有任何问题,因为这些visitEnd方法都是空实现,什么事情也不做。
118 |
119 | 调用visitMethod方法仅仅只是为类生成一个方法,如果方法的访问标志没有ACC_NATIVE或ACC_ABSTRACT,也就是说方法不是native方法或者抽象方法,那么我们还需要继续为方法添加Code属性。
120 |
121 | 我们先来了解MethodVisitor接口定义的几个常用API:
122 |
123 | * visitCode:访问方法的Code属性,实际上也是一个空方法,什么事情也不做;
124 | * visitMaxs:用于设置方法的局部变量表与操作数栈的大小;
125 |
126 | MethodVisitor接口提供的编写字节码指令相关的API:
127 |
128 | * visitInsn:往Code属性的code数组中添加一条无操作数的字节码指令,如dup指令、aload_0指令等;
129 | * visitVarInsn:往Code属性的code数组中添加一条需要一个操作数的字节码指令,如aload指令;
130 | * visitFieldInsn:往Code属性的code数组中添加一条访问字段的字节码指令,用于添加putfield、getfield、putstatic、getstatic指令;
131 | * visitTypeInsn:往Code属性的code数组中添加一条操作数为常量池中某个CONSTANT_Class_info常量的索引的字节码指令,如new指令;
132 | * visitMethodInsn:往Code属性的code数组中添加一条调用方法的字节码指令,如invokevirtual指令。
133 |
134 | 这些API简化了我们操作字节码的难度,如使用visitTypeInsn生成一条new指令,我们不需要知道new指令需要的操作数是多少,用到的常量也不需要去创建,只需要传递类的内部类型名称,即使用‘/’符号替代‘.’符号的类名。
135 |
136 | visitMethodInsn方法会为我们生成对应的方法符号引用常量:CONSTANT_Methodref_info。visitMethodInsn方法的定义如下。
137 |
138 | ```java
139 | public void visitMethodInsn(final int opcode,final String owner,final String name, final String descriptor,final boolean isInterface)
140 | ```
141 |
142 | visitMethodInsn方法的各参数解析:
143 |
144 | * opcode:字节码指令的操作码,如invokesepcial指令的操作码十进制的值为183;
145 | * owner:类的内部类型名称,如“java/lang/Object”;
146 | * name:方法名称,如“”;
147 | * descriptor:方法描述符,如“()V”;
148 | * isInterface:是否是接口,使用invokeinterface指令才传true,其它都传false。
149 |
150 | 现在,我们使用MethodVisitor为新增的``方法生成方法体,即为``方法生成调用父类``方法的字节码指令,并设置该方法的局部变量表以及操作数栈的大小。实现代码如下。
151 |
152 | ```java
153 | static void generateMethod(ClassWriter classWriter){
154 | MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "", "()V", null, null);
155 | methodVisitor.visitCode();
156 | // 调用父类构造器
157 | methodVisitor.visitVarInsn(ALOAD, 0);
158 | methodVisitor.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","", "()V", false);
159 | // 添加一条返回指令
160 | methodVisitor.visitInsn(RETURN);
161 | // 设置操作数栈和局部变量表大小
162 | methodVisitor.visitMaxs(1,1);
163 | methodVisitor.visitEnd();
164 | }
165 | ```
166 |
167 | 此时再修改代码main方法,在调用ClassWriter实例的visitEnd方法之前,先调用generateMethod方法,为类生成一个``方法。最终生成的Java类如下。
168 |
169 | ```java
170 | public class AsmGenerateClass {
171 | public AsmGenerateClass() {
172 | }
173 | }
174 | ```
175 |
176 | ---
177 |
178 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_04.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
179 |
180 |
--------------------------------------------------------------------------------
/chapter05/05.md:
--------------------------------------------------------------------------------
1 | # 给类添加字段
2 |
3 | 给类添加字段可调用ClassWriter实例的visitField方法,该方法可添加静态字段,也可添加非静态字段。visitField方法的定义如下。
4 |
5 | ```java
6 | public final FieldVisitor visitField(final int access,final String name, final String descriptor,final String signature,final Object value)
7 | ```
8 |
9 | visitField方法的各参数说明:
10 |
11 | * access:字段的访问标志,如public、final、static;
12 | * name:字段的名称;
13 | * descriptor:字段的类型描述符,如”Ljava/lang/String;”;
14 | * signature:字段的类型签名;
15 | * value:字段的初始值,此参数只用于静态字段,如接口中声明的字段或类中声明的静态常量字段,并且类型必须是基本数据类型或String类型。
16 |
17 | 如果是添加静态常量字段,且字段类型是基本数据类型或String类型,那么指定value,ASM会为该字段生成一个ConstantValue属性,同时也会为value生成一个对应类型的常量。如value的类型为int类型,则会为value生成一个CONSTANT_Integer_info常量,并添加到常量池中。
18 |
19 | 比如添加一个静态常量字段,且类型为int类型,代码如下。
20 |
21 | ```java
22 | FieldVisitor fieldVisitor = classWriter.visitField(ACC_PUBLIC | ACC_STATIC | ACC_FINAL,"age", "I", null, 100);
23 | ```
24 |
25 | 比如添加一个非静态字段,类型为String,代码如下。
26 |
27 | ```java
28 | FieldVisitor fieldVisitor = classWriter.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
29 | ```
30 |
31 | 调用visitField方法会返回一个FieldVisitor实例,即该字段的访问者。如果有需要,可以继续调用FieldVisitor实例的visitAnnotation方法,给该字段添加一个注解。比如给字段添加一个lombok框架的@Getter注解,代码如下。
32 |
33 | ```java
34 | fieldVisitor.visitAnnotation("Llombok/Getter;", false);
35 | ```
36 |
37 | 现在看一个完整的例子,我们基于5.4《创建类并创建方法》中的案例代码,在调用ClassWriter实例的visit方法之后,调用我们编写的generateField方法,为类添加一个字段,字段类型为String,访问标志为private,字段名为name,并且给该字段添加一个lombok框架的@Getter注解。generateField方法实现代码如下。
38 |
39 | ```java
40 | static void generateField(ClassWriter classWriter) {
41 | FieldVisitor fieldVisitor = classWriter.visitField(ACC_PRIVATE,"name", "Ljava/lang/String;", null, null);
42 | fieldVisitor.visitAnnotation("Llombok/Getter;", false);
43 | }
44 | ```
45 |
46 | 最终生成的AsmGenerateClass类的Java代码如下。
47 |
48 | ```java
49 | public class AsmGenerateClass {
50 | @Getter
51 | private String name;
52 | public AsmGenerateClass() {
53 | }
54 | }
55 | ```
56 |
57 | ---
58 |
59 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_05.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
60 |
61 |
--------------------------------------------------------------------------------
/chapter05/06.md:
--------------------------------------------------------------------------------
1 | # 改写类并改写方法
2 |
3 | 改写类,就是基于被改写的类的class字节数组修改,生成新的类,一般改写类不会修改类的名称,因为修改类的名称或使用不同类加载器加载,那对虚拟机而言就是一个新的类。Java Agent允许我们在类加载之前修改类,也可结合Attach API使用,在类加载之后,程序运行期间可随时修改类,通过重新加载类替换旧的类。关于Java Agent和Attach API见本书第七章。
4 |
5 | 改写一个类首先需要获取到一个类的字节数组。ASM框架提供的ClassReader用于解析符合class文件格式的字节数组,我们可通过使用ClassReader读取并解析获取到一个类的字节数组,再将解析后的字节数组交给ClassWriter去改写。
6 |
7 | ClassReader的构造方法有五个,如下。
8 |
9 | ```java
10 | public ClassReader(final String className) throws IOException
11 | public ClassReader(final byte[] classFile)
12 | public ClassReader(final InputStream inputStream) throws IOException
13 | public ClassReader(final byte[] classFileBuffer, final int classFileOffset, final int classFileLength)
14 | ClassReader(final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion)
15 | ```
16 |
17 | 前四个构造方法是我们可以使用的,且最后都会调用到最后一个构造方法。第一个构造方法可直接传递类名,ASM根据类名从当前classpath去读取该类的class文件;第二个构造方法可直接传递一个符合class文件结构的字节数组;第三个构造方法是传递一个输入流,ASM从输入流读取字节数据。
18 |
19 | 以使用ClassReader类的传递一个类名的构造方法创建一个ClassReader实例为例,代码如下。
20 |
21 | ```java
22 | String className = "com.wujiuye.asmbytecode.book.fifth.UseAsmModifyClass";
23 | ClassReader classReader = new ClassReader(className);
24 | ```
25 |
26 | ClassReader并不是访问者,但ClassReader类提供了accept方法用于接收一个ClassVisitor实例,由该ClassVisitor实例访问ClassReader实例解析后的class字节数组。accept方法的定义如下。
27 |
28 | ```java
29 | public void accept(final ClassVisitor classVisitor, final int parsingOptions)
30 | ```
31 |
32 | accept方法各参数解析:
33 |
34 | * classVisitor:类访问者,如ClassWriter;
35 | * parsingOptions:解析选项,可以是SKIP_CODE、SKIP_DEBUG、SKIP_FRAMES、EXPAND_FRAMES中的零个或多个。零个传0,多个使用“或”运算符组合。
36 |
37 | 我们来看一个ClassReader与ClassWriter结合使用的例子:从classpath中读取一个现有类,并给该类添加一个字段,然后将改写后的类的字节数组输出到文件。代码如下。
38 |
39 | ```java
40 | public class UseAsmModifyClass {
41 |
42 | public static void main(String[] args) throws IOException {
43 | String className = "com.wujiuye.asmbytecode.book.fifth.UseAsmModifyClass";
44 | ClassReader classReader = new ClassReader(className);
45 | ClassWriter classWriter = new ClassWriter(0);
46 | classReader.accept(classWriter, 0);
47 | generateField(classWriter);
48 | byte[] newByteCode = classWriter.toByteArray();
49 | ByteCodeUtils.savaToFile(className, newByteCode);
50 | }
51 |
52 | static void generateField(ClassWriter classWriter) {
53 | FieldVisitor fieldVisitor = classWriter.visitField(ACC_PRIVATE,
54 | "name", "Ljava/lang/String;", null, null);
55 | fieldVisitor.visitAnnotation("Llombok/Getter;", false);
56 | }
57 |
58 | }
59 | ```
60 |
61 | 在UseAsmModifyClass 类的main方法中,我们先创建一个ClassReader 实例,用来读取UseAsmModifyClass类的class文件并解析,然后创建一个ClassWriter实例,调用ClassReader实例 的accept方法传入该ClassWriter实例,之后调用generateField方法为该类生成一个私有的String类型的字段,字段名为name,并为该字段添加一个@Getter注解。最后调用ByteCodeUtils工具类的savaToFile方法将改写后的class字节数组输出到文件。改写后的类如下图所示。
62 |
63 | 
64 |
65 | 为该类添加方法与从头开始创建一个新的类并为类添加方法一样,可通过调用ClassWriter实例的visitMethod方法为类添加方法。但改写方法就不能简单的通过调用ClassWriter实例的visitMethod方法完成了。
66 |
67 | 以移除UseAsmModifyClass类的main方法为例。我们需要自己实现一个ClassVisitor,但我们不做ClassWriter能做的事情,只做ClassWriter不能做的事情,因此我们可以使用代理模式实现自定义的ClassVisitor。自定义的ClassVisitor实现代码如下。
68 |
69 | ```java
70 | public class MyClassWriter extends ClassVisitor {
71 |
72 | private ClassWriter classWriter;
73 |
74 | public MyClassWriter(ClassWriter classWriter) {
75 | super(Opcodes.ASM6, classWriter);
76 | this.classWriter = classWriter;
77 | }
78 |
79 | @Override
80 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
81 | if ("main".equals(name)) {
82 | return null;
83 | }
84 | return super.visitMethod(access, name, descriptor, signature, exceptions);
85 | }
86 |
87 | public byte[] toByteArray() {
88 | return classWriter.toByteArray();
89 | }
90 |
91 | }
92 | ```
93 |
94 | 自定义的MyClassWriter重写了ClassVisitor类的visitMethod方法,当方法名称为“main”时,返回null,否则调用父类的visitMethod方法,由父类调用构造MyClassWriter时,参数传入的ClassWriter实例的visitMethod方法,创建main方法与创建main方法访问者。
95 |
96 | 能够移除main方法的原理是,当visitMethod方法返回null时,main方法不会创建,也不会创建main方法的访问者,因此不会创建“main”方法。
97 |
98 | 我们在调用ClassReader实例的accept方法时,accept方法会遍历ClassReader实例读取到的类的class文件结构的各项,遍历的目的是调用ClassWriter实例的相应visit方法,将ClassReader实例读取到的类的文件结构各项填充到ClassWriter实例的class文件结构。
99 |
100 | 现在我们可以使用自定义的MyClassWriter改写UseAsmModifyClass类,去掉UseAsmModifyClass的main方法,实现代码如下。
101 |
102 | ```java
103 | public class UseAsmModifyClass {
104 | public static void main(String[] args) throws IOException {
105 | String className = "com.wujiuye.asmbytecode.book.fifth.UseAsmModifyClass";
106 | ClassReader classReader = new ClassReader(className);
107 | // 创建MyClassWriter实例
108 | MyClassWriter classWriter = new MyClassWriter(new ClassWriter(0));
109 | classReader.accept(classWriter, 0);
110 |
111 | byte[] newByteCode = classWriter.toByteArray();
112 | ByteCodeUtils.savaToFile(className, newByteCode);
113 | }
114 | }
115 | ```
116 |
117 | 改写后的UseAsmModifyClass类如下图所示。
118 |
119 | 
120 |
121 | 如果我们不想去掉main方法,只是想改变main方法中的代码,怎么实现呢?
122 |
123 | 与实现自定义的ClassVisitor类一样,我们也可以通过实现自定义的MethodVisitor类实现。如在UseAsmModifyClass的main方法插入一行输出“hello word!”的代码,实现代码如下。
124 |
125 | ```java
126 | public class MainMethodWriter extends MethodVisitor {
127 |
128 | private MethodVisitor methodVisitor;
129 |
130 | public MainMethodWriter(MethodVisitor methodVisitor) {
131 | super(Opcodes.ASM6, methodVisitor);
132 | this.methodVisitor = methodVisitor;
133 | }
134 |
135 | @Override
136 | public void visitCode() {
137 | super.visitCode();
138 | methodVisitor.visitFieldInsn(GETSTATIC,
139 | Type.getInternalName(System.class),
140 | "out",
141 | Type.getDescriptor(System.out.getClass()));
142 | methodVisitor.visitLdcInsn("hello word!");
143 | methodVisitor.visitMethodInsn(INVOKEVIRTUAL,
144 | Type.getInternalName(System.out.getClass()),
145 | "println",
146 | "(Ljava/lang/String;)V", false);
147 | }
148 |
149 | }
150 | ```
151 |
152 | MainMethodWriter类重写了visitCode方法,目的是在main方法的第一条字节码指令的前面插入输出“hello word!”的字节码指令。当然,也可以重写visitInsn方法,在main方法的return指令之前插入字节码指令,代码如下。
153 |
154 | ```java
155 | @Override
156 | public void visitInsn(int opcode) {
157 | if (opcode == RETURN) {
158 | // 如果操作码等于return指令的操作码
159 | // 则在return指令之前插入一些字节码指令
160 | }
161 | super.visitInsn(opcode);
162 | }
163 | ```
164 |
165 | 现在,我们改写自定义的MyClassWriter类,修改visitMethod方法,判断如果是main方法,则创建一个MainMethodWriter,代码如下。
166 |
167 | ```java
168 | public class MyClassWriter extends ClassVisitor {
169 |
170 | private ClassWriter classWriter;
171 |
172 | public MyClassWriter(ClassWriter classWriter) {
173 | super(Opcodes.ASM6, classWriter);
174 | this.classWriter = classWriter;
175 | }
176 |
177 | @Override
178 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
179 | if ("main".equals(name)) {
180 | MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
181 | return new MainMethodWriter(methodVisitor);
182 | }
183 | return super.visitMethod(access, name, descriptor, signature, exceptions);
184 | }
185 |
186 | public byte[] toByteArray() {
187 | return classWriter.toByteArray();
188 | }
189 |
190 | }
191 | ```
192 |
193 | 改写后的UseAsmModifyClass类如下图所示。
194 |
195 | 
196 |
197 | ---
198 |
199 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_06.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
200 |
201 |
--------------------------------------------------------------------------------
/chapter05/07.md:
--------------------------------------------------------------------------------
1 | # 创建类并实现接口
2 |
3 | 前面我们已经学习了如何使用ASM创建一个类并为类添加方法。实现接口也非常简单,只需要在调用类访问器的visit方法时,指定该类需要实现的接口,而实现接口定义的方法就是使用visitMethod方法为该类添加一个访问标志与接口中定义的方法相同、方法名称与方法描述符都相同的方法。
4 |
5 | 以实现SayHelloInterface接口为例,SayHelloInterface接口的定义如下。
6 |
7 | ```java
8 | public interface SayHelloInterface {
9 | void sayHello();
10 | }
11 | ```
12 |
13 | 现在,我们使用ClassWriter创建一个新的类,类名为SayHelloInterfaceImpl,并为该类实现SayHelloInterface接口,实现SayHelloInterface接口的sayHello方法,为sayHello插入输出“hello word”的字节码指令。代码如下。
14 |
15 | ```java
16 | public class UseAsmImpInterface {
17 |
18 | public static void main(String[] args) throws IOException {
19 | // 创建的类的类名
20 | String implClassName = SayHelloInterface.class.getName() + "Impl";
21 | ClassWriter cw = new ClassWriter(0);
22 | // 设置class文件结构的版本号、类名、类签名、父类、实现的接口
23 | cw.visit(Opcodes.V1_8, ACC_PUBLIC,
24 | implClassName.replace(".", "/"),
25 | null,
26 | Type.getInternalName(Object.class),
27 | new String[]{Type.getInternalName(SayHelloInterface.class)});
28 | // 创建asyHello方法
29 | MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "sayHello",
30 | "()V", null, null);
31 | // 插入输出"hello word!"的字节码指令
32 | mv.visitFieldInsn(GETSTATIC,
33 | Type.getInternalName(System.class),
34 | "out",
35 | Type.getDescriptor(System.out.getClass()));
36 | mv.visitLdcInsn("hello word!");
37 | mv.visitMethodInsn(INVOKEVIRTUAL,
38 | Type.getInternalName(System.out.getClass()),
39 | "println",
40 | "(Ljava/lang/String;)V", false);
41 | // void方法也需要有return指令
42 | mv.visitInsn(RETURN);
43 | // 设置局部变量表和操作数栈的大小
44 | mv.visitMaxs(2,1);
45 | // 获取生成的类的字节数组
46 | byte[] byteCode = cw.toByteArray();
47 | // 保存到文件
48 | ByteCodeUtils.savaToFile(implClassName, byteCode);
49 | }
50 |
51 | }
52 | ```
53 |
54 | 在创建ClassWriter 时,我们给构造方法传递的参数值为0,意味着我们需要自己计算方法的局部变量表和操作数栈的大小,所以在创建sayHello方法时,需要调用方法访问者的visitMaxs方法设置局部变量表和操作数栈的大小。
55 |
56 | 那为什么操作数栈的大小是2,局部变量表的大小是1呢?
57 |
58 | 我们一共为sayHello方法添加了三条指令,第一条指令是获取一个静态字段,指令返回字段的引用存储在操作数栈顶,接着将字符串“hello word!”的引用放入栈顶,最后才调用PrintStream实例的println方法,因此操作数栈的深度至少为2才能完成println方法的调用。而局部变量表只存储this引用,所以设置为1。
59 |
60 | 此案例代码中,还用到了ASM提供的工具类(Type类)的一些方法:
61 |
62 | * getInternalName:获取一个类的内部类型名称,比如我们调用String.class.getName方法获取到的名称是“java.lang.String”,调用Type的getInternalName方法获取到的就是“java/lang/String”;
63 | * getDescriptor:获取类的描述符,如String类的描述符为“Ljava/lang/String;”。
64 |
65 | 运行案例代码的main方法,我们能够得到一个SayHelloInterfaceImpl类,但是这个类并不能加载使用,因为没有生成``方法。生成的SayHelloInterfaceImpl类如下图所示。
66 |
67 | 
68 |
69 | ---
70 |
71 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_07.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
72 |
73 |
--------------------------------------------------------------------------------
/chapter05/08.md:
--------------------------------------------------------------------------------
1 | # 继承类并重写父类方法
2 |
3 | 与创建类并实现接口相似,继承类并重写父类的方法,只需要在调用类访问器的visit方法时,指定该类所继承的父类,重写父类的方法,就是使用visitMethod方法为该类添加一个,访问标志与父类中的方法相同、方法名称与方法描述符都相同的方法。与实现接口不同的是,我们可以在重写的方法中调用父类的方法。
4 |
5 | 以继承BaseClass为例,BaseClass类如下。
6 |
7 | ```java
8 | public class BaseClass {
9 | public void sayHello() {
10 | System.out.println("BaseClass sayHello");
11 | }
12 | }
13 | ```
14 |
15 | 现在,我们使用ClassWriter创建一个新的类,类名为SubClass,并指定该类继承BaseClass类。然后重写父类的sayHello方法,在子类的sayHello方法中,先调用父类的sayHello方法,再输出“SubClass sayHello”字符串。实现代码如下。
16 |
17 | ```java
18 | public class UseAsmOverrideMethod {
19 |
20 | public static void main(String[] args) throws IOException {
21 | // 创建的类的类名
22 | String subClassName = BaseClass.class.getName()
23 | .replace("Base", "Sub");
24 | ClassWriter cw = new ClassWriter(0);
25 |
26 | // 设置class文件结构的版本号、类名、类签名、父类、实现的接口
27 | cw.visit(Opcodes.V1_8, ACC_PUBLIC,
28 | subClassName.replace(".", "/"),
29 | null,
30 | Type.getInternalName(BaseClass.class),
31 | null);
32 |
33 | // 生成初始化方法
34 | // generateInitMethod(cw);
35 |
36 | // 创建sayHello方法
37 | MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "sayHello",
38 | "()V", null, null);
39 |
40 | // 调用父类的方法
41 | mv.visitVarInsn(ALOAD, 0);
42 | mv.visitMethodInsn(INVOKESPECIAL,
43 | Type.getInternalName(BaseClass.class),
44 | "sayHello",
45 | "()V", false);
46 |
47 | // 插入输出"SubClass sayHello"的字节码指令
48 | mv.visitFieldInsn(GETSTATIC,
49 | Type.getInternalName(System.class),
50 | "out",
51 | Type.getDescriptor(System.out.getClass()));
52 | mv.visitLdcInsn("SubClass sayHello");
53 | mv.visitMethodInsn(INVOKEVIRTUAL,
54 | Type.getInternalName(System.out.getClass()),
55 | "println",
56 | "(Ljava/lang/String;)V", false);
57 |
58 | mv.visitInsn(RETURN);
59 | // 设置局部变量表和操作数栈的大小
60 | mv.visitMaxs(2, 1);
61 |
62 | // 获取生成的类的字节数组
63 | byte[] byteCode = cw.toByteArray();
64 | // 保存到文件
65 | ByteCodeUtils.savaToFile(subClassName, byteCode);
66 | }
67 | }
68 | ```
69 |
70 | 注意:为了简化代码,省去了为类生成``方法的代码,所以生成的这个SubClass 类是不能被加载使用的。
71 |
72 | 运行此案例代码,我们将得到一个SubClass类,如下图所示。
73 |
74 | 
75 |
76 | ---
77 |
78 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_08.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
79 |
80 |
--------------------------------------------------------------------------------
/chapter05/09.md:
--------------------------------------------------------------------------------
1 | # 本章小结
2 |
3 | 本章我们通过自己实现一个简单的ASM框架,了解访问者模式在ASM中的应用,以此加深对ASM框架的API的理解。并通过一些实战案例,介绍ASM框架一些常用的API的基本使用。本章介绍的这些案例基本覆盖我们工作中80%的使用场景,只要我们掌握这80%,其余的再通过看API文档学习就很容易理解。
4 |
5 | ---
6 |
7 | 发布于:2021 年 10 月 10 日
作者: [吴就业](https://www.wujiuye.com/)
GitHub链接:https://github.com/wujiuye/JVMByteCodeGitBook
链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/chapter05_09.md
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
8 |
9 |
--------------------------------------------------------------------------------
/chapter05/README.md:
--------------------------------------------------------------------------------
1 | # ASM快速上手
2 |
3 | 相信读者们读完第三章之后对字节码指令都很熟悉了。比如调用方法的四条常用字节码指令都需要一个操作数,而操作数的值是常量池中表示某个方法符号引用的常量在常量池中的索引,因此,在写字节码指令时,我们需要知道要调用的方法的描述符是否存在常量池中,如果不存在,我们需要往常量池添加常量,这些都是重复且繁琐的工作。因此,我们需要一件趁手的兵器来帮我们完成这些工作,这件兵器就是ASM。
4 |
5 | 本章内容包括:
6 |
7 | * [框架简介](01.md)
8 | * [访问者模式在ASM框架中的应用](02.md)
9 | * [在项目中使用ASM](03.md)
10 | * [创建类并创建方法](04.md)
11 | * [给类添加字段](05.md)
12 | * [改写类并改写方法](06.md)
13 | * [创建类并实现接口](07.md)
14 | * [继承类并重写父类方法](08.md)
15 | * [本章小结](09.md)
16 |
17 |
--------------------------------------------------------------------------------
/chapter05/images/chapter05-02-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-02-01.png
--------------------------------------------------------------------------------
/chapter05/images/chapter05-04-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-04-01.png
--------------------------------------------------------------------------------
/chapter05/images/chapter05-04-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-04-02.png
--------------------------------------------------------------------------------
/chapter05/images/chapter05-06-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-06-01.png
--------------------------------------------------------------------------------
/chapter05/images/chapter05-06-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-06-02.png
--------------------------------------------------------------------------------
/chapter05/images/chapter05-06-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-06-03.png
--------------------------------------------------------------------------------
/chapter05/images/chapter05-06-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-06-04.png
--------------------------------------------------------------------------------
/chapter05/images/chapter05-06-05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/chapter05/images/chapter05-06-05.png
--------------------------------------------------------------------------------
/opensourceprojects/README.md:
--------------------------------------------------------------------------------
1 | # 字节码实战开源项目
2 |
3 | 本章分享一些有趣的字节码实战开源项目...
4 |
5 | * [实现类Spring框架@Async注解功能的asyncframework](asyncframework.md)
6 | * [一款轻量级的分布式调用链路追踪Java探针vine](vine.md)
7 | * [运行时解析json生成class的json-class-generator](jcg.md)
8 | * [JavaAgent入门级项目Beemite](beemite.md)
9 |
10 |
--------------------------------------------------------------------------------
/opensourceprojects/asyncframework.md:
--------------------------------------------------------------------------------
1 | # 实现类Spring框架@Async注解功能的asyncframework
2 |
3 | asyncframework是笔者于2019年实现的一个类Spring框架@Async注解功能的异步框架,只需要在接口上添加一个`@AsyncFunction`注解就可让这个方法异步执行,并已发布在笔者的[Github](https://github.com/wujiuye/asyncframework)上。
4 |
5 | asyncframework框架的`@AsyncFunction`注解不仅支持用在无返回值的方法上,与Spring框架一样,它同样支持`@AsyncFunction`注解用在有返回值的方法上。
6 |
7 | 但与Spring框架实现不同的是,asyncframework框架是完全基于动态字节码技术实现的,支持在非`spring`项目中使用,这也是我当初写它的原因。
8 |
9 | 如果你也对字节码感兴趣,我非常推荐你阅读这个框架的源码,浓缩的都是精华,十几个类包含了设计模式的使用、字节码、以及框架的设计思想,对你理解`spring`的`@Async`注解实现原理也有帮助。
10 |
11 | 首先允许笔者向您介绍如何使用asyncframework,再介绍asyncframework的实现原理。
12 |
13 | ## 如何使用asyncframework
14 |
15 | 第一步:在Java项目中添加依赖
16 |
17 | ```
18 |
19 | com.github.wujiuye
20 | asyncframework
21 | 1.2.0-RELEASE
22 |
23 | ```
24 |
25 | 第二步:定义接口以及编写接口的实现类
26 |
27 | ```java
28 | /**
29 | * @author wujiuye
30 | * @version 1.0 on 2019/11/24
31 | */
32 | public interface AsyncMessageSubscribe {
33 | /**
34 | * 异步无返回值
35 | *
36 | * @param queue
37 | */
38 | @AsyncFunction
39 | void pullMessage(String queue);
40 | /**
41 | * 异步带返回值
42 | *
43 | * @param s1
44 | * @param s2
45 | * @return
46 | */
47 | @AsyncFunction
48 | AsyncResult doAction(String s1, String s2);
49 | }
50 | ```
51 |
52 | 编写实现类:
53 |
54 | ```java
55 | public class AsyncMessageSubscribe implements AsyncMessageSubscribe {
56 | @Override
57 | public void pullMessage(String queue) {
58 | System.out.println(queue + ", current thread name:" + Thread.currentThread().getName());
59 | }
60 | @Override
61 | public AsyncResult doAction(String s1, String s2) {
62 | return new AsyncResult<>("hello wujiuye! current thread name:" + Thread.currentThread().getName());
63 | }
64 | }
65 | ```
66 |
67 | 第三步:配置全局线程池,以及使用`AsyncProxyFactory`创建代理对象
68 |
69 | 在调用`AsyncProxyFactory`的`getInterfaceImplSupporAsync`方法创建代理类实例时,需要指定异步执行使用哪个线程池,以及接口的实现类。
70 |
71 | ```java
72 | public class AsmProxyTest {
73 | // 配置全局线程池
74 | static ExecutorService executorService = Executors.newFixedThreadPool(2);
75 | @Test
76 | public void testAutoProxyAsync() throws Exception {
77 | AsyncMessageSubscribe proxy = AsmProxyFactory.getInterfaceImplSupporAsync(
78 | AsyncMessageSubscribe.class, impl, executorService);
79 | // 异步不带返回值
80 | proxy.pullMessage("wujiuye");
81 | // 异步带返回值
82 | AsyncResult asyncResult = proxy.doAction("sssss", "ddd");
83 | System.out.println(asyncResult.get());
84 | }
85 | }
86 | ```
87 |
88 | 你可能会问,这还要创建代理类去调用,我直接`new`一个`Runnable`放到线程池执行不是更方便?
89 |
90 | 确实如此,但如果通过包扫描自动创建代理对象那就不一样了,`spring`就是通过`BeanPostProcess`实现的。而且,当我们需要把异步改为同步时,只需要去掉注解,而当想同步改异步时,也只需要添加注解,不需要改代码。
91 |
92 | ## 异步无返回值的实现原理
93 |
94 | 我们以实现消息异步订阅为例,介绍在不使用任何框架的情况下,如何通过静态代理实现将订阅消息方法由同步切换到异步,而这正是asyncframework的实现原理,asyncframework只是将静态代理改为动态代理。
95 |
96 | 定义消息订阅接口:
97 |
98 | ```java
99 | public interface MessageSubscribeTemplate {
100 | void subscribeMessage(MessageQueueEnum messageQueue,
101 | OnReceiveMessageCallback onReceiveMessageCallback,
102 | Class tagClass);
103 | }
104 | ```
105 |
106 | 消息订阅接口实现类:
107 |
108 | ```java
109 | public class AwsSqsMessageConsumer implements MessageSubscribeTemplate {
110 | @Override
111 | public void subscribeMessage(MessageQueueEnum messageQueue,
112 | OnReceiveMessageCallback onReceiveMessageCallback,
113 | Class tagClass){
114 | // 编写实现逻辑
115 | }
116 | }
117 | ```
118 |
119 | 提示:为什么消息订阅抽象为接口?因为当时我们经常会切换MQ框架,一开始使用RocketMQ,后面由于成本问题又切换到了AWS的SQS服务。
120 |
121 | 下面就可以通过静态代理实现消息订阅的同步切异步,代码如下。
122 |
123 | ```java
124 | public class MessageSubscribeTemplateProxy implements MessageSubscribeTemplate {
125 | private ExecutorService executorService;
126 | private MessageSubscribeTemplate target;
127 |
128 | public MessageSubscribeTemplateProxy(ExecutorService executorService,
129 | MessageSubscribeTemplate target) {
130 | this.target = target;
131 | this.executorService = executorService;
132 | }
133 |
134 | @Override
135 | public void subscribeMessage(MessageQueueEnum var1, OnReceiveMessageCallback var2, Class var3) {
136 | // 实现异步调用逻辑,就是放到线程池中去执行
137 | executorService.execute(()->this.target.subscribeMessage(var1, var2, var3));
138 | }
139 | }
140 | ```
141 |
142 | asyncframework框架就是实现动态编写MessageSubscribeTemplateProxy代理类,以此省去同步切异步或异步切同步时修改MessageSubscribeTemplateProxy代理类的麻烦。
143 |
144 | 有了asyncframework,我们只需要编写消息订阅模版的实现类即可,同步还是异步我们不必关系,当想让订阅方法异步执行就在方法上添加@AsyncSubscribe注解。并且支持接口多个方法,对某些方法添加注解,就只会是这些方法实现异步执行。
145 |
146 | ## 异步带返回值的实现原理
147 |
148 | 笔者在实现支持带返回值的方法异步执行这个功能时,遇到了两个大难题:
149 |
150 | * 难点一:带返回值的方法如何去实现异步?
151 |
152 | * 难点二:如何编写字节码实现泛型接口的代理类?
153 |
154 | 在`spring`项目中,如果想在带返回值的方法上添加`@Async`注解,就需要方法返回值类型为`AsyncResult`,笔者也去看了一下`spring`的源码,发现`AsyncResult`是一个`Future`。
155 |
156 | 思路有是有了,但仅仅只是依靠`Future`还是实现不了的。
157 |
158 | 我们知道,`ExecutorService`的`submit`方法支持提交一个`Callable`带返回值的任务,并且`submit`方法返回一个`Future`,调用这个`Future`的`get`方法当前线程会阻塞,直到任务执行结束。
159 |
160 | 所以如果我们在代理类方法中调用`Future`的get方法等待结果,再将结果包装成`AsyncResult`返回,这就不是异步执行了,而是同步执行了。
161 |
162 | 所以我们要解决的问题就是:代理类必须要在将异步方法提交到线程池后,就要立即返回一个`AsyncResult`,并且要确保当外部调用这个`AsyncResult`的`get`方法时,获取到的结果就是最终方法执行后返回的结果。
163 |
164 | 笔者想到的方法是:在代理类将异步方法提交到线程池后,立即返回一个`AsyncResult`代理对象,这个`AsyncResult`代理对象代理的是`Future`的get方法,当外部调用这个`AsyncResult`代理对象的get方法时,再去调用`Future`的get方法。
165 |
166 | 先实现`AsyncResult`,这是一个非阻塞的`Future`,因为不需要阻塞。
167 |
168 | ```java
169 | public class AsyncResult implements Future {
170 | private T result;
171 | public AsyncResult(T result) {
172 | this.result = result;
173 | }
174 |
175 | @Override
176 | public T get() throws InterruptedException, ExecutionException {
177 | return result;
178 | }
179 |
180 | @Override
181 | public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
182 | return get();
183 | }
184 |
185 | /**
186 | * 由字节码调用
187 | *
188 | * @param future 提交到线程池执行返回的future
189 | * @param
190 | * @return
191 | */
192 | public static AsyncResult newAsyncResultProxy(final Future> future) {
193 | return new AsyncResult(null) {
194 | @Override
195 | public T get() throws InterruptedException, ExecutionException {
196 | AsyncResult asyncResult = future.get();
197 | return asyncResult.get();
198 | }
199 | };
200 | }
201 |
202 | }
203 | ```
204 |
205 | `newAsyncResultProxy`方法才是整个异步实现的最关键一步,该方法是给字节码生成的代理对象调用的,代理方法实际返回结果是newAsyncResultProxy方法返回的AsyncResult。当外部调用这个`AsyncResult`的`get`方法时,实际上是去调用`ExecutorService`的`submit`方法返回的那个`Future`的`get`方法。对使用者屏蔽了这个阻塞获取结果的实现过程。
206 |
207 | 还是以消息订阅为例:
208 |
209 | ```java
210 | // 接口
211 | public interface AsyncMessageSubscribe {
212 | @AsyncFunction
213 | AsyncResult doAction(String s1, String s2);
214 | }
215 |
216 | // 接口实现类
217 | private AsyncMessageSubscribe impl = new AsyncMessageSubscribe() {
218 | @Override
219 | public AsyncResult doAction(String s1, String s2) {
220 | return new AsyncResult<>("current thread name:" + Thread.currentThread().getName());
221 | }
222 | };
223 | ```
224 |
225 | asyncframework框架使用动态字节码技术生成的将AsyncMessageSubscribe#doAction方法提交到线程池执行的Callable代码如下。
226 |
227 | ```java
228 | public static class AsyncMessageSubscribe_doActionCallable implements Callable> {
229 | private AsyncMessageSubscribe target;
230 | private String param1;
231 | private String param2;
232 |
233 | public AsyncMessageSubscribe_doActionCallable(AsyncMessageSubscribe var1, String var2, String var3) {
234 | this.target = var1;
235 | this.param1 = var2;
236 | this.param2 = var3;
237 | }
238 |
239 | public AsyncResult call() throws Exception {
240 | return this.target.doAction(this.param1, this.param2);
241 | }
242 | }
243 | ```
244 |
245 | asyncframework框架使用动态字节码技术生成的AsyncMessageSubscribe的动态代理类如下。
246 |
247 | ```java
248 | public class AsyncMessageSubscribeProxy implements AsyncMessageSubscribe {
249 | private ExecutorService executorService;
250 | private AsyncMessageSubscribe target;
251 |
252 | public MessageSubscribeTemplateProxy(ExecutorService executorService,
253 | MessageSubscribeTemplate target) {
254 | this.executorService = executorService;
255 | this.target = target;
256 | }
257 |
258 | public AsyncResult doAction(String s1, String s2) {
259 | AsyncMessageSubscribe_doActionCallable callable = new AsyncMessageSubscribe_doActionCallable(target, "wujiuye", "hello");
260 | Future result = executorService.submit(callable);
261 | AsyncResult asyncResult = AsyncResult.newAsyncResultProxy(result);
262 | return asyncResult;
263 | }
264 | }
265 | ```
266 |
267 | ## 在实现asyncframework中踩的动态字节码实现泛型接口的坑
268 |
269 | asyncframework框架动态实现代理类异步方法的代码源码在`FutureFunctionHandler`这个类中。
270 |
271 | ```java
272 | public class FutureFunctionHandler implements AsyncFunctionHandler{
273 | /**
274 | * asyncMethod有返回值,且返回值类型为Future的处理
275 | *
276 | * @param classWriter 类改写器
277 | * @param interfaceClass 接口
278 | * @param asyncMethod 异步方法
279 | * @param proxyObjClass 接口的实现类
280 | * @param executorServiceClass 线程池的类型
281 | */
282 | @Override
283 | public void doOverrideAsyncFunc(ClassWriter classWriter, Class> interfaceClass, Method asyncMethod, Class> proxyObjClass, Class extends ExecutorService> executorServiceClass) {
284 | ...........
285 | // invoke submit callable
286 | methodVisitor.visitVarInsn(ALOAD, 0);
287 | methodVisitor.visitFieldInsn(GETFIELD, ByteCodeUtils.getProxyClassName(proxyObjClass), "executorService", Type.getDescriptor(executorServiceClass));
288 | methodVisitor.visitVarInsn(ALOAD, index);
289 | if (!executorServiceClass.isInterface()) {
290 | methodVisitor.visitMethodInsn(INVOKEVIRTUAL, executorServiceClass.getName().replace(".", "/"),
291 | "submit", ByteCodeUtils.getFuncDesc(Future.class, Callable.class), false);
292 | } else {
293 | methodVisitor.visitMethodInsn(INVOKEINTERFACE, executorServiceClass.getName().replace(".", "/"),
294 | "submit", ByteCodeUtils.getFuncDesc(Future.class, Callable.class), true);
295 | }
296 | // 将返回值存到操作数栈
297 | methodVisitor.visitVarInsn(ASTORE, ++index);
298 |
299 | // 再来一层代理,对外部屏蔽线程阻塞等待
300 | methodVisitor.visitVarInsn(ALOAD, index);
301 | methodVisitor.visitMethodInsn(INVOKESTATIC, AsyncResult.class.getName().replace(".", "/"),
302 | "newAsyncResultProxy", ByteCodeUtils.getFuncDesc(AsyncResult.class, Future.class),
303 | false);
304 |
305 | methodVisitor.visitInsn(ARETURN);
306 | ..............
307 | }
308 | }
309 | ```
310 |
311 | 线程池在调用`AsyncMessageSubscribe_doActionCallable`这个`Callable`的时候,它查找的call方法的方法描述符是()Ljava.lang.Object;。因为`Callable`是个泛型接口。
312 |
313 | 如果把实现类的签名和实现的`call`方法的签名改为下面这样反而不行。
314 |
315 | ```
316 | 类的签名:Ljava/lang/Object;Ljava/util/concurrent/Callable;>;"
317 |
318 | call方法的签名:
319 | ()Lcom/wujiuye/asyncframework/handler/async/AsyncResult;
320 | ```
321 |
322 | 因为泛型``编译后的描述符是`Ljava.lang.Object;`。
323 |
324 | 如`AsyncResult`泛型类。(选部分)
325 |
326 | ```java
327 | public class AsyncResult implements Future {
328 |
329 | private T result;
330 |
331 | @Override
332 | public T get() throws InterruptedException, ExecutionException {
333 | return result;
334 | }
335 |
336 | }
337 | ```
338 |
339 | `AsyncResult`泛型类编译后的字节码信息。(选部分)
340 |
341 | ```
342 | public class com.wujiuye.asyncframework.handler.async.AsyncResult implements java.util.concurrent.Future {
343 | private T result;
344 | descriptor: Ljava/lang/Object;
345 |
346 | public T get() throws java.lang.InterruptedException, java.util.concurrent.ExecutionException;
347 | descriptor: ()Ljava/lang/Object;
348 | Code:
349 | 0: aload_0
350 | 1: getfield #2 // Field result:Ljava/lang/Object;
351 | 4: areturn
352 | ```
353 |
354 | 类型`T`的`descriptor(类型描述符)`为`Ljava/lang/Object;`,以及`get`方法中,`getfield`指令指定的类型描述符也是`Ljava/lang/Object;`。
355 |
356 | `Callable`接口也是泛型接口,编译后`call`方法的方法描述符便是`()Ljava.lang.Object;`。
357 |
358 | ```
359 | @FunctionalInterface
360 | public interface Callable {
361 | V call() throws Exception;
362 | }
363 | ```
364 |
365 | 所以,如果通过字节码实现`Callable`接口,`call`方法不要设置方法签名,设置方法签名意味着也要改变方法的描述符,一改变就会导致线程池中调用这个`Callable`的`call`方法抛出抽象方法调用错误,原因是根据`Callable`接口的`call`方法的描述符在这个`Callable`对象的类(Class)中找不到对应的`call`方法。
366 |
367 | ---
368 |
369 | A:既然`spring`都已经提供这样的功能,你为什么还要实现一个这样的框架呢?
370 |
371 | Q:因为我之前写组件的时候有需要用到,但又不想为了使用这个功能就把spring依赖到项目中,会比较臃肿。其次,也是因为喜欢折腾,想要把自己的想法实现。
372 |
373 | `asyncframework`可以取代`spring`的`@Async`使用,只要封装一个`starter`包,依靠`spring`提供的`BeanPostProcess`实现无缝整合。但`spring`都已经提供了,我就不想去造轮子了,`asyncframework`我推荐是在非`spring`项目中使用。
374 |
375 | ---
376 |
377 | 发布于:2021 年 06 月 27 日
作者: [吴就业](https://www.wujiuye.com)
GitHub链接: https://github.com/wujiuye/asyncframework
博客链接:https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/opensourceprojects_asyncframework.md
未经作者许可,禁止转载!
378 |
379 |
--------------------------------------------------------------------------------
/opensourceprojects/beemite.md:
--------------------------------------------------------------------------------
1 | # JavaAgent入门级项目Beemite
2 |
3 | Beemite[^1]是笔者刚开始学习JVM字节码时的一个实战Demo级项目,该项目采用`javaagent`+`asm`动态字节码插桩实现业务代码调用链监控,适合新手入门学习。
4 |
5 | Beemite分两个版本:
6 |
7 | * 旧版本:`master`分支,写于`2018`年年底,实现在类加载之前改写字节码,插入埋点;
8 | * 新版本:`agentmain`分支,使用`Attach API`,在`Java`程序运行期间改写字节码,重新加载类。
9 |
10 | 主要实现的功能有:
11 | * 业务代码调用链插桩,在方法执行之前拦截获取类名、方法名,方法调用的参数,在方法执行异常时,获取到异常信息;
12 | * 为统计方法执行时间插入埋点,在方法执行之前和返回之前获取系统时间。
13 |
14 | ### 使用方法
15 |
16 | #### 旧版本
17 |
18 | 1、将`bee-mite`模块执行`maven package`进行打包,获取`jar`包的绝对路径。
19 |
20 | 2、以项目中提供的`demo`为例,在`IDEA`中,在`bee-mite-webdemo`项目下,点击锤子->`Edit Config....`-> `VM options` ->输入下面内容
21 |
22 | ```
23 | -javaagent:/MyProjects/asm-aop/insert-pile/target/bee-mite-1.2.0-jar-with-dependencies.jar=com.wujiuye
24 | ```
25 | 等号后面是参数,意思是改写哪个包下的类。
26 |
27 | 如果报如下异常:
28 | ```
29 | java.lang.VerifyError: Expecting a stackmap frame at branch target 18
30 | ```
31 | jdk1.8可以添加参数:`-noverify`解决
32 | ```
33 | -noverify -javaagent:/MyProjects/asm-aop/insert-pile/target/insert-pile-1.0-SNAPSHOT.jar=com.wujiuye
34 | ```
35 |
36 | #### 新版本
37 |
38 | * 1、先将`bee-mite`模块执行`maven package`进行打包;
39 | * 2、将`bee-mite-boot`模块打包,或者直接在`idea`中启动。
40 |
41 | `bee-mite-boot`这个模块只有一个类,就是`BeemiteBoot`,它的作用就是查询系统当前有哪些`java`进程,获取到进程的`id`,然后根据进程`id`,将上一步编译的`bee-mite`的`jar`包加载到目标进程。
42 |
43 | 需要告诉`BeemiteBoot`,`bee-mite`的`jar`包放在哪里,就是`bee-mite`的`jar`包的绝对路径。目前我是写死在代码中的,可以通过修改代码替换。
44 |
45 | ```text
46 | try {
47 | vm.loadAgent("/Users/wjy/MyProjects/beemite/bee-mite/target/bee-mite-1.2.0-jar-with-dependencies.jar", pageckName + "`" + plus);
48 | } finally {
49 | vm.detach();
50 | }
51 | ```
52 |
53 | 当`BeemiteBoot`启动起来之后,就可以根据提示一步步操作了。
54 |
55 | ```
56 | 找到如下Java进程,请选择:
57 | [1] 2352
58 | [2] 3818 com.wujiuye.ipweb.DemoAppcliction
59 | [3] 3595 org.jetbrains.idea.maven.server.RemoteMavenServer36
60 | [4] 3821 org.jetbrains.jps.cmdline.Launcher
61 | ```
62 |
63 | 选择对应进程之后,会提示输入目标进程的应用包名,目的是过滤只改写指定包名下的类。
64 | ```text
65 | 应用的包名:
66 | com.wujiuye
67 | ```
68 |
69 | 输入包名后会提示选择插件,目前就两个插件,一个是监控调用链的,另一个是打印方法执行耗时的,多可选。
70 | ```text
71 | 选择插件:
72 | 1 打印调用链路插件
73 | 2 打印方法执行耗时插件
74 | 结束请输入:0
75 | ```
76 | 完成后,就已经成功将`bee-mite`的`jar`包加载到目标进程了。
77 |
78 | #### 结果展示
79 |
80 | `bee-mite-boot`是一个`web`项目,在浏览器中输入`http://127.0.0.1:8080/user/wujiuye/25`,即可看到插桩后输出的日记
81 |
82 | ```text
83 | [接收到事件,打印日记]savaFuncStartRuntimeLog[null,com/wujiuye/ipweb/handler/UserHandler,queryUser,1585486646788]
84 | [接收到事件,打印日记]savaBusinessFuncCallLog[null,com/wujiuye/ipweb/handler/UserHandler,queryUser]
85 | [接收到事件,打印日记]savaFuncStartRuntimeLog[null,com/wujiuye/ipweb/service/impl/UserServiceImpl,queryUser,1585486646790]
86 | [接收到事件,打印日记]savaBusinessFuncCallLog[null,com/wujiuye/ipweb/service/impl/UserServiceImpl,queryUser]
87 | [接收到事件,打印日记]savaFuncEndRuntimeLog[null,com/wujiuye/ipweb/service/impl/UserServiceImpl,queryUser,1585486646791]
88 | [接收到事件,打印日记]savaFuncEndRuntimeLog[null,com/wujiuye/ipweb/handler/UserHandler,queryUser,1585486646791]
89 | ```
90 |
91 | ### 源码导读
92 |
93 | agentmain分支是最新版本的分支,以下是agentmain分支各package说明:
94 |
95 | `core`包:实现字节码插桩,在方法执行之前拦截获取类名、方法名,方法调用的参数,在方法执行异常时,获取到异常信息,以及在方法执行之前和`return`之前获取当前系统时间,可用于统计方法执行耗时。
96 |
97 | `tracsformer`包:代码插桩过滤器,使用责任连模式,对字节码进行多次插桩,每个插桩器只负责自己想要实现的逻辑。
98 |
99 | `event`包:事件的封装,埋点代码抛出的事件放入事件队列,异步分派事件给监听器进行处理。
100 |
101 | `logs`包:提供事件监听器接口,具体实现可扩展,我这里提供了两个默认的实现类,默认的实现类只是将日记打印,在控制台打印日记信息。
102 |
103 | 因为字节码是插入到业务代码中的,当执行业务代码的时候会执行埋点代码,如果处理程序也在业务代码中进行,那么这将是个耗时的操作,影响性能,拖慢一次请求的响应速度,所以当埋点代码执行的时候,Beemite是直接抛出一个事件,让线程池异步消费事件,分派事件给相应的监听器处理,这样就可以执行耗时操作,比如将日记输出到硬盘,再存储到`ES`,便于后期进行项目代码异常排查。
104 |
105 | ### Beemite是怎么改写class的
106 |
107 | Beemite会将改写后的`class`字节码输出到文件,并会在控制台打印输出的文件路径,可以看下输出后的`class`。
108 |
109 | 以`UserServiceImpl`为例,这个类是插桩的目标,来看下对比改写后的代码,到底`bee-mite`改写字节码都做了什么。
110 |
111 | 源代码
112 |
113 | ```java
114 | public class UserServiceImpl {
115 |
116 | public Map queryMap(String username,Integer age) {
117 | Map map = new HashMap<>();
118 | map.put("username", username);
119 | map.put("age", age);
120 | return map;
121 | }
122 |
123 | }
124 | ```
125 |
126 | `Beemite`插桩后
127 |
128 | ```java
129 | @Service
130 | public class UserServiceImpl implements UserService {
131 | public UserServiceImpl() {
132 | }
133 |
134 | public Map queryUser(String var1, Integer var2) {
135 | long var3 = System.currentTimeMillis();
136 | FuncRuntimeEvent.sendFuncStartRuntimeEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var3);
137 | try {
138 | Object[] var8 = new Object[]{var1, var2};
139 | BusinessCallLinkEvent.sendBusinessFuncCallEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var8);
140 | HashMap var9 = new HashMap();
141 | var9.put("username", var1);
142 | var9.put("age", var2);
143 | long var5 = System.currentTimeMillis();
144 | FuncRuntimeEvent.sendFuncEndRuntimeEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var5);
145 | return var9;
146 | } catch (Exception var7) {
147 | BusinessCallLinkEvent.sendBusinessFuncCallThrowableEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var7);
148 | throw var7;
149 | }
150 | }
151 | }
152 | ```
153 |
154 | 因为使用了责任链模式,会对代码进行两次插桩,目的就是为了后面容易扩展功能,其实只是`18`年时候自己的水平问题,没有想到通过暴露切点的方式实现更好,少写字节码。我简单看了下`arthas`的部分源码,它的实现就是改写的字节码只插入三个埋点,其它功能不再操作字节码。
155 |
156 | 相信看了对比你也能知道Beemite都插入了哪些代码,这些代码都是通过`asm`写字节码指令插入的。当然也不是很难,对于新手而言,唯一的难点可能就是`try-catch`代码块的插入。`visitTryCatchBlock`方法的三个`label`的位置,以及`catch`块处理异常算是个难点,我们可以通过在源码类中添加`try-catch`块,然后使用`javap`查看异常处理表,例如。
157 |
158 | ```
159 | * Exception table:
160 | * from to target type
161 | * 0 27 30 Class java/lang/Exception
162 | ```
163 |
164 | 那么三个`label`对应的就是`from`、`to`、`target`,且当`type`为`any`的时候就是`try-finally`。
165 |
166 | ---
167 |
168 | [^1]: https://github.com/wujiuye/beemite
169 |
170 | 发布于:2021 年 06 月 27 日
作者: [吴就业](https://www.wujiuye.com)
GitHub链接: https://github.com/wujiuye/beemite
博客链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/opensourceprojects_vine.md
来源: Github开源项目:beemite,未经作者许可,禁止转载!
171 |
172 |
--------------------------------------------------------------------------------
/opensourceprojects/jcg.md:
--------------------------------------------------------------------------------
1 | # 运行时解析json生成class的json-class-generator
2 |
3 | `JCG`(`json-class-generator`)[^1]是一个可用于运行时根据`json`生成`class`的工具,可能使用场景不多。由于是运行时生成的`class`,所以生成的`class`也只能通过反射去使用。
4 |
5 | ## 技术栈
6 | * `gson`:`json`解析工具;
7 | * `asm`:字节码生成工具;
8 |
9 | ## 特性
10 | * 将`json`解析生成`class`,为`class`添加字段和对应字段的`get`、`set`方法;
11 | * 支持为生成的`class`添加注解,使用注解规则声明将注解添加在类或是字段上;
12 |
13 | ## 基础功能:将json解析生成class
14 |
15 | 为验证结果的正确性,可配置将本工具包生成的`class`输出到文件,通过`idea`打开可以查看生成的`java`代码。
16 |
17 | ```java
18 | public class JsonToClassTest {
19 |
20 | static {
21 | // value为输出的目录
22 | System.setProperty("jcg.classSavaPath", "/Users/wjy/MyProjects/JsonClassGenerator");
23 | }
24 | }
25 | ```
26 |
27 | 默认情况下,当`JSON`有字段为`null`时则会解析失败,如果这些字段不是必须的,那么我们可以通过以下配置让`JCG`忽略这些字段:
28 | ```java
29 | public class JsonToClassTest {
30 |
31 | static {
32 | /**
33 | * 是否忽略空字段,默认为false
34 | */
35 | System.setProperty("jcg.ignore_null_field", "true");
36 | }
37 | }
38 | ```
39 |
40 | 既然要用到动态`json`解析生成`class`,那么说明`json`我们是通过`API`或者读取数据库获取的。为了简单,我们就直接定义`json`字符串来测试了。
41 |
42 | 假设`json`为:
43 | ```json
44 | {
45 | "name":"name",
46 | "price":1.0,
47 | "nodes":[
48 | {
49 | "id":222,
50 | "note":"xxx"
51 | }
52 | ]
53 | }
54 | ```
55 |
56 | 那么我们期望`JCG`工具为我们生成的`class`应该是这样的:
57 | ```java
58 | public class Xxx {
59 | private String name;
60 | private BigDecimal price;
61 | private List nodes;
62 | // get、set方法省略
63 | }
64 | public class Yyy {
65 | private Integer id;
66 | private String note;
67 | // get、set方法省略
68 | }
69 | ```
70 |
71 | 现在我们开始使用`JCG`工具将上面例子中的`json`解析生成`class`
72 | ```java
73 | public class JsonToClassTest {
74 |
75 | static {
76 | System.setProperty("jcg.classSavaPath", "/Users/wjy/MyProjects/JsonClassGenerator");
77 | }
78 |
79 | @Test
80 | public void test() {
81 | String json = "{\"name\":\"offer name\",\"price\":1.0,\"nodes\":[{\"id\":222,\"note\":\"xxx\"}]}";
82 | String className = "com.wujiuye.jcg.model.TestBean";
83 |
84 | // 我们需要为这串json定义一个类型名,
85 | // 然后调用JcgClassFactory的generateResponseClassByJson方法即可生成Class
86 | Class> cls = JcgClassFactory.getInstance()
87 | .generateResponseClassByJson(className, json);
88 | try {
89 | // 验证生成的class是否正确
90 | Object obj = new Gson().fromJson(json, cls);
91 | System.out.println(obj.getClass());
92 | // 验证生成的get/set方法是否能正常调用
93 | Method method = obj.getClass().getDeclaredMethod("getNodes");
94 | Object result = method.invoke(obj);
95 | System.out.println(result);
96 | } catch (Exception e) {
97 | e.printStackTrace();
98 | }
99 | }
100 |
101 | }
102 | ```
103 |
104 | 结果省略。
105 |
106 | ## 特色:为生成的class添加注解
107 |
108 | 为了使用某些框架的特性,我们可能需要在将`json`解析生成`class`时,就需要为`class`添加注解。比如我们想将`JCG`生成的`class`用于后续的`json`解析,那么我们可能需要在`class`上或者字段上添加一些诸如`@JsonIgnore`之类的注解,这些需求`JCG`都可以满足。
109 |
110 | 假设,我们想在解析`json`生成的`class`上添加一个`@TestAnnno`注解,可以这么实现:
111 | ```java
112 | public class JsonToClassTest {
113 |
114 | static {
115 | System.setProperty("jcg.classSavaPath", "/Users/wjy/MyProjects/JsonClassGenerator");
116 | }
117 |
118 | @Test
119 | public void test() {
120 | String json = "{\"name\":\"offer name\",\"price\":1.0,\"nodes\":[{\"id\":222,\"note\":\"xxx\"}]}";
121 | String className = "com.wujiuye.jcg.model.TestBean";
122 |
123 | // 注解规则
124 | AnnotationRule annotationRule = new AnnotationRule(TestAnno.class, ElementType.TYPE, "");
125 | annotationRule.putAttr("value", "122");
126 | // 注册注解规则
127 | AnnotationRuleRegister.registRule(className, annotationRule);
128 |
129 | // 调用JcgClassFactory的generateResponseClassByJson方法即可生成Class
130 | Class> cls = JcgClassFactory.getInstance()
131 | .generateResponseClassByJson(className, json);
132 | }
133 |
134 | }
135 | ```
136 |
137 | 注解规则映射类`AnnotationRule`的构造方法说明:
138 | * 参数`1`:要添加的注解的类型;
139 | * 参数`2`:注解在类上还是字段上,第二个参数和第三个参数需要配和使用。(只支持添加在类上、添加在字段上两种类型);
140 | * 参数`3`:添加的路径,`JCG`根据路径通过深度遍历寻找目标字段,如果是添加在类上,则会加在目录字段对应的`class`上;
141 |
142 | 如果参数`2`为`ElementType.TYPE`,参数`3`为`""`,那么结果就是在`json`生成的`class`上添加注解;如果参数`2`为`ElementType.FIELD`,参数`3`为`""`则会报错,但如果参数`3`为`"name"`,则是在`name`字段上添加注解。
143 |
144 | 在前面给出的`json`例子中,由于`nodes`字段是一个数组,且数组元素不是基本数据类型,因此`JCG`也会为数组元素生成一个`class`。如果想为`nodes`元素类型对应的`class`也添加注解,那么可以通过`path`实现。例如:
145 |
146 | * 参数`2`为`ElementType.TYPE`,参数`3`为`"nodes"`,则会在`nodes`元素对应的类上添加注解;
147 | * 参数`2`为`ElementType.FIELD`,参数`3`为`"nodes"`,则只是在`nodes`字段上添加注解;
148 | * 参数`2`为`ElementType.FIELD`,参数`3`为`"nodes.id"`,则会在`nodes`元素对应的类的`id`字段上添加注解;
149 |
150 | 代码如下
151 | ```java
152 | public class JsonToClassTest {
153 |
154 | static {
155 | System.setProperty("jcg.classSavaPath", "/Users/wjy/MyProjects/JsonClassGenerator");
156 | }
157 |
158 | @Test
159 | public void test() {
160 | String json = "{\"name\":\"offer name\",\"price\":1.0,\"nodes\":[{\"id\":222,\"note\":\"xxx\"}]}";
161 | String className = "com.wujiuye.jcg.model.TestBean";
162 |
163 | // 给nodes数组的元素类型class添加@TestAnno
164 | AnnotationRule fieldRule = new AnnotationRule(TestAnno.class, ElementType.TYPE, "nodes");
165 | fieldRule.putAttr("value", "12233");
166 | AnnotationRuleRegister.registRule(className, fieldRule);
167 |
168 | // 给nodes数组元素类型class的id字段添加注解@TestAnno
169 | AnnotationRule fieldClassRule = new AnnotationRule(TestAnno.class, ElementType.FIELD, "nodes.id");
170 | fieldClassRule.putAttr("type", ElementType.FIELD);
171 | AnnotationRuleRegister.registRule(className, fieldClassRule);
172 |
173 | // 调用JcgClassFactory的generateResponseClassByJson方法即可生成Class
174 | Class> cls = JcgClassFactory.getInstance()
175 | .generateResponseClassByJson(className, json);
176 | }
177 |
178 | }
179 | ```
180 |
181 | 注解规则映射类`AnnotationRule`的`putAttr`方法说明:
182 | * `key`: 对应注解的属性名;
183 | * `value`: 对应注解的属性值,属性值必须是基本数据类型、`String`、枚举、注解,目前不支持数组;
184 |
185 | 如果注解的属性也是注解类型,那么可以通过`putAttr`方法给`AnnotationRule`添加一个`AnnotationRule`实现,代理如下:
186 |
187 | ```java
188 | public class JsonToClassTest {
189 |
190 | static {
191 | System.setProperty("jcg.classSavaPath", "/Users/wjy/MyProjects/JsonClassGenerator");
192 | }
193 |
194 | @Test
195 | public void test() {
196 | String json = "{\"name\":\"offer name\",\"price\":1.0,\"nodes\":[{\"id\":222,\"note\":\"xxx\"}]}";
197 | String className = "com.wujiuye.jcg.model.TestBean";
198 |
199 | AnnotationRule fieldRule = new AnnotationRule(TestAnno.class, ElementType.TYPE, "nodes");
200 | fieldRule.putAttr("value", "12233");
201 | // 设置@TestAnno的map属性,该属性类型为注解类型
202 | AnnotationRule annoRule = new AnnotationRule(Map.class, null, "");
203 | annoRule.putAttr("value", "haha");
204 | //
205 | fieldRule.putAttr("map", annoRule);
206 | AnnotationRuleRegister.registRule(className, fieldRule);
207 |
208 | // 调用JcgClassFactory的generateResponseClassByJson方法即可生成Class
209 | Class> cls = JcgClassFactory.getInstance()
210 | .generateResponseClassByJson(className, json);
211 | }
212 |
213 | }
214 | ```
215 |
216 | 生成的结果如下:
217 | ```java
218 | @TestAnno(
219 | value = "12233",
220 | map = @Map("haha")
221 | )
222 | public class TestBean$$Nodes {
223 | private Integer id;
224 | private String note;
225 | // get/set省略
226 | }
227 | ```
228 |
229 | `TestBean$$Nodes`是`JCG`为`json`中的`nodes`字段生成的一个`class`。
230 |
231 | ## 复杂注解的支持
232 |
233 | 针对注解属性是数组的玩法比较复杂,`JCG`在`1.0.1`版本开始支持,使用方法如下:
234 | ```java
235 | /**
236 | * @author wujiuye 2020/06/08
237 | */
238 | public class JsonToClassTest {
239 |
240 | static {
241 | System.setProperty("jcg.classSavaPath", "/Users/wjy/MyProjects/JsonClassGenerator");
242 | }
243 |
244 | @Test
245 | public void test2() throws ClassNotFoundException {
246 | String json = "{\"name\":\"offer name\",\"price\":1.0,\"nodes\":[{\"id\":222,\"note\":\"xxx\"}]}";
247 | String className = "com.wujiuye.jcg.model.OfferBean";
248 |
249 | AnnotationRule fieldRule = new AnnotationRule(TestAnno.class, ElementType.TYPE, "nodes");
250 | fieldRule.putAttr("value", "12233");
251 |
252 | // 注解的属性类型为注解数组的写法
253 | AnnotationRule[] mapArray = new AnnotationRule[2];
254 | AnnotationRule annoRule = new AnnotationRule(Map.class, null, "");
255 | annoRule.putAttr("value", "haha");
256 | mapArray[0] = annoRule;
257 | mapArray[1] = annoRule;
258 | fieldRule.putAttr("mapArray", mapArray);
259 |
260 | // 注解的属性类型为枚举数组的写法
261 | ElementType[] typeArray = new ElementType[2];
262 | typeArray[0] = ElementType.TYPE;
263 | typeArray[1] = ElementType.FIELD;
264 | fieldRule.putAttr("typeArray", typeArray);
265 |
266 | AnnotationRuleRegister.registRule(className, fieldRule);
267 |
268 | Class> cls = JcgClassFactory.getInstance().generateResponseClassByJson(className, json);
269 | try {
270 | Object obj = new Gson().fromJson(json, cls);
271 | System.out.println(obj.getClass());
272 | Method method = obj.getClass().getDeclaredMethod("getNodes");
273 | Object result = method.invoke(obj);
274 | System.out.println(result);
275 | } catch (Exception e) {
276 | e.printStackTrace();
277 | }
278 | }
279 |
280 | }
281 | ```
282 |
283 | ---
284 |
285 | [^1]: https://github.com/wujiuye/json-class-generator
286 |
287 | 发布于:2021 年 06 月 27 日
作者: [吴就业](https://www.wujiuye.com)
GitHub链接: https://github.com/wujiuye/json-class-generator
博客链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/opensourceprojects_json-class-generator.md
来源: Github开源项目:json-class-generator,未经作者许可,禁止转载!
288 |
289 |
--------------------------------------------------------------------------------
/opensourceprojects/vine.md:
--------------------------------------------------------------------------------
1 | # 一款轻量级的分布式调用链路追踪Java探针vine
2 |
3 | vine[^1]是一款轻量级的分布式调用链路追踪-Java探针。对于同进程内,vine在调用链路入口处创建用于收集日记的单向链表, 调用链路上每经过一个方法都会向链表尾部追加日记,最终在调用链路出口处打印整个调用链路的日记;不同进程间,通过传递事件ID将整条链路串连起来。
4 |
5 | * vine目前已经适配OpenFeign、WebMvc框架;
6 | * vine通过类加载器实现与Spring Boot应用环境隔离,各自依赖的jar包不受影响;
7 | * vine还支持配置采样率,降低对应用性能的损耗;
8 | * vine使用logback输出日记,对于同进程内,一次调用链路只打印一次日记。
9 |
10 | vine只是探针,不负责日记的收集。 如果您项目中也是将日记收集到阿里云日记服务(或者其它日记服务),那么可以使用vine将调用链路日记输出到阿里云日记服务(或者其它日记服务)。
11 |
12 | ## 使用说明
13 |
14 | 启动示例:
15 | ```shell
16 | java -javaagent:{绝对路径}/vine-agent.jar={绝对路径}/vine-core-jar-with-dependencies.jar,\
17 | {绝对路径}/vine-spy.jar=agent.package={应用包名,如:com.wujiuye.app} -jar {应用的jar包}
18 | ```
19 |
20 | Docker镜像构建配置文件示例:
21 | ```shell
22 | FROM java:8-jdk-alpine
23 |
24 | WORKDIR /usr/app
25 |
26 | COPY ./test-app.jar ./
27 |
28 | COPY ./agent/vine-agent.jar ./
29 | COPY ./agent/vine-core-jar-with-dependencies.jar ./
30 | COPY ./agent/vine-spy.jar ./
31 |
32 | ENTRYPOINT ["java", "-server", "-XX:+UseG1GC",\
33 | "-javaagent:/usr/app/vine-agent.jar=/usr/app/vine-core-jar-with-dependencies.jar,/usr/app/vine-spy.jar=agent.package=com.test",\
34 | "-jar", "test-app.jar"]
35 | ```
36 |
37 | ## 各模块说明
38 | * vine-spy: 定义Spy和ContextSpy类,这两个类由启动类加载器加载,定义了方法入口和出口静态方法,由应用代码中的埋点代码调用;
39 | * vine-core: 探针的核心功能实现,实现class字节码修改、接收Spy、ContextSpy上报的事件、构造调用链路、日记输出,适配主流框架(webmvc、openfeign);
40 | * vine-agent: 定义premain、agentmain方法,负责使用不同类加载器加载vine-spy.jar与vine-core.jar;
41 |
42 | ## vine的核心设计
43 |
44 | ### 实现环境隔离
45 | 使用不同类加载器加载不同模块,实现环境隔离:
46 | * vine-spy.jar:由启动类加载器加载;
47 | * vine-core.jar:自定义的类加载器加载,与应用隔离;
48 | * vine-agent.jar:由系统类加载器加载;
49 |
50 |
51 |
52 | ---
53 |
54 | [^1]: https://github.com/wujiuye/vine
55 |
56 | 发布于:2021 年 06 月 27 日
作者: [吴就业](https://www.wujiuye.com)
GitHub链接: https://github.com/wujiuye/vine
博客链接: https://www.wujiuye.com/ebook/JVMByteCodeGitBook/chapter/opensourceprojects_beemite.md
来源: Github开源项目:vine,未经作者许可,禁止转载!
57 |
58 |
--------------------------------------------------------------------------------
/qrcode/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/qrcode/.DS_Store
--------------------------------------------------------------------------------
/qrcode/wujiuye_dashang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wujiuye/JVMByteCodeGitBook/73736309d1ea2c776cc502bedf1044ce5bdcc804/qrcode/wujiuye_dashang.png
--------------------------------------------------------------------------------
/xuyan.md:
--------------------------------------------------------------------------------
1 | # 序言
2 |
3 | ## 为什么写这本书
4 |
5 | 笔者曾在学习Java虚拟机字节码的过程中遇到过很多问题,也曾浪费不少时间去查阅资料,在学习ASM框架时,更是苦于找不到系统且详细的介绍ASM框架如何使用的资料,而选择自己看API文档摸索,学习过程中遇到的一些问题都需要花费大量时间去解决。例如,使用IDEA查看字节码反编译后的Java代码看似没问题,但总能遇到各种VerifyError。
6 |
7 | 在开始写作本书时,市面上还没有一本适合新手入门Java虚拟机字节码的书籍,从网上找的资料也是零零散散,笔者之前也写过一些关于Java虚拟机字节码方面的文章,看到很多读者留言:希望作者能写一篇介绍ASM框架的使用,网上找不到合适的资料,看英文API文档又看不懂、希望作者能详细介绍一下字节码指令的执行过程等。
8 |
9 | 因此,笔者下定决心一定要完成这本书。本书将归纳以及提炼知识点,为读者制定合理的学习路线,帮助读者更快的掌握Java虚拟机字节码技术,了解字节码背后的执行原理,以及帮助读者快速入门使用ASM框架操作字节码。
10 |
11 | ## 读者对象
12 |
13 | * Java开发工程师;
14 |
15 | * Java虚拟机字节码发烧友;
16 |
17 | * 想了解动态代理实现原理的读者;
18 |
19 | * 想了解字节码插桩实现原理的读者;
20 |
21 | ## 本书介绍
22 |
23 | 这本书详细介绍ASM框架的API、Class文件结构解析、HotSpot虚拟机栈大小分配与类加载阶段的源码分析、动态代理与字节码插桩的实现。其内容安排如下:
24 |
25 | 第一章:介绍Java虚拟机基础知识,了解虚拟机栈、栈帧、局部变量表、操作数栈是理解字节码指令的基础。
26 |
27 | 第二章:通过使用Java代码结合设计模式实现一个解析class文件结构的工具分析Java代码编译后生成的class文件的结构。
28 |
29 | 第三章:介绍字节码指令,与学习一门语言一样,学习底层字节码是如何实现各种条件分支语句、循环语句,以及try-catch代码块的。透过字节码了解try-catch-finally是怎么实现的,以及try-with-resource语法糖是怎么实现的。最后分析四条常用的方法调用指令在使用上的区别。
30 |
31 | 第四章:简析类的加载过程,并介绍双亲委派模型。也为后续章节编写自定义类加载器加载动态生成或改写类的字节码做铺垫。本章深入分析HotSpot虚拟机类加载部分源码,重点介绍字节码验证阶段,探索VerifyError的由来。
32 |
33 | 第五章:介绍ASM框架的使用、介绍Javassist与ASM的不同点与各自的优缺点。通过使用访问者模式实现一个简单的class字节码操作框架介绍ASM框架的实现原理,帮助读者更好的理解ASM的API。通过使用ASM操作字节码实现创建类和方法、改写类和方法、实现接口、继承父类覆写方法等,熟悉ASM框架的API。
34 |
35 | 第六章、第七章:通过实战掌握前面所学的知识点,学以致用。所选案例皆是常见的动态字节码技术使用场景。如实现两种不同方式的动态代理、实现APM系统中的字节码插桩。
36 |
37 | 第八章:补充一些知识点,如类型检查与栈映射桢、泛型以及泛型方法的调用。
38 |
39 | 本书Class文件结构参考《Java虚拟机规范》Java SE 8版[^1]。
40 |
41 | ## 源码下载
42 | * OpenJDK Github源码下载地址: [OpenJDK下载(不同版本对应不同分支)](https://github.com/unofficial-openjdk/openjdk/tree/jdk8u/jdk8u)
43 | * 本书中的案例代码下载地址:[https://github.com/wujiuye/bytecode-book](https://github.com/wujiuye/bytecode-book)
44 |
45 | [^ 1]:《Java虚拟机规范》是Java领域最重要和最权威的著作之一,Java SE 8版完整且详细的讲解基于Java SE 8的Java虚拟机规范。
46 |
47 | ## 打赏作者
48 |
49 |
--------------------------------------------------------------------------------