javap: 了解编译器的内部工作

javap : Java class文件分解器,其可以将不可读字节码转换为对应的可读指令。通过对照源码和指令,可以让我们了解编译器的内部工作,对于理解程序执行过程和执行效率等都有很大的帮助。本文就来介绍一下 javap 的简单使用。

认识 javap

众所周知, *.java 文件会被 java编译器转变为 *.class 字节码文件,再交由JVM转变为对应平台的机器指令,这就是 Java程序多平台兼容的关键。

1
*.java -> *.class -> 机器指令

需要知道的是,编译器并不是简单的去除所不必要的东西 (如注释,换行等),再转变为 16进制文件,而是做了编译器优化,这些行为是无法直接知道的。虽然字节码也是可以被解读的,但这对人来说并不友好,而使用 JDK 提供的 javap 就可以将不可读字节码转换为对应的可读指令。

JDK 目录结构

从 JDK 目录结构可以看出 : JDK = JRE + Tools&Tool APIs

javap 正是 JDK 提供的开发工具之一。

1
2
3
4
5
6
7
8
9
10
.
├── bin # JDK开发工具的可执行文件
│   ├── java # Java解释器 : *.class -> 机器指令
│   ├── javac # Java编译器 : *.java -> *.class
│   ├── javadoc # API 文档生成器
│   ├── javap # Java class文件分解器
│   └── ...
├── include # 包含C语言头文件,支持Java本地接口与Java虚拟机调试程序接口的本地编程技术
├── jre # 包含: JVM + 运行时的类包 + Java应用启动器
└── lib # 开发工具使用的归档包文件

使用 javap

这里只列举常用 javap 的常用指令。正如所见 javap -cjavap -v 可以满足绝大部分需求。

javap [options] class 说明
-help / —help / -? 帮助。
-v / -verbose 输出附加信息,包括绝大部分详细信息。
-l 输出行号和本地变量表。
-package 显示程序包/受保护的/公共类。
-p / -private 显示所有类和成员。
-c 对代码进行反汇编。
-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)。
-constants 显示最终常量。

javap -c

这里演示一下 javap -c 的用法和功能。

*.java

1
2
3
4
5
6
7
8
public class JavapTest {

public static void main(String[] args) {
String str1 = "a" + "b" + "c";
String str2 = str1 + "dfg";
}

}

javap 是作用在 *.class 上的,先使用 javac 获得 *.java 对应的 *.class

如果希望直接在 shell 中使用 JDK工具,需要配置环境变量。

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
# 在java文件目录执行,会在当前目录获得.class文件
$ javac JavapTest.java
# 获取对应.class文件的反编译指令
$ javap -c JavapTest

Compiled from "JavapTest.java"
public class JavapTest {
public JavapTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: ldc #2 // String abc
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String dfg
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}

1) 编译器对字符常量拼接的优化

由以下可以看出,当对字符常量的拼接时,编译器直接创建了拼接结果。

1
0: ldc           #2                  // String abc

2) 编译器对字符引用对象拼接的优化

由以下可以看出,当对字符引用对象的拼接时,编译器会自动创建 StringBuilder 进行拼接。

1
2
3
4
5
6
7
8
3: new           #3                  // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String dfg
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:

IDEA 整合 javap

使用命令编译再反编译,在日常使用时还是很麻烦的,所有这里介绍一下使用 IDEA外部工具(External Tools) 添加 javap ,这样使用起来会十分方便 。

推荐使用 javap -v ,因未知原因 javap -c 并不能正常输出完整信息🤣。

添加方法

进入 preferences -> Tools -> External Tools 添加

  • 设置程序位置Program : $JDKPath$/bin/javap
  • 设置参数Arguments : -v $FileClass$
  • 设置工作空间为编译输出位置Working directory : $OutputPath$

IDEA-javap

使用方法

在对应文件右击 External Tools 即可使用所配置的外部工具。