再谈无关性
Java语言诞生时所喊的口号是“一次编写,到处运行”(Write Onece,Run Anywhere),足以看见当时的开发者们对冲破平台限制的渴望。Java虚拟机屏蔽了各个平台的差异,使得这个口号成为现实。从Java诞生至今已经有20多年,相比当初它已经强大了很多,以至于我们不得不重新审视它的平台无关性。如今Java虚拟机已经不是专门用于执行Java语言了,一切符合Java虚拟机规范的语言都可以被编译成.class文件被Java虚拟机加载执行,比如Groovy、Kotlin、Scala、JPython等。如今使用这些语言的人相对还不多,谁能保证日后Java虚拟机在语言无关性上的优势不会赶上甚至超越它在平台无关性上的优势呢?
不论是平台无关性还是语言无关性,它们的基础仍然是虚拟机和字节码存储格式。Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。
Class文件的组成
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
**从内容的角度来看, Class文件主要包含的内容是无符号数和表。**后面的解析都要以这两种数据类型为基础,所以先解释清楚这两个概念。
无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表:由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。
为了更直观的解析class文件,使用NotePad++ (安装HexEditor插件)后打开.class文件如下图:
魔数与Class文件的版本
每个class文件的头4个字节被称为魔数。它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。从上图我们得知class文件的头4个字节是“ca fe ba be”(咖啡宝贝?!)。这个魔数似乎也预示着日后“Java”这个商标名称的出现……
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。从上图得知,我们的主版本号是16进制下的34换算成10进制等于52. Java的主版本号是从45开始(45=JDK1.1),因此可以得知我们的主版本号是JDK1.8。次版本号为0,因此当前对应的Java版本号是JDK1.8.0。我们验证一下:
注意,_201是1.8.0版本下的补丁版本号。
常量池
紧接着主、次版本号之后的是常量池入口。常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,从上图得知常量池的容量为0x0016=十进制下的22,得出当前常量池中有21项常量。
在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。
常量池中主要存放的数据是:字面量和符号引用。
1.字面量:比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
2.符号引用:属于编译原理方面的概念,主要包含这些常量:被模块导出或者开放的包(package)、类或接口的全限定名、字段的名称和描述符、方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量(JDK1.8的lambda表达式还记得嘛?)
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接(具体见第7章)。也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在介绍虚拟机类加载过程时再详细讲解。
常量池中每一项常量都是一个子表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量,为了支持Java模块化系统(Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。
这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位,代表着当前常量属于哪种常量类型。17种常量类型所代表的具体含义如下表所示:
我们使用javap命令反编译一下看一下class文件的常量池到底长啥样:
源码:
package com.leon.util.json;
import javax.xml.ws.RequestWrapper;
/**
* @author created by leon on 2020-04-10
* @since v1.0
*/
public class XmlUtil {
private String strVal;
private int intVal;
private int[] intArr;
private static final String CONSTANT_STR = "leon";
private static int STATIC_INT_VAL = 1;
@RequestWrapper
public void method1() {
int count1 = 10;
double dcount2 = 5.0D;
}
public void method2(String str) {
}
public String method3() {
return new String("leon");
}
public String method4(String str) {
return null;
}
public void method5(int count) {
if (count < 1) {
System.out.println(count);
} else {
count -- ;
method5(count);
}
}
public void method6() {
this.method5(5);
}
public static void main(String[] args) {
System.out.println("main method.");
}
class InnerClass {
private int innerCount1;
private String innerCount2;
}
}
先用javac XmlUtil.java编译一下,再通过javap -verbose XmlUtil得到反编译结果如下:
Classfile /D:/S00L00M07/mall/common-util/src/main/java/com/leon/util/json/XmlUtil.class
Last modified 2020-4-26; size 1350 bytes
MD5 checksum dedbeb866f206d860b627d15cb85ce52
Compiled from "XmlUtil.java"
public class com.leon.util.json.XmlUtil
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#49 // java/lang/Object."<init>":()V
#2 = Double 5.0d
#4 = Class #50 // java/lang/String
#5 = String #51 // leon
#6 = Methodref #4.#52 // java/lang/String."<init>":(Ljava/lang/String;)V
#7 = Fieldref #53.#54 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Methodref #55.#56 // java/io/PrintStream.println:(I)V
#9 = Methodref #13.#57 // com/leon/util/json/XmlUtil.method5:(I)V
#10 = String #58 // main method.
#11 = Methodref #55.#59 // java/io/PrintStream.println:(Ljava/lang/String;)V
#12 = Fieldref #13.#60 // com/leon/util/json/XmlUtil.STATIC_INT_VAL:I
#13 = Class #61 // com/leon/util/json/XmlUtil
#14 = Class #62 // java/lang/Object
#15 = Class #63 // com/leon/util/json/XmlUtil$InnerClass
#16 = Utf8 InnerClass
#17 = Utf8 InnerClasses
#18 = Utf8 strVal
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 intVal
#21 = Utf8 I
#22 = Utf8 intArr
#23 = Utf8 [I
#24 = Utf8 CONSTANT_STR
#25 = Utf8 ConstantValue
#26 = Utf8 STATIC_INT_VAL
#27 = Utf8 <init>
#28 = Utf8 ()V
#29 = Utf8 Code
#30 = Utf8 LineNumberTable
#31 = Utf8 method1
#32 = Utf8 RuntimeVisibleAnnotations
#33 = Utf8 Ljavax/xml/ws/RequestWrapper;
#34 = Utf8 method2
#35 = Utf8 (Ljava/lang/String;)V
#36 = Utf8 method3
#37 = Utf8 ()Ljava/lang/String;
#38 = Utf8 method4
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#40 = Utf8 method5
#41 = Utf8 (I)V
#42 = Utf8 StackMapTable
#43 = Utf8 method6
#44 = Utf8 main
#45 = Utf8 ([Ljava/lang/String;)V
#46 = Utf8 <clinit>
#47 = Utf8 SourceFile
#48 = Utf8 XmlUtil.java
#49 = NameAndType #27:#28 // "<init>":()V
#50 = Utf8 java/lang/String
#51 = Utf8 leon
#52 = NameAndType #27:#35 // "<init>":(Ljava/lang/String;)V
#53 = Class #64 // java/lang/System
#54 = NameAndType #65:#66 // out:Ljava/io/PrintStream;
#55 = Class #67 // java/io/PrintStream
#56 = NameAndType #68:#41 // println:(I)V
#57 = NameAndType #40:#41 // method5:(I)V
#58 = Utf8 main method.
#59 = NameAndType #68:#35 // println:(Ljava/lang/String;)V
#60 = NameAndType #26:#21 // STATIC_INT_VAL:I
#61 = Utf8 com/leon/util/json/XmlUtil
#62 = Utf8 java/lang/Object
#63 = Utf8 com/leon/util/json/XmlUtil$InnerClass
#64 = Utf8 java/lang/System
#65 = Utf8 out
#66 = Utf8 Ljava/io/PrintStream;
#67 = Utf8 java/io/PrintStream
#68 = Utf8 println
{
public com.leon.util.json.XmlUtil();
descriptor: ()V
flags: 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 9: 0
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc2_w #2 // double 5.0d
6: dstore_2
7: return
LineNumberTable:
line 21: 0
line 22: 3
line 23: 7
RuntimeVisibleAnnotations:
0: #33()
public void method2(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 26: 0
public java.lang.String method3();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: new #4 // class java/lang/String
3: dup
4: ldc #5 // String leon
6: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: areturn
LineNumberTable:
line 29: 0
public java.lang.String method4(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: aconst_null
1: areturn
LineNumberTable:
line 33: 0
public void method5(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: iload_1
1: iconst_1
2: if_icmpge 15
5: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
8: iload_1
9: invokevirtual #8 // Method java/io/PrintStream.println:(I)V
12: goto 23
15: iinc 1, -1
18: aload_0
19: iload_1
20: invokevirtual #9 // Method method5:(I)V
23: return
LineNumberTable:
line 37: 0
line 38: 5
line 40: 15
line 41: 18
line 43: 23
StackMapTable: number_of_entries = 2
frame_type = 15 /* same */
frame_type = 7 /* same */
public void method6();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: iconst_5
2: invokevirtual #9 // Method method5:(I)V
5: return
LineNumberTable:
line 46: 0
line 47: 5
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String main method.
5: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 51: 0
line 52: 8
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #12 // Field STATIC_INT_VAL:I
4: return
LineNumberTable:
line 17: 0
}
SourceFile: "XmlUtil.java"
InnerClasses:
#16= #15 of #13; //InnerClass=class com/leon/util/json/XmlUtil$InnerClass of class com/leon/util/json/XmlUtil
上图中,常量池都计算出来了,并且我们发现还有一些是我们没有见过的,比如:“I”“V”“”“LineNumberTable”“LocalVariableTable”等,这些看起来在源代码中不存在的常量是哪里来的?这部分常量的确不来源于Java源代码,它们都是编译器自动生成的。在下文具体介绍到我们再说。
访问标志
在常量池结束之后,紧接着的2个字节是访问标志(access_flag)。这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等。具体的标志位以及标志的含义以及含义如下图:
上图反编译的结果来看,我们的flags: ACC_PUBLIC, ACC_SUPER。表示这是一个普通的类。
类索引、父类索引、接口索引的集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。
类索引:确定这个类的全限定名。
父类索引:这个类的父类的全限定名。Java中只允许单继承,所以父类索引只有一个,除了java.lang.Object之外,所有的类都有父类索引;
接口索引集合:由于Java的类可以实现多个接口,因此接口索引是一个集合。
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
可以修饰字段的修饰符有很多,比如作用域(public/private)、实例变量还是类变量(static修饰)、可变性(final)、序列化(transient)、可见性(volatile)。这些修饰符都很适合用标志位来表示,但是字段的数据类型、字段名这些是无法固定的,只能引用常量池中的常量来描述。字段表的结构如下:
access_flags:标志位,与类中的access_flags有点相似,都是一个u2的数据类型,可以设置的标志如下:
很明显,由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的。
name_index:对常量池项的引用,表示字段的简单名称。
descriptor_index:字段的描述符引用。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,具体如下:
通过观察这个反编译的结果,可以得知20和21项表述的是一个int类型的变量,变量名为intVal,对比源码,确实如此。
方发表集合
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式。它的方法访问标志如下:
方法的定义可以通过访问标志、名称索引、描述符索引来表达清楚,但方法里面的代码去哪里了?方法里的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。
在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名之中,所以Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式之中,特征签名的范围明显要更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。
属性表集合
属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
1.Code属性
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。它的结构如下:
这里我们挑选几个最重要的结构进行解释:
max_stack:代表了操作数栈深度的最大值。
在方法执行的任意时刻,操作数栈都不会超过这个深度。在前端编译后,一个方法的操作数栈就被定下来了。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
max_locals:表示局部变量表存储所需的空间。
它的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。变量槽的分配策略如下:
一个变量槽:对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽;
两个变量槽:而double和long这两种64位的数据类型则需要两个变量槽来存放。
此外,方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理程序的参数(Exception HandlerParameter,就是try-catch语句中catch块中所定义的异常)、方法体中定义的局部变量都需要依赖局部变量表来存放。
code_length:表示生成的字节码长度。
关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到2的32次幂,但是**《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器就会拒绝编译**。一般来讲,编写Java代码时只要不是刻意去编写一个超级长的方法来为难编译器,是不太可能超过这个最大值的限制的。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归并于一个方法之中,就有可能因为方法生成字节码超长的原因而导致编译失败。
code:用来存储Java源程序方法编译后生成的字节码指令。
字节码指令是一个u1类型的单字节,一个u1类型的取值范围是0~255,因此一共可以表达256个指令。目前,《Java虚拟机规范》已经定义了其中约200条编码值对应的指令含义,编码与指令之间的对应关系可查阅本书的附录C“虚拟机字节码指令表”。
Code属性是学习后面关于字节码执行引擎内容的必要基础,因此请静下心来仔细理解这枯燥无味的内容!
Code属性是学习后面关于字节码执行引擎内容的必要基础,因此请静下心来仔细理解这枯燥无味的内容!
Code属性是学习后面关于字节码执行引擎内容的必要基础,因此请静下心来仔细理解这枯燥无味的内容!
2.Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。
3.LineNumberTable属性
用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
4.LocalVariableTable属性
用于描述栈桢中局部变量表与Java源码中定义的变量之间的关系。
它不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:vars选项来取消或要求生成这项信息。
如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,譬如IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
5.LocalVariableTypeTable属性
把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature)。
对于非泛型类型来说,描述符和特征签名能描述的信息是能吻合一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确描述泛型类型了。因此出现了这个属性,使用字段的特征签名来完成泛型的描述。
6.ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。
对非static类型的变量(也就是实例变量)的赋值是在实例构造器< init >()方法中进行的;
而对于类变量,则有两种方式可以选择:在类构造器< clinit >()方法中或者使用ConstantValue属性。
目前HotSpot虚拟机的处理机制如下:
1)如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化;
2)如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在()方法中进行初始化。
7.InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。
8.Deprecated及Synthetic属性
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用“@deprecated”注解进行设置。
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。
9.StackMapTable属性
这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。
10.Signature属性
Signature属性会记录泛型签名信息。
Java语言的泛型采用的是擦除法实现的伪泛型,字节码(Code属性)中所有的泛型信息编译(类型变量、参数化类型)在编译之后都通通被擦除掉。
这种伪泛型擦除的好处:
实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间;
坏处:无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得泛型信息。
而Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取的泛型类型,最终的数据来源也是这个属性。
11.MethodParameters属性
MethodParameters是在JDK 8时新加入到Class文件格式中的,它是一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息。
13.模块化相关属性
JDK 9的一个重量级功能是Java的模块化功能,因为模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。
到这里Class文件大致的结构已经呈现出来了,总的结构其实就是各种表,表又由无符号数和其他子表构成的。比如常量池表、方发表、局部变量表等。通过这些表把Java源程序像庖丁解牛一样,以极其严谨的方式解剖的十分详细!区区一万五千字其实没办法把Class文件完全讲解详细,其中还有很多细节是没有展示出来,这里我们只解剖了相对重要的部分,但还有一部分,比如Code属性中的字节码指令等,必须有这部分的知识,我们才能系统性的理解大部分反编译后的Class文件。期待下一篇…