1.前言

在《【编译引擎】学习阅读Class文件结构(16进制版)》中,我们一起直接阅读了Class文件的16进制版本。

虽然这种方式可以帮助我们深刻理解Class文件结构,但如果您从事的是应用软件开发(而不是编译器相关工作),这样就比较低效了。

今天我们就来看看JVM提供的javap,如何提升我们解读Class结构的工作效率。

image-20210129115436313

2.javap

2.1.命令行学习方法

最好的学习资源就是官方文档:

https://docs.oracle.com/en/java/javase/11/tools/javap.html#GUID-BE20562C-912A-4F91-85CF-24909F212D7F

在这里,我们可以获得最全面的javap使用指导。

image-20210604135122739

2.2.命令行详解

2.2.1.javap的命令行结构

javap [options] classfiles…

[options]:javap的命令行选项

classfiles:是我们需要反汇编的一个或多个类文件。

image-20210604135556794

2.2.2.最常用的options

实战中,比较常用的options是输出所有显示所有类和成员、输出类的附件信息(如:堆栈大小、局部变量数量和方法的参数)

javap -v -p Test.class

其中,

  • -v:输出类的附件信息(如:堆栈大小、局部变量数量和方法的参数)
  • -p:显示所有的类和成员

2.2.3.梳理options

在官方文档中,javap的options很多,笔者做了这样的归类:

  • 与类有关的options:

-l:打印行和局部变量表

-package:显示程序包/受保护的/公共类和成员 (默认)

-public:只显示公共类和成员

-protected:只显示受保护的和公共的类和成员

-p -private:显示所有的类和成员

-s:打印内部类型签名

-constants:显示static final常量

-c:打印类中每个方法的反汇编代码,例如,组成Java字节码的指令

-v,-verbose:打印堆栈大小、局部变量数量和方法的参数

  • 与JVM有关的options:

-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的输出结果——字节码。

image-20210604142536789

3.javap输出结果解读

说明:Java虚拟机规范中很大的篇幅就是约定字节码各个Section的职责、含义、约束,以及JVM指令集,本文不可能一一详尽阐述。因此本文聚焦于解读字节码的总体结构,旨在最快速地为大家建立Class文件结构的脉络,后续案例也会进一步展开字节码技术,对试题进行更有深度的解读。

3.1.字节码的基础知识

为了便于不太了解JVM字节码的程序猿更快进入下一章节,我们简单回顾和小结一下字节码的知识:

  • STEP1.我们通过java命令,将.java文件转化为.class文件(也就是字节码)
  • STEP2.字节码文件本身是什么呢?我们可以用16进制编辑器打开它

字节码文件本身就是一串字节流。

image-20210604144157084

  • STEP3.为了看的更加清楚一些,我们将有关联的字节用同一种颜色着色:

如果您做过网络协议的开发,会发现字节码(字节流)与协议栈(如:ModBus)的逻辑很类似,几个字节为一组表达一个信息。

image-20210604144211764

  • STEP4.抽象一下上述着色字节流,我们可以将Class文件结构抽象如下

字节码基础属性

常量池:占据字节码文件最大的篇幅

类的基本信息:包含类名的索引、类访问标识、父类名的索引、实现了多少接口等信息

字段列表:包含有多少字段,每个字段名的索引、访问标识等

方法列表:包含多少方法,每个方法名的索引、访问标识、方法的实现等

附加属性

image-20210604145305097

3.2.通过示例代码,解读javap的输出

我们以一段示例代码,来解读javap的输出:

  • 示例代码:

image-20210604150455456

  • 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的大版本/小版本号

image-20210604151205810

3.3.2.常量池

常量池的细节知识有很多,但是有3个关键点:

  • 访问权限、字符串等等,都是JVM所认为的"常量”
  • 常量池中存储了多种类型的常量
  • 常量之间以类似"指针"的形式来表达源代码

例如:class Demo2,那么常量池有就会有一个UTF-8类型的常量表示"Demo2”,还会有一个类的符号引用指向"Demo2"这个UTF-8的常量。

image-20210604151237501

3.3.3.访问标识、类索引

在常量池的基础上,JVM首先要表达源文件中的类,类的关键要素包括:

  • 类名:javap的输出结果中有多处呈现了类名是Demo2。

  • 类的访问权限:本例中,javap的输出就是表示Demo2类是public的,并且继承于Object类

image-20210604151731952

3.3.4.字段表

进一步,字节码要表达:

  • Demo2类中有几个字段:javap的输出告诉我们,Demo2只有1个字段field1
  • field1字段的数据类型:从输出可以看到,field1是int类型
  • field1字段的访问权限:从输出可以看出,field1是private

image-20210604152056658

3.3.5.方法表

更进一步,字节码要表达:

  • Demo2@类中有几个方法:本例中,有一个默认的构造函数,还有一个hello方法
  • 方法的原型:从输出看,hello方法的访问权限是public,返回值是void,没有输入参数
  • 方法的具体实现:从输出看,hello方法被转换为了73~88行的JVM指令序列、行号表、局部变量表。

image-20210604152334770

说明:本文篇幅有限,无法展开讲解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高级特性与最佳实践》