1.字节码
Java能发展到现在,其“一次编译,多处运行”的功能功不可没,这里最主要的功劳就是JVM和字节码了,在不同平台和操作系统上根据JVM规范的定制JVM可以运行相同字节码(.Class文件),并得到相同的结果。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,将java文件编译后生成.class文件交由Java虚拟机去执行,在android上,class文件被包装成.dex文件交由DVM执行。
通过学习Java字节码指令可以对代码的底层运行结构有所了解,能更深层次了解代码背后的实现原理,例如字符串的相加的实现原理就是通过StringBuilder
的append
进行相加。用过字节码的视角看它的执行步骤,对Java代码的也能有更深的了解,知其然,也要知其所以然。
通过学习字节码知识还可以实现字节码插桩功能,例如用ASM 、AspectJ
等工具对字节码层面的代码进行操作,实现一些Java代码不好操作的功能。
1. 字节码的格式
下面举个简单的例子,分析其字节码的结构
public class Main {
public static void main(String[] args) {
System.out.println("HelloWorld");
}
}
上图中纯数字字母就是字节码,右边的是具体代码执行的字节码指令。
上面看似一堆乱码,但是JVM对字节码是有规范的,下面一点一点分析其代码结构
1.1魔数(Magic Number)
魔数唯一的作用是确定这个文件是否为一个能被虚拟机接收的Class
文件。很多文件存储标准中都使用魔数来进行身份识别,譬如gif和jpeg文件头中都有魔数。魔数的定义可以随意,只要这个魔数还没有被广泛采用同时又不容易引起混淆即可。
这里字节码中的魔数为0xCafeBabe
(咖啡宝贝),这个魔数值在Java还被称作Oak
语言的时候就已经确定下来了,据原开发成员所说是为了寻找一些好玩的、容易记忆的东西,选择0xCafeBabe
是因为它象征着著名咖啡品牌Peet`s Coffee
中深受喜欢的Baristas
咖啡,咖啡同样也是Java的logo标志。
1.2版本号(Version Number)
紧接着魔数的四个字节(00 00 00 33)存储的是Class文件的版本号。前两个是次版本号(Minor Version),转化为十进制为0;后两个为主版本号(Major Version),转化为十进制为52,序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。高版本的JDK能向下兼容以前的版本的Class文件,但不能运行以后版本的Class文件,及时文件格式并未发生变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
1.3常量池(Constant Pool)
这部分内容前面做了一个简要的笔记,感兴趣的可以去看看。
紧接着版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据结构,也是占用Class文件控件最大的数据项目之一,同事也是在Class文件中第一个出现的表类型数据项目。
常量池的前两个字节(00 22)代表的是常量池容量计数器,与Java中语言习惯不一样的是,这个容量计数是从1开始的,这里的22转换成十进制后为34,去除一个下标计数即表示常量池中有33个常量,这一点从字节码中的Constant pool
也可以看到,最后一个是#33 = Utf8 (Ljava/lang/String;)V
容量计数器后存储的是常量池的数据。 常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值(例如字符串),符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符,当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或者运行时解析、翻译到内存地址中。如下图。
常量池的每一项常量都是一个表,在JDK71.7之前共有11中结构不同的表结构数据,在JDK1.7之后为了更好底支持动态语言调用,又额外增加了三种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info
),总计14中,表结构如下图
上图中tag
是标志位,用于区分常量类型,length
表示这个UTF-8编码的字符串长度是多少节,它后面紧更着的长度为length
字节的连续数据是一个使用UTF-8缩略编码表示的字符串。上图的u1,u2,u4,u8表示比特数量,分别为1,2,4,8个byte。
UTF-8缩略编码与普通UTF-8编码的区别是:从\u0001
到\u007f
之间的字符(相当于1-127的ASCII码)的缩略编码使用一个字节表示,从\u0080
到\u07ff
之间的所有字符的缩略编码用两个字节表示,从\u0800
到\uffff
之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示,这么做的主要目的还是为了节省空间。
由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info
型常量来描述名称,所以CONSTANT_Utf8_info
型常量的最大长度就是Java中的方法、字段名的最大长度。这里的最大长度就是length的最大值,即u2
类型能表达的最大值65535,所以Java程序中如果定义了超过64K英文字符的变量或发放名,将会无法编译。
回到上面那个例子,00 22后面跟着的是 0A 0006 0014,第一个字节0A转化为十进制为10,表示的常量类型为CONSTANT_Methodref_info
,这从常量表中可以看到这个类型后面会两个u2
来表示index
,分别表示CONSTANT_Class_info
和CONSTANT_NameAndType_info
。所以0006和0014转化为10进制分别是6和20。这里可能不知道这些数字指代什么意思,下面展示的是编译后的字节码指令就可以清楚了。
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // HelloWorld
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/verzqli/snake/Main
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/verzqli/snake/Main;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 HelloWorld
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/verzqli/snake/Main
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
从上面可以看到Constant pool
中一共有33个常量,第一个常量类型为Methodref
,他其实指代的是这个Main
类,它是最基础的Object类,然后这里它有两个索引分别指向6和20,分别是Class和NameAndType类型,和上面十六进制字节码描述的一样。
1.4访问标志(Access Flags)
在常量池结束后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型,如果是类的话,是否被声明为final等,具体的标志位以及标志的含义见下表。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标识是否为public类型 |
ACC_FINAL | 0x0010 | 标识是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 用于兼容早期编译器,新编译器都设置改标志,以在使用invokespecial指令时对子类方法做特殊处理 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生,而是由编译器产生 |
ACC_INTERFACE | 0x0200 | 标识是否为一个接口,接口默认同事设置ACC_ABSTRACT |
ACC_ABSTRACT | 0x0400 | 标识是否为一个抽象类,不可与ACC_FINAL同时设置 |
ACC_ANNOTATION | 0x2000 | 标识这是否是一个注解类 |
ACC_ENUM | 0x4000 | 标识这是否是一个枚举 |
ACCESS_FLAGS中一共有16个标志位可用,当前只定义了其中8个(上面显示了比8个多,是因为ACC_PRIVATE,ACC_PROTECTED,ACC_STATIC,ACC_VOLATILE,ACC_TRANSTENT并不是修饰类的,这里写出来是让大家知道还有这么些标志符),对于没有使用到的标志位要求一律为0。Java不会穷举上面所有标志的组合,而是同|
运算来组合表示,至于这些标志位是如何表示各种状态,可以看这篇文章,讲的很清楚。
我们继续回到例子
例子中只是一个简单的Main类,所以他的标志是ACC_PUBLIC和ACC_SUPER,其他标志都不存在,所以它的访问标志为0x0001|0x0020=0x0021。
1.5 类索引、父类索引、接口索引
类索引和父类索引都是一个u2
类型的数据,接口索引是一组u2
类型的数据的集合,Class文件中由着三项数据来确定这个类的继承关系。这三者按顺序排列在访问标志之后,本文例子中他们分别是:0005,0006,0000,也就是类索引为5,父类索引为6,接口索引集合大小为0 ,查询上面字节码指令的常量池可以一一对应(5对应com/verzqli/snake/Main
,6对应java/lang/Object
)。
类索引确定这个类的全限定名,父类索引确定这个类的父类全限定 名,因为Java不允许多重继承,所以父类索引只有一个,除了Object
外,所有的类都有其父类,也就是其父类索引不为0.接口索引即可用来描述这个类实现了哪些接口,这些被实现的接口按implements
(如果这个类本身就是一个接口,则应当是extends
语句)后的接口顺序从左到右排列在接口索引集合中。
1.6 字段表集合(Field Info)
字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量。但是不包含方法内部声明的局部变量。在Java中描述一个字段可能包含一下信息:
- 字段的作用域(public,private,protected修饰符)
- 是实例变量还是类变量(static修饰符)
- 是否可变(final修饰符)
- 并发可见 (vlolatile修饰符,是否强制从主内存中读写)
- 是否可悲序列化(transient修饰符)
- 字段数据基本类型(基本类型、对象、数组)
- 字段名称
上述信息中,每个修饰符都是bool值,要么有要么没有,很适合用和访问标志一样的标志位来表示。而字段名称,字段数据类型只能引用常量池中