首先我们先来编写一个类,用于本文编译后文件的学习,这段代码所对应的完整的编译后的文本详见附录
public class Female {
protected Object obj;
public String func() {
return obj.toString();
}
}
public class Sxm extends Female{
public static long timer = 0;
private int num;
public int getNum() {
return this.num;
}
public void setNum(int num) {
this.num = num;
}
public void addNum(int n) {
if (n < 0) {
return;
}
if (n == 0) {
throw new RuntimeException();
} else {
this.num += n;
}
}
public Object getSuperObj() {
return super.obj;
}
class Inner {
public String str = "This is inner class";
}
}
源码和编译后文件的区别
首先源码和编译后的文件是有区别的
- 编译后的文件中只包含了一个类,所以源码中的内部类会被单独编译为一个文件
- 只有源码中会有注释,编译后的文件中不含有注释信息
- 编译后的文件中也没有import和package信息,而是以内部名称(internal name)代替
什么是内部名称
实际上内部名称也是用于标识类的位置,和源码中的全限定类名相似,如Object
的全限定类名是java.lang.Object
,而在编译后变成了java/lang/Object
类型描述符和方法描述符
在我们了解了internal name之后,编译后文件的变量类型仍然和源码有很大差别。例如在源码中public static long timer = 0;
,在编译后文件中则变成了public static J timer
,这里可能就会有疑问:J是什么类型?
对于编译后文件来说,类对象基本参照internal name都可以理解,但是基本类型则完全不同
java type | type descriptor |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | L |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
编译后文件中类对象表示为L + internal name,数组表示为[ + 数组中元素的type descriptor。这里注意类对象最后的;是必须的
当我们了解编译后文件如何表示变量类型,现在我们可以来看一看编译后文件是如何表示方法的。以public void setNum(int)
为例,在编译后文件中public setNum(I)V
。方法返回空void在编译后文件中用V表示,如果入参有多个则同事罗列,下面给出几个示例
源码 | 方法描述符 |
---|---|
void (int, float) | (IF)V |
int (Object) | (Ljava/lang/Object;)I |
int[] (int i,String) | (ILjava/lang/String;)[i |
如何阅读代码
接下来我们将继续深入,尝试阅读编译后文件中的代码。
局部变量和操作数栈
在尝试阅读字节码之前,首先来学习一下java代码的运行方式。java代码运行在线程(thread)中,每一个线程都有自己的执行栈(stack),也就是所谓的函数堆栈,里面记录线程的调用信息,每当调用一个方法都会新加一层,一个方法退出则出栈一层。每一个调用的方法都被封装为帧(frame)格式,如前文所说,栈中每一帧都表示调用的方法,其中要特别说明的是:即使方法调用抛出异常,也会出栈,直到异常被捕获(catch)。
帧中存在两部分:局部变量表(lacal variables)和操作数栈(operand stack)。局部变量可以通过下标的方式获取变量,也就是说其底层实现类似于数组,在栈帧初始化的时候,局部变量表中下标0存储this对象,然后是方法的入参。比如a.equals(b)
,局部变量表初始化时会有两个变量a和b,分别对应this对象和方法入参。
操作数栈中存储的是字节码指令使用的操作数,和所有的栈结构一样,遵循先入后出的顺序。所有指令都会取栈顶数据执行动作,如运算指令IADD,ISUB等,会pop两次相加再push;还有IFGE这样的条件指令,pop后进行判断是否满足条件。不过于局部变量不同,操作数栈在初始时是空的。
此外还有一点要注意,操作数栈和局部变量存储所有类型的变量都是一格,除了long和double这种64的数据,需要两格空间存储。
字节码指令
字节码指令包含opcode和参数。opcode实际上是一个unsigned byte,为了便于阅读,所以有一个英文的助记词,例如GOTO指令,实际上在字节码文件中是167,但是为了我们方便阅读,所以有一个英文名字叫GOTO。
这些指令可以分为两类,一组是用于局部变量和操作数栈做交互用的。ILOAD,LLOAD,FLOAD,DLOAD, 和ALOAD指令读取局部变量,然后push到操作数栈。传入一个参数i表示要读取的下标。其中ILOAD用于读取boolean byte char short int类型的数据,而LLOAD FLOAD DLOAD分别用于读取long float double类型数据,最后ALOAD用于读取所有非基本类型的数据,如object和数组。与之相对的ISTORE,LSTORE,FSTORE,DSTORE和ASTORE指令则是将操作数栈pop后存入局部变量。
其他的指令又可以分成以下几类:
- 操作操作数栈 这类指令用于操作操作数栈。例如POP弹出栈顶元素;DUP复制栈顶元素再push回去;SWAP弹出两次交换后重新压入
- 常量 这一类指令把常量压入操作数栈。如ACONST_NULL向栈顶压入null;ICONST_0压入int类型值0;FCONST_0压入float类型值0f;LDC压入任意类型的常量
- 运算与逻辑 这类指令弹出两个栈元素做运算,再将运算结果重新压入栈。ADD,SUB,MUL,DIV和REM分别对应+,-,*,/和%。逻辑运算opcode有**<<,>>,>>>,|,&and^**
- 类型转换 将栈顶元素弹出,进行类型转换后重新压入,这类指令对应源码中类型强制转换操作。如I2F,F2D,L2D等数值类型转换;CHECKCAST将一个引用值转换为其他类型。
- 操作对象 创建新对象,对象加锁,检验对象类型等。如NEW会新建一个对象压入操作数栈。
- 成员变量
读取和写入成员变量,最常见于get和set方法。如
GETFIELD owner name desc
,先从栈中pop一个类型为owner的引用值,从中读取类型为desc的成员变量name;PUTFIELDowner name desc
则是取一个值和一个引用,写入成员变量;此外还有GETSTATIC和PUTSTATIC用于读取静态变量。- 调用方法 这类指令负责调用方法和构造器,先弹出和参数数量一样多的元素,外加一个调用者类型的引用值,如果方法有返回值则将返回值压入栈。如INVOKEVIRTUAL INVOKESTATIC INVOKESPECIAL INVOKEINTERFACE 和INVOKEDYNAMIC
- 数组 用于数组的读写。xALOAD弹出一个下标和数组地址,然后取出数组中的这个下标放入栈内;类似的还有xASTORE负责存储数据
- 跳转 这种指令就是源码中的判断或者循环代码块,还有break和continue关键字。如IFEQ会判断当前栈顶元素是否为0,如果是就会跳转到指向的label;TABLESWITCH和LOOKUPSWITCH对应源码中的switch代码块
- 返回 这里主要是xRETURN和RETURN指令。其中RETURN指令返回void,xRETURN则返回其指定的数据类型。
异常
在源码中还有一种代码结构不能被上述提及到的指令表示,那就是try-catch-finally异常捕获处理。如下面这段源码
public static void sleep(long d) {
try {
Thread.sleep(d);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
那么编译后对应
TRYCATCHBLOCKtry catch catchjava/lang/InterruptedException
try:
LLOAD 0
INVOKESTATIC java/lang/Thread sleep (J)V
RETURN
catch:
INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
RETURN
参考文献:
[1]: ASM开发手册 [2]: JAVA 异常堆栈中的行号(lineNumber)是怎么来的? [3]: 求 Java字节码中 stack map frames 简单说明?
附录 字节码全文
// class version 52.0 (52)
// access flags 0x21
public class org/miku/bean/Sxm extends org/miku/bean/Female {
// compiled from: Sxm.java
// access flags 0x0
INNERCLASS org/miku/bean/Sxm$Inner org/miku/bean/Sxm Inner
// access flags 0x9
public static J timer
// access flags 0x2
private I num
// access flags 0x1
public <init>()V
L0
LINENUMBER 8 L0
ALOAD 0
INVOKESPECIAL org/miku/bean/Female.<init> ()V
RETURN
L1
LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public getNum()I
L0
LINENUMBER 14 L0
ALOAD 0
GETFIELD org/miku/bean/Sxm.num : I
IRETURN
L1
LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public setNum(I)V
// parameter num
L0
LINENUMBER 18 L0
ALOAD 0
ILOAD 1
PUTFIELD org/miku/bean/Sxm.num : I
L1
LINENUMBER 19 L1
RETURN
L2
LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L2 0
LOCALVARIABLE num I L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
public addNum(I)V
// parameter n
L0
LINENUMBER 22 L0
ILOAD 1
IFGE L1
L2
LINENUMBER 23 L2
RETURN
L1
LINENUMBER 25 L1
FRAME SAME
ILOAD 1
IFNE L3
L4
LINENUMBER 26 L4
NEW java/lang/RuntimeException
DUP
INVOKESPECIAL java/lang/RuntimeException.<init> ()V
ATHROW
L3
LINENUMBER 28 L3
FRAME SAME
ALOAD 0
DUP
GETFIELD org/miku/bean/Sxm.num : I
ILOAD 1
IADD
PUTFIELD org/miku/bean/Sxm.num : I
L5
LINENUMBER 30 L5
RETURN
L6
LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L6 0
LOCALVARIABLE n I L0 L6 1
MAXSTACK = 3
MAXLOCALS = 2
// access flags 0x1
public getSuperObj()Ljava/lang/Object;
L0
LINENUMBER 33 L0
ALOAD 0
GETFIELD org/miku/bean/Female.obj : Ljava/lang/Object;
ARETURN
L1
LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 9 L0
LCONST_0
PUTSTATIC org/miku/bean/Sxm.timer : J
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}