简介
javap是 Java class文件分解器,可以反编译,也可以查看java编译器生成的字节码。用于分解class文件。
本文以64位OpenJDK 14.0.2版本来介绍javap工具。
- 首先看下javap的使用方法
用法: javap <options> <classes> 其中, 可能的选项包括: -? -h --help -help 输出此帮助消息 -version 版本信息 -v -verbose 输出附加信息 -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的 系统信息(路径、大小、日期、SHA-256 散列) -constants 显示最终常量 --module <模块>, -m <模块> 指定包含要反汇编的类的模块 --module-path <路径> 指定查找应用程序模块的位置 --system <jdk> 指定查找系统模块的位置 --class-path <路径> 指定查找用户类文件的位置 -classpath <路径> 指定查找用户类文件的位置 -cp <路径> 指定查找用户类文件的位置 -bootclasspath <路径> 覆盖引导类文件的位置 --multi-release <version> 指定要在多发行版 JAR 文件中使用的版本
常用的命令为javap -p -v -l
Bean对象反编译
- 先看一下最简单的bean,这里添加了一些注释信息
/**
* 注释信息
*/
public class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
用javap -p -v -l Student 进行反编译,得到下面的信息
Classfile /Users/shellingford/data/devtool/workspace/test/target/classes/Student.class
Last modified 2022年4月9日; size 496 bytes
SHA-256 checksum a17b7f5fcf2f45162bd029d889684a9d41da8f0c59ac057ba4f748df1627abd3
Compiled from "Student.java"
public class Student
minor version: 0
major version: 58
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // Student
super_class: #2 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // Student.name:Ljava/lang/String;
#8 = Class #10 // Student
#9 = NameAndType #11:#12 // name:Ljava/lang/String;
#10 = Utf8 Student
#11 = Utf8 name
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 LStudent;
#18 = Utf8 getName
#19 = Utf8 ()Ljava/lang/String;
#20 = Utf8 setName
#21 = Utf8 (Ljava/lang/String;)V
#22 = Utf8 SourceFile
#23 = Utf8 Student.java
{
private java.lang.String name;
descriptor: Ljava/lang/String;
flags: (0x0002) ACC_PRIVATE
public Student();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStudent;
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #7 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStudent;
public void setName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #7 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 12: 0
line 13: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LStudent;
0 6 1 name Ljava/lang/String;
}
SourceFile: "Student.java"
第一部分是class的文件信息,包含了最后修改时间,文件大小,SHA-256摘要等等。
第二部分是类信息,包含了类名、编译版本、访问权限、父类信息、接口数量、字段数量、属性数量。
其中major表示的是java大版本号,例如58表示jdk14, minor主要是一些小的版本更新。在class运行时jvm会校验版本号,如果jvm版本号低于编译的class版本号就会报错
Unsupported major.minor version 58.0
第三部分是常量池
第四部分是类的内容,包含了属性和方法
其中可以看到LineNumberTable,这是方法内的一个偏移量和代码行号转换信息。虽然注释信息不会被写入class,但占用的行会影响代码行号,classes中的行号信息是包含注释在内的行号。
字段和属性
在反编译的第二部分,字段(fields)数量是1,属性(attributes)数量是1 。那字段和属性有什么区别的呢?
我们修改一下Student代码
public class Student {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
除了name外,我们额外增加一个age,这个时候字段(fields)数量变成了2 。所以字段(fields)指的就是类的属性,这也和Field类对应。
我们再给Student增加一个接口
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = -310391221195116443L;
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
接口数量也变成了1
再看一下字段是怎么展示的
这里的常量可以在常量池中找到,但需要注意的是,只有final修饰的字段才会在字段上直接初始化,其余的都是在构造函数中初始化的。可以看下面的修改
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = -310391221195116443L;
private String name = "张三";
private Integer age = 18;
private int no = 1;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
方法
先看一下代码
public class Student {
public void study(){
int score = 1;
try {
score = 2;
}catch(Exception e){
score = 3;
}finally{
score = 4;
}
}
}
反编译是这样的
public void study();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_1
4: iconst_4
5: istore_1
6: goto 22
9: astore_2
10: iconst_3
11: istore_1
12: iconst_4
13: istore_1
14: goto 22
17: astore_3
18: iconst_4
19: istore_1
20: aload_3
21: athrow
22: return
Exception table:
from to target type
2 4 9 Class java/lang/Exception
2 4 17 any
9 12 17 any
LineNumberTable:
line 4: 0
line 6: 2
line 10: 4
line 11: 6
line 7: 9
line 8: 10
line 10: 12
line 11: 14
line 10: 17
line 11: 20
line 12: 22
LocalVariableTable:
Start Length Slot Name Signature
10 2 2 e Ljava/lang/Exception;
0 23 0 this LStudent;
2 21 1 score I
StackMapTable: number_of_entries = 3
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class Student, int ]
stack = [ class java/lang/Exception ]
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 4 /* same */
再看一下异常部分
是由异常表来处理的,其中第一个Exception是代码显示捕获的Exception,剩下2个都是any。
第一个any是由2到4,和Exception范围一样,是用来捕获非Exception的,即便没有显示捕获错误,jvm也会捕获,跳转到17偏移量,最终在21偏移量被继续抛出这个异常。
第二个any也是类似的,只是它捕获的是Catch代码块中抛出的异常,最终也在21偏移量被抛出。
除了异常捕获部分,还能看到有个本地变量表
这里展示了有3个本地变量,分别是代表实例的this,捕获的异常Exception和我们申明的变量score。
接着是StackMapTable部分,这个是为了在验证操作数栈和本地变量类型与操作命令是否相符而存在的,主要是为了提升跳转或者重置栈之后校验的效率。
调用
public class Student {
public void study(){
int score = 1;
try {
score = 2;
}catch(Exception e){
score = 3;
}finally{
score = 4;
}
}
public void display(){
study();
}
}
反编译之后,调用使用的invokevirtual命令
当然在jvm中调用方法的指令不止invokevirtual(用于非私有实例方法),还有invokestatic(用于静态方法调用),invokespecial(用于私有实例方法、构造函数、super关键字调用父类实例方法或者构造器、接口默认方法),invokeinterface(用于接口方法),invokedynamic(用于调用动态方法)