java语言号称“一处编译,处处运行”,它能“打出”这样的广告,主要是因为两点:1、它运行在虚拟机环境里,不管哪种操作系统,只要安装了jdk的运行环境就行;2、我们今天的主角-字节码文件,jvm提供了字节码规范,它可以解析字节码文件,因此只要符合字节码的语法,那么在jvm中都是能运行,比如现在的scala,Groovy,Kotlin等都是实现了jvm字节码规范的语言。
首先按照惯例,我们简单写一个java代码,然后查看对应的.class文件
public class MyTest1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
通过使用javap -v MyTest.class,生成了关于常量池,方法和字段描述符,类信息等
Classfile /Users/xxx/Documents/workspace/Jvm/target/classes/jp/zhanng/bytecode/MyTest1.class
Last modified 2019-12-21; size 481 bytes
MD5 checksum fc01e439a20da18c0c4ece664cc129ab
Compiled from "MyTest1.java"
public class jp.zhang.bytecode.MyTest1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // jp/zhang/bytecode/MyTest1.a:I
#3 = Class #22 // jp/zhang/bytecode/MyTest1
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ljp/zhang/bytecode/MyTest1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest1.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 jp/zhang/bytecode/MyTest1
#23 = Utf8 java/lang/Object
{
public jp.zhang.bytecode.MyTest1();
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 a:I
9: return
LineNumberTable:
line 7: 0
line 9: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Ljp/zhang/bytecode/MyTest1;
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljp/zhang/bytecode/MyTest1;
public void setA(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 16: 0
line 17: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Ljp/zhang/bytecode/MyTest1;
0 6 1 a I
}
SourceFile: "MyTest1.java"
到目前为止,我们应该还是不知道上面的信息是个啥的,下面我们介绍一下jvm的字节码规范,后续我们使用16进制查看工具,具体解析出MyTest1.class文件中的信息,看是否能与上述IDEA反编译的结果一致。
- 魔术(u4):固定值:CA FE BA BE
- 版本号(u4):前2个是次版本号,后2个是主版本号,jvm会验证当前jdk版本号与编译字节码文件的jdk版本号,如果大于编译使用的额,那么会验证通过,如果小于编译使用的jdk,则会报错。(低版本不能运行高版本文件,但是高版本可以运行低版本)
- 常量池(u2+n字节):前2位代表常量池的个数,后续的n则是具体的常量池表/数组,具体规范部分可以查看笔者的字节码的组成及各部分规范
- 类的访问控制权限(u2),值是由多个权限并集而成。
- 类名(u2),类的全路径信息
- 父类名(u2),对应父类的路径信息
- 接口(u2+n):u2前2位代表所实现接口的个数,后面n代表对应的接口名
- 字段fields(u2+n):前2位代表字段的个数,后面n代表字段的表信息,同样参看规范链接部分
- 方法(u2+n):前2位代表方法的个个数,后面n代表方法的描述信息
- 附加属性信息(u2+n):附加属性的个数和对应的属性表
以上就完整的介绍了字节码的整体组成部分,现在我们再把一些细节进行介绍,后面我们就可以正式进入解读字节码文件了
- 常量池:常量池描述了类的很多信息,包含字段,方法,类等描述信息,相当于class文件的资源仓库,常量池主要分2种,分别为字面量和符号引用;字面量就是String字符串、final定义的常量,这种类型因为是不可修改的,所以是字面量,还有种是符号引用,包括了类和接口的信息,字段和字段的描述信息,方法和方法的描述信息。
- 常量池由常量池个数和常量池表组成,常量的池的个数计算需要-1,因为常量池的个数计算是从索引为1开始的,常量0已结被占用,代表null;在常量表部分,根据规范,其中最开始一定有一个tag标记为哪种常量,对应的长度也是固定的u1;具体查看链接中的规范。
- 每个字段都有对应的描述信息,用于描述字段的数据类型、方法的参数列表(包含参数的个数,顺序)和返回值,对于基本数据类型,它们的表示为:S--->short,B--->byte,Z-->boolean,I--->int,F-->float,D--->double,C-->char,J-->long,V--->void,L-->d对象,也就是我们常见的:Ljava/lang/Object;(这就是Object类在字节码中的表现形式,对象的表现形式后面有分号)
- 数组的表示,每一个维度都由“[”表示,以此类推,int[][]的代表形式就是:[[I;String[][]的表示形式就是:[[Ljava/lang/string;注意后面的分号也是代表的一部分
- 方法的描述,按照方法中参数列表出现的顺序、返回值这样的(表现形式为:小括号(参数列表)返回值 )先后顺序进行表示,此时并没有方法的名称(对应的名称描述是在后续的方法规范中进行指向)
eg:public String getNameByIdAndType(int id,String type),则对应的表现形式为:(I,Ljava/lang/string;)Ljava/lag/string;
有了以上的全局理解后,我们再通过16进制工具打开刚刚生成的MyTest1.class文件
下一篇我们就来一个字节一个字节的读下去,我们也可以字节成为“反编译器”了,哈哈