<audio id="audio" title="【工具篇】 常用工具介绍" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bb/08/bb192dd3752a1a6577e4653532582b08.mp3"></audio>

在前面的文章中，我曾使用了不少工具来辅助讲解，也收到了不少同学留言，说不了解这些工具，不知道都有什么用，应该怎么用。那么今天我便统一做一次具体的介绍。本篇代码较多，你可以点击文稿查看。

## javap：查阅Java字节码

javap是一个能够将class文件反汇编成人类可读格式的工具。在本专栏中，我们经常借助这个工具来查阅Java字节码。

举个例子，在讲解异常处理那一篇中，我曾经展示过这么一段代码。

```
public class Foo {
  private int tryBlock;
  private int catchBlock;
  private int finallyBlock;
  private int methodExit;

  public void test() {
    try {
      tryBlock = 0;
    } catch (Exception e) {
      catchBlock = 1;
    } finally {
      finallyBlock = 2;
    }
    methodExit = 3;
  }
}

```

编译过后，我们便可以使用javap来查阅Foo.test方法的字节码。

```
$ javac Foo.java
$ javap -p -v Foo
Classfile ../Foo.class
  Last modified ..; size 541 bytes
  MD5 checksum 3828cdfbba56fea1da6c8d94fd13b20d
  Compiled from &quot;Foo.java&quot;
public class Foo
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // Foo
  super_class: #8                         // java/lang/Object
  interfaces: 0, fields: 4, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #8.#23         // java/lang/Object.&quot;&lt;init&gt;&quot;:()V
   #2 = Fieldref           #7.#24         // Foo.tryBlock:I
   #3 = Fieldref           #7.#25         // Foo.finallyBlock:I
   #4 = Class              #26            // java/lang/Exception
   #5 = Fieldref           #7.#27         // Foo.catchBlock:I
   #6 = Fieldref           #7.#28         // Foo.methodExit:I
   #7 = Class              #29            // Foo
   #8 = Class              #30            // java/lang/Object
   #9 = Utf8               tryBlock
  #10 = Utf8               I
  #11 = Utf8               catchBlock
  #12 = Utf8               finallyBlock
  #13 = Utf8               methodExit
  #14 = Utf8               &lt;init&gt;
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               test
  #19 = Utf8               StackMapTable
  #20 = Class              #31            // java/lang/Throwable
  #21 = Utf8               SourceFile
  #22 = Utf8               Foo.java
  #23 = NameAndType        #14:#15        // &quot;&lt;init&gt;&quot;:()V
  #24 = NameAndType        #9:#10         // tryBlock:I
  #25 = NameAndType        #12:#10        // finallyBlock:I
  #26 = Utf8               java/lang/Exception
  #27 = NameAndType        #11:#10        // catchBlock:I
  #28 = NameAndType        #13:#10        // methodExit:I
  #29 = Utf8               Foo
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/Throwable
{
  private int tryBlock;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  private int catchBlock;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  private int finallyBlock;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  private int methodExit;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  public Foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object.&quot;&lt;init&gt;&quot;:()V
         4: return
      LineNumberTable:
        line 1: 0

  public void test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: iconst_0
         2: putfield      #2                  // Field tryBlock:I
         5: aload_0
         6: iconst_2
         7: putfield      #3                  // Field finallyBlock:I
        10: goto          35
        13: astore_1
        14: aload_0
        15: iconst_1
        16: putfield      #5                  // Field catchBlock:I
        19: aload_0
        20: iconst_2
        21: putfield      #3                  // Field finallyBlock:I
        24: goto          35
        27: astore_2
        28: aload_0
        29: iconst_2
        30: putfield      #3                  // Field finallyBlock:I
        33: aload_2
        34: athrow
        35: aload_0
        36: iconst_3
        37: putfield      #6                  // Field methodExit:I
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang<!-- [[[read_end]]] -->/Exception
             0     5    27   any
            13    19    27   any
      LineNumberTable:
        line 9: 0
        line 13: 5
        line 14: 10
        line 10: 13
        line 11: 14
        line 13: 19
        line 14: 24
        line 13: 27
        line 14: 33
        line 15: 35
        line 16: 40
      StackMapTable: number_of_entries = 3
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 7 /* same */
}
SourceFile: &quot;Foo.java&quot;

```

这里面我用到了两个选项。第一个选项是-p。默认情况下javap会打印所有非私有的字段和方法，当加了-p选项后，它还将打印私有的字段和方法。第二个选项是-v。它尽可能地打印所有信息。如果你只需要查阅方法对应的字节码，那么可以用-c选项来替换-v。

javap的-v选项的输出分为几大块。

1.基本信息，涵盖了原class文件的相关信息。

class文件的版本号（minor version: 0，major version: 54），该类的访问权限（flags: (0x0021) ACC_PUBLIC, ACC_SUPER），该类（this_class: #7）以及父类（super_class: #8）的名字，所实现接口（interfaces: 0）、字段（fields: 4）、方法（methods: 2）以及属性（attributes: 1）的数目。

这里属性指的是class文件所携带的辅助信息，比如该class文件的源文件的名称。这类信息通常被用于Java虚拟机的验证和运行，以及Java程序的调试，一般无须深入了解。

```
Classfile ../Foo.class
  Last modified ..; size 541 bytes
  MD5 checksum 3828cdfbba56fea1da6c8d94fd13b20d
  Compiled from &quot;Foo.java&quot;
public class Foo
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // Foo
  super_class: #8                         // java/lang/Object
  interfaces: 0, fields: 4, methods: 2, attributes: 1

```

class文件的版本号指的是编译生成该class文件时所用的JRE版本。由较新的JRE版本中的javac编译而成的class文件，不能在旧版本的JRE上跑，否则，会出现如下异常信息。（Java 8对应的版本号为52，Java 10对应的版本号为54。）

```
Exception in thread &quot;main&quot; java.lang.UnsupportedClassVersionError: Foo has been compiled by a more recent version of the Java Runtime (class file version 54.0), this version of the Java Runtime only recognizes class file versions up to 52.0

```

类的访问权限通常为ACC_开头的常量。具体每个常量的意义可以查阅Java虚拟机规范4.1小节[1]。

2.常量池，用来存放各种常量以及符号引用。

常量池中的每一项都有一个对应的索引（如#1），并且可能引用其他的常量池项（#1 = Methodref #8.#23）。

```
Constant pool:
   #1 = Methodref          #8.#23         // java/lang/Object.&quot;&lt;init&gt;&quot;:()V
... 
   #8 = Class              #30            // java/lang/Object
...
  #14 = Utf8               &lt;init&gt;
  #15 = Utf8               ()V
...
  #23 = NameAndType        #14:#15        // &quot;&lt;init&gt;&quot;:()V
...
  #30 = Utf8               java/lang/Object

```

举例来说，上图中的1号常量池项是一个指向Object类构造器的符号引用。它是由另外两个常量池项所构成。如果将它看成一个树结构的话，那么它的叶节点会是字符串常量，如下图所示。

<img src="https://static001.geekbang.org/resource/image/f8/8c/f87469e321c52b21b0d2abb88e7b288c.png" alt="" />

3.字段区域，用来列举该类中的各个字段。

这里最主要的信息便是该字段的类型（descriptor: I）以及访问权限（flags: (0x0002) ACC_PRIVATE）。对于声明为final的静态字段而言，如果它是基本类型或者字符串类型，那么字段区域还将包括它的常量值。

```
  private int tryBlock;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE


```

另外，Java虚拟机同样使用了“描述符”（descriptor）来描述字段的类型。具体的对照如下表所示。其中比较特殊的，我已经高亮显示。

4.方法区域，用来列举该类中的各个方法。

除了方法描述符以及访问权限之外，每个方法还包括最为重要的代码区域（Code:)。

```
  public void test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
...
        10: goto          35
...
        34: athrow
        35: aload_0
...
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang/Exception
             0     5    27   any
            13    19    27   any
      LineNumberTable:
        line 9: 0
...
        line 16: 40
      StackMapTable: number_of_entries = 3
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
...

```

代码区域一开始会声明该方法中的操作数栈（stack=2）和局部变量数目（locals=3）的最大值，以及该方法接收参数的个数（args_size=1）。注意这里局部变量指的是字节码中的局部变量，而非Java程序中的局部变量。

接下来则是该方法的字节码。每条字节码均标注了对应的偏移量（bytecode index，BCI），这是用来定位字节码的。比如说偏移量为10的跳转字节码10: goto 35，将跳转至偏移量为35的字节码35: aload_0。

紧跟着的异常表（Exception table:）也会使用偏移量来定位每个异常处理器所监控的范围（由from到to的代码区域），以及异常处理器的起始位置（target）。除此之外，它还会声明所捕获的异常类型（type）。其中，any指代任意异常类型。

再接下来的行数表（LineNumberTable:）则是Java源程序到字节码偏移量的映射。如果你在编译时使用了-g参数（javac -g Foo.java），那么这里还将出现局部变量表（LocalVariableTable:），展示Java程序中每个局部变量的名字、类型以及作用域。

行数表和局部变量表均属于调试信息。Java虚拟机并不要求class文件必备这些信息。

```
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           14       5     1     e   Ljava/lang/Exception;
            0      41     0  this   LFoo;

```

最后则是字节码操作数栈的映射表（StackMapTable: number_of_entries = 3）。该表描述的是字节码跳转后操作数栈的分布情况，一般被Java虚拟机用于验证所加载的类，以及即时编译相关的一些操作，正常情况下，你无须深入了解。

## 2.OpenJDK项目Code Tools：实用小工具集

OpenJDK的Code Tools项目[2]包含了好几个实用的小工具。

在第一篇的实践环节中，我们使用了其中的字节码汇编器反汇编器ASMTools[3]，当前6.0版本的下载地址位于[4]。ASMTools的反汇编以及汇编操作所对应的命令分别为：

```
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class &gt; Foo.jasm

```

和

```
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm

```

该反汇编器的输出格式和javap的不尽相同。一般我只使用它来进行一些简单的字节码修改，以此生成无法直接由Java编译器生成的类，它在HotSpot虚拟机自身的测试中比较常见。

在第一篇的实践环节中，我们需要将整数2赋值到一个声明为boolean类型的局部变量中。我采取的做法是将编译生成的class文件反汇编至一个文本文件中，然后找到boolean flag = true对应的字节码序列，也就是下面的两个。

```
iconst_1;
istore_1;

```

将这里的iconst_1改为iconst_2[5]，保存后再汇编至class文件即可完成第一篇实践环节的需求。

除此之外，你还可以利用这一套工具来验证我之前文章中的一些结论。比如我说过class文件允许出现参数类型相同、而返回类型不同的方法，并且，在作为库文件时Java编译器将使用先定义的那一个，来决定具体的返回类型。

具体的验证方法便是在反汇编之后，利用文本编辑工具复制某一方法，并且更改该方法的描述符，保存后再汇编至class文件。

Code Tools项目还包含另一个实用的小工具JOL[6]，当前0.9版本的下载地址位于[7]。JOL可用于查阅Java虚拟机中对象的内存分布，具体可通过如下两条指令来实现。

```
$ java -jar /path/to/jol-cli-0.9-full.jar internals java.util.HashMap
$ java -jar /path/to/jol-cli-0.9-full.jar estimates java.util.HashMap

```

## 3.ASM：Java字节码框架

ASM[8]是一个字节码分析及修改框架。它被广泛应用于许多项目之中，例如Groovy、Kotlin的编译器，代码覆盖测试工具Cobertura、JaCoCo，以及各式各样通过字节码注入实现的程序行为监控工具。甚至是Java 8中Lambda表达式的适配器类，也是借助ASM来动态生成的。

ASM既可以生成新的class文件，也可以修改已有的class文件。前者相对比较简单一些。ASM甚至还提供了一个辅助类ASMifier，它将接收一个class文件并且输出一段生成该class文件原始字节数组的代码。如果你想快速上手ASM的话，那么你可以借助ASMifier生成的代码来探索各个API的用法。

下面我将借助ASMifier，来生成第一篇实践环节所用到的类。（你可以通过该地址[9]下载6.0-beta版。）

```
$ echo '
public class Foo {
 public static void main(String[] args) {
  boolean flag = true;
  if (flag) System.out.println(&quot;Hello, Java!&quot;);
  if (flag == true) System.out.println(&quot;Hello, JVM!&quot;);
 }
}' &gt; Foo.java
# 这里的javac我使用的是Java 8版本的。ASM 6.0可能暂不支持新版本的javac编译出来的class文件
$ javac Foo.java
$ java -cp /PATH/TO/asm-all-6.0_BETA.jar org.objectweb.asm.util.ASMifier Foo.class | tee FooDump.java
...
public class FooDump implements Opcodes {

public static byte[] dump () throws Exception {

ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;

cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, &quot;Foo&quot;, null, &quot;java/lang/Object&quot;, null);

...

{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, &quot;main&quot;, &quot;([Ljava/lang/String;)V&quot;, null, null);
mv.visitCode();
mv.visitInsn(ICONST_1);
mv.visitVarInsn(ISTORE, 1);
mv.visitVarInsn(ILOAD, 1);
...
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
...

```

可以看到，ASMifier生成的代码中包含一个名为FooDump的类，其中定义了一个名为dump的方法。该方法将返回一个byte数组，其值为生成类的原始字节。

在dump方法中，我们新建了功能类ClassWriter的一个实例，并通过它来访问不同的成员，例如方法、字段等等。

每当访问一种成员，我们便会得到另一个访问者。在上面这段代码中，当我们访问方法时（即visitMethod），便会得到一个MethodVisitor。在接下来的代码中，我们会用这个MethodVisitor来访问（这里等同于生成）具体的指令。

这便是ASM所使用的访问者模式。当然，这段代码仅包含ClassWriter这一个访问者，因此看不出具体有什么好处。

我们暂且不管这个访问者模式，先来看看如何实现第一篇课后实践的要求。首先，main方法中的boolean flag = true;语句对应的代码是：

```
mv.visitInsn(ICONST_1);
mv.visitVarInsn(ISTORE, 1);

```

也就是说，我们只需将这里的ICONST_1更改为ICONST_2，便可以满足要求。下面我用另一个类Wrapper，来调用修改过后的FooDump.dump方法。

```
$ echo 'import java.nio.file.*;

public class Wrapper {
  public static void main(String[] args) throws Exception {
    Files.write(Paths.get(&quot;Foo.class&quot;), FooDump.dump());
  }
}' &gt; Wrapper.java
$ javac -cp /PATH/TO/asm-all-6.0_BETA.jar FooDump.java Wrapper.java
$ java -cp /PATH/TO/asm-all-6.0_BETA.jar:. Wrapper
$ java Foo

```

这里的输出结果应和通过ASMTools修改的结果一致。

通过ASM来修改已有class文件则相对复杂一些。不过我们可以从下面这段简单的代码来开始学起：

```
  public static void main(String[] args) throws Exception {
    ClassReader cr = new ClassReader(&quot;Foo&quot;);
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    cr.accept(cw, ClassReader.SKIP_FRAMES);
    Files.write(Paths.get(&quot;Foo.class&quot;), cw.toByteArray());
  }

```

这段代码的功能便是读取一个class文件，将之转换为ASM的数据结构，然后再转换为原始字节数组。其中，我使用了两个功能类。除了已经介绍过的ClassWriter外，还有一个ClassReader。

ClassReader将读取“Foo”类的原始字节，并且翻译成对应的访问请求。也就是说，在上面ASMifier生成的代码中的各个访问操作，现在都交给ClassReader.accept这一方法来发出了。

那么，如何修改这个class文件的字节码呢？原理很简单，就是将ClassReader的访问请求发给另外一个访问者，再由这个访问者委派给ClassWriter。

这样一来，新增操作可以通过在某一需要转发的请求后面附带新的请求来实现；删除操作可以通过不转发请求来实现；修改操作可以通过忽略原请求，新建并发出另外的请求来实现。

<img src="https://static001.geekbang.org/resource/image/2a/ce/2a5d6813e32b8f88abae2b9f7b151fce.png" alt="" />

```
import java.nio.file.*;
import org.objectweb.asm.*;

public class ASMHelper implements Opcodes {

  static class MyMethodVisitor extends MethodVisitor {
    private MethodVisitor mv;
    public MyMethodVisitor(int api, MethodVisitor mv) {
      super(api, null);
      this.mv = mv;
    }

    @Override
    public void visitCode() {
      mv.visitCode();
      mv.visitFieldInsn(GETSTATIC, &quot;java/lang/System&quot;, &quot;out&quot;, &quot;Ljava/io/PrintStream;&quot;);
      mv.visitLdcInsn(&quot;Hello, World!&quot;);
      mv.visitMethodInsn(INVOKEVIRTUAL, &quot;java/io/PrintStream&quot;, &quot;println&quot;, &quot;(Ljava/lang/String;)V&quot;, false);
      mv.visitInsn(RETURN);
      mv.visitMaxs(2, 1);
      mv.visitEnd();
    }
  }

  static class MyClassVisitor extends ClassVisitor {

    public MyClassVisitor(int api, ClassVisitor cv) {
      super(api, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
        String[] exceptions) {
      MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
      if (&quot;main&quot;.equals(name)) {
        return new MyMethodVisitor(ASM6, visitor);
      }
      return visitor;
    }
  }

  public static void main(String[] args) throws Exception {
    ClassReader cr = new ClassReader(&quot;Foo&quot;);
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    ClassVisitor cv = new MyClassVisitor(ASM6, cw);
    cr.accept(cv, ClassReader.SKIP_FRAMES);
    Files.write(Paths.get(&quot;Foo.class&quot;), cw.toByteArray());
  }
}

```

这里我贴了一段代码，在ClassReader和ClassWriter中间插入了一个自定义的访问者MyClassVisitor。它将截获由ClassReader发出的对名字为“main”的方法的访问请求，并且替换为另一个自定义的MethodVisitor。

这个MethodVisitor会忽略由ClassReader发出的任何请求，仅在遇到visitCode请求时，生成一句“System.out.println(“Hello World!”);”。

由于篇幅的限制，我就不继续深入介绍下去了。如果你对ASM有浓厚的兴趣，可以参考这篇教程[10]。

你对这些常用工具还有哪些问题呢？可以给我留言，我们一起讨论。感谢你的收听，我们下期再见。

[1]<br />
[https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.1](https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.1)<br />
[2]<br />
[http://openjdk.java.net/projects/code-tools/](http://openjdk.java.net/projects/code-tools/)<br />
[3]<br />
[https://wiki.openjdk.java.net/display/CodeTools/asmtools](https://wiki.openjdk.java.net/display/CodeTools/asmtools)<br />
[4]<br />
[https://adopt-openjdk.ci.cloudbees.com/view/OpenJDK/job/asmtools/lastSuccessfulBuild/artifact/asmtools-6.0.tar.gz](https://adopt-openjdk.ci.cloudbees.com/view/OpenJDK/job/asmtools/lastSuccessfulBuild/artifact/asmtools-6.0.tar.gz)<br />
[5]<br />
[https://cs.au.dk/~mis/dOvs/jvmspec/ref--21.html](https://cs.au.dk/~mis/dOvs/jvmspec/ref--21.html)<br />
[6]<br />
[http://openjdk.java.net/projects/code-tools/jol/](http://openjdk.java.net/projects/code-tools/jol/)<br />
[7]<br />
[http://central.maven.org/maven2/org/openjdk/jol/jol-cli/0.9/jol-cli-0.9-full.jar](http://central.maven.org/maven2/org/openjdk/jol/jol-cli/0.9/jol-cli-0.9-full.jar)<br />
[8]<br />
[https://asm.ow2.io/](https://asm.ow2.io/)<br />
[9]<br />
[https://repository.ow2.org/nexus/content/repositories/releases/org/ow2/asm/asm-all/6.0_BETA/asm-all-6.0_BETA.jar](https://repository.ow2.org/nexus/content/repositories/releases/org/ow2/asm/asm-all/6.0_BETA/asm-all-6.0_BETA.jar)<br />
[10]<br />
[http://web.cs.ucla.edu/~msb/cs239-tutorial/](http://web.cs.ucla.edu/~msb/cs239-tutorial/)


