1.前言
在《【编译引擎】学习阅读Class文件结构(16进制版)》中,我们一起直接阅读了Class文件的16进制版本。
虽然这种方式可以帮助我们深刻理解Class文件结构,但如果您从事的是应用软件开发(而不是编译器相关工作),这样就比较低效了。
今天我们就来看看JVM提供的javap,如何提升我们解读Class结构的工作效率。
2.javap
2.1.命令行学习方法
最好的学习资源就是官方文档:
https://docs.oracle.com/en/java/javase/11/tools/javap.html#GUID-BE20562C-912A-4F91-85CF-24909F212D7F
在这里,我们可以获得最全面的javap使用指导。
2.2.命令行详解
2.2.1.javap的命令行结构
javap [options] classfiles…
[options]:javap的命令行选项
classfiles:是我们需要反汇编的一个或多个类文件。
2.2.2.最常用的options
实战中,比较常用的options是输出所有显示所有类和成员、输出类的附件信息(如:堆栈大小、局部变量数量和方法的参数)
javap -v -p Test.class
其中,
- -v:输出类的附件信息(如:堆栈大小、局部变量数量和方法的参数)
- -p:显示所有的类和成员
2.2.3.梳理options
在官方文档中,javap的options很多,笔者做了这样的归类:
-l:打印行和局部变量表
-package:显示程序包/受保护的/公共类和成员 (默认)
-public:只显示公共类和成员
-protected:只显示受保护的和公共的类和成员
-p -private:显示所有的类和成员
-s:打印内部类型签名
-constants:显示static final常量
-c:打印类中每个方法的反汇编代码,例如,组成Java字节码的指令
-v,-verbose:打印堆栈大小、局部变量数量和方法的参数
-classpath ,-cp :指定javap命令用于查找类的路径。覆盖默认的CLASSPATH环境变量
-bootclasspath :指定加载引导类的路径。默认情况下,引导类是实现位于jre/lib/rt.jar和其它几个jar
-extdir dirs:覆盖扩展类的位置。扩展的默认位置是java.ext.dirs的值
Joption:将指定的选项传递给JVM(JVM的options详见java命令文档)
eg:
javap -J-version
javap -J-Djava.security.manager -J-Djava.security.policy=MyPolicy MyClassName
-sysinfo:显示正在处理的类的系统信息(路径、大小、日期、MD5哈希值)
2.2.4.javap的形和神
前面解读了javap的命令行手册,这些只能算作javap的形,也比较好掌握,我们看看官方文档的javap输出的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| Compiled from "HelloWorldFrame.java"
public class HelloWorldFrame extends javax.swing.JFrame {
java.lang.String message;
public HelloWorldFrame();
Code:
0: aload_0
1: invokespecial #1 // Method javax/swing/JFrame."<init>":()V
4: aload_0
5: ldc #2 // String Hello World!
7: putfield #3 // Field message:Ljava/lang/String;
10: aload_0
11: new #4 // class HelloWorldFrame$1
14: dup
15: aload_0
16: invokespecial #5 // Method HelloWorldFrame$1."<init>":(LHelloWorldFrame;)V
19: invokevirtual #6 // Method setContentPane:(Ljava/awt/Container;)V
22: aload_0
23: bipush 100
25: bipush 100
27: invokevirtual #7 // Method setSize:(II)V
30: return
public static void main(java.lang.String[]);
Code:
0: new #8 // class HelloWorldFrame
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokevirtual #10 // Method setVisible:(Z)V
13: return
}
|
不了解JVM字节码的程序猿依然看不懂,这些就是javap的神。
这就好像辟邪剑法
与辟邪剑谱
的关系。
接下来,我们就来解读一下javap的输出结果——字节码。
3.javap输出结果解读
说明:Java虚拟机规范中很大的篇幅就是约定字节码各个Section的职责、含义、约束,以及JVM指令集,本文不可能一一详尽阐述。因此本文聚焦于解读字节码的总体结构,旨在最快速地为大家建立Class文件结构的脉络,后续案例也会进一步展开字节码技术,对试题进行更有深度的解读。
3.1.字节码的基础知识
为了便于不太了解JVM字节码的程序猿更快进入下一章节,我们简单回顾和小结一下字节码的知识:
- STEP1.我们通过java命令,将.java文件转化为.class文件(也就是字节码)
- STEP2.字节码文件本身是什么呢?我们可以用16进制编辑器打开它
字节码文件本身就是一串字节流。
- STEP3.为了看的更加清楚一些,我们将有关联的字节用同一种颜色着色:
如果您做过网络协议的开发,会发现字节码(字节流)与协议栈(如:ModBus)的逻辑很类似,几个字节为一组表达一个信息。
- STEP4.抽象一下上述着色字节流,我们可以将Class文件结构抽象如下
字节码基础属性
常量池:占据字节码文件最大的篇幅
类的基本信息:包含类名的索引、类访问标识、父类名的索引、实现了多少接口等信息
字段列表:包含有多少字段,每个字段名的索引、访问标识等
方法列表:包含多少方法,每个方法名的索引、访问标识、方法的实现等
附加属性
3.2.通过示例代码,解读javap的输出
我们以一段示例代码,来解读javap的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
| javap -v -p Demo2.class
Classfile /C:/Demo2.class
Last modified 2021-1-25; size 613 bytes
MD5 checksum f7c661d99330a1eefb32b6429e5a48b4
Compiled from "Demo2.java"
public class com.firelord.zsample.lang.jvm.frontcompiler.Demo2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #6.#22 // com/firelord/zsample/lang/jvm/frontcompiler/Demo2.field1:I
#3 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #25 // hello world
#5 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #28 // com/firelord/zsample/lang/jvm/frontcompiler/Demo2
#7 = Class #29 // java/lang/Object
#8 = Utf8 field1
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/firelord/zsample/lang/jvm/frontcompiler/Demo2;
#17 = Utf8 hello
#18 = Utf8 i
#19 = Utf8 SourceFile
#20 = Utf8 Demo2.java
#21 = NameAndType #10:#11 // "<init>":()V
#22 = NameAndType #8:#9 // field1:I
#23 = Class #30 // java/lang/System
#24 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#25 = Utf8 hello world
#26 = Class #33 // java/io/PrintStream
#27 = NameAndType #34:#35 // println:(Ljava/lang/String;)V
#28 = Utf8 com/firelord/zsample/lang/jvm/frontcompiler/Demo2
#29 = Utf8 java/lang/Object
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 java/io/PrintStream
#34 = Utf8 println
#35 = Utf8 (Ljava/lang/String;)V
{
private int field1;
descriptor: I
flags: ACC_PRIVATE
public com.firelord.zsample.lang.jvm.frontcompiler.Demo2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field field1:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/firelord/zsample/lang/jvm/frontcompiler/Demo2;
public void hello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: iconst_1
1: istore_1
2: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
5: ldc #4 // String hello world
7: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 7: 0
line 8: 2
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/firelord/zsample/lang/jvm/frontcompiler/Demo2;
2 9 1 i I
}
SourceFile: "Demo2.java"
|
3.2.1.Class文件基础信息
Class文件的基础信息包括:
- class文件的路径
- class文件的修改时间、class文件的大小
- class文件的MD5值
- java文件的名称
- jdk的大版本/小版本号
3.3.2.常量池
常量池的细节知识有很多,但是有3个关键点:
- 访问权限、字符串等等,都是JVM所认为的"常量”
- 常量池中存储了多种类型的常量
- 常量之间以类似"指针"的形式来表达源代码
例如:class Demo2,那么常量池有就会有一个UTF-8类型的常量表示"Demo2”,还会有一个类的符号引用指向"Demo2"这个UTF-8的常量。
3.3.3.访问标识、类索引
在常量池的基础上,JVM首先要表达源文件中的类,类的关键要素包括:
3.3.4.字段表
进一步,字节码要表达:
- Demo2类中有几个字段:javap的输出告诉我们,Demo2只有1个字段field1
- field1字段的数据类型:从输出可以看到,field1是int类型
- field1字段的访问权限:从输出可以看出,field1是private
3.3.5.方法表
更进一步,字节码要表达:
- Demo2@类中有几个方法:本例中,有一个默认的构造函数,还有一个hello方法
- 方法的原型:从输出看,hello方法的访问权限是public,返回值是void,没有输入参数
- 方法的具体实现:从输出看,hello方法被转换为了73~88行的JVM指令序列、行号表、局部变量表。
说明:本文篇幅有限,无法展开讲解hello方法的指令执行过程,以及如何动态set/get局部变量表。有兴趣的读者可以尝试解读一下hello方法的指令执行过程。
4.总结
本文解读了javap的使用以及javap的输出结果,具体如下:
- 理解字节码以及JVM价值
- javap的学习方法
- javap命令行常用option
- javap命令行的options解读
- 字节码的主体结构
- 通过一个示例代码,演练了javap输出结果中各个section的含义
5.参考资料
https://docs.oracle.com/en/java/javase/11/tools/javap.html#GUID-BE20562C-912A-4F91-85CF-24909F212D7F
《深入理解Java虚拟机:JVM高级特性与最佳实践》