目录
•写在前面
提到java,我们可能第一时间想起的就是那句口号,“一次编写,到处运行”,这体现了java与平台无关的优势,而实现这种特性的的基础,是通过将java编译成字节码文件,虚拟机可以载入和执行同一种平台无关的字节码,从而实现这个平台无关性。但是,如果我们换一种思路就会发现,既然虚拟机直接载入的是字节码文件,也就是说并不直接执行java文件,这也就是说虚拟机其实并不关心是什么语言编译成的字节码文件,只要你提供给我字节码文件,虚拟机就能执行,这一点便体现了java虚拟机的语言无关性。有很多可以在java虚拟机上运行的语言,比如Glojure、JRuby、Jython、Scala等等,这些语言都是通过编译成字节码文件,在java虚拟机上执行的。说白了,实现语言无关性的基础仍然是虚拟机和字节码储存格式,java虚拟机不和包括java在内的任何语言绑定,它只和“Class文件”这种特定的二进制文件格式关联,class文件中包含了java虚拟机指令集和符号表以及若干的其他辅助信息。值得一提的是,既然无论什么语言,只要编译class文件,虚拟机就可以运行,这也就意味着class文件有着许多强制性的预发和结构化的约束来保证安全性。任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件中,比如类或接口可以通过类加载器直接生成(类加载器看我另一篇文章)。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这也导致整个class文件中储存的内容几乎全部都是程序运行的必要数据,没有空隙存在,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的分割成若干个8位字节进行储存。
•无符号数和表
根据java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型,即无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表,具体的基本数据项可以看下面这个图。
无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一个类型的数据为某一类型的集合。
下面将具体介绍各数据项的具体细节,在此之前,再一次强调,class文件中由于没有任何分隔符号,所以无论是顺序还是数量,甚至是数据储存的字节序这样的细节,都被严格限定了,哪个字节代表什么含义、长度是多少、先后顺序如何、都不允许改变。后面具体分析的class文件,使用winhex打开,class文件是我随便在以前的项目找的,你们也可以打开一个class进行查看验证。这里我先贴一张总的出来,后面单独数据项分析的时候,我就部分截图了。
•魔数和Class文件的版本
每个Class文件的头4个字节称为魔数。它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件,可以这么理解,魔数是某个文件格式的标识,很多文件储存标准中,都是使用魔数来进行身份识别,比如图片格式,像gif、jpeg等在文件头中都存有魔数。使用魔数而不是拓展名来进行识别主要是基于安全方面考虑,因为文件拓展名可以随意的改动。所以如果你想要的一个自己的类型格式文件,你可以自由的选择魔数值(当然啦,不要和现有格式混淆),所以class文件的魔数是0xCAFEBABE,看下面图。
紧接着魔数的4个字节存储的是class文件的版本号,第5和第6个字节是次版本号,第7和第8个字节是主版本号。这里大概说一下java版本号的规则,java的版本号是从45开始的,JDK之后的每个JDK大版本发布主版本号向上加1(它之前的JDK1.0-1.1使用的是45.0-45.3),高版本的JDK能向下兼容以前版本的class文件,但不能运行以后版本的class文件,即使class文件格式没有发生任何变化,虚拟机也必须拒绝执行超过其版本号的class文件。我的是1.8
•常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为class文件之中的资源仓库,它是class文件结构中于其他项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时它还是在class文件中第一个出现的表类型数据项目。由于常量的数量不确定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。值得一提的是,和java习惯不一样的是,这个容量计数是从1开始的,而不是从0开始了。看我打开的这个class文件的常量池容量是0x0036,即十进制的54,这就代表常量池中有53项常量,索引值为1-53。
常量池中主要存放两大类常量:字面量和符号引用,字面量如文本字符串、声明为final的常量值,而符号引用则属于编译原理方面的概念,包含了类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。值得一提的是,java代码在进行javac编译的时候,在虚拟机加载class文件的时候进行动态连接的,所以说在class文件中不会保存各个方法、字段的最终内存布局信息。因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址。虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。常量池中的每一项常量都是一个表,大概如下
我们可以对照表查看常量对应的类型,下图我的里面对应的是0x0A,十进制是10,在表中就是指向声明方法的类描述符。这个字节是标志位,标志的是什么类型,我们已经知道了类型是10,纳闷按照表,接下来的四个字节(分为两个2字节)分别代表的意思看图表。
后面的常量也是按照这个方式计算出来,当然,如果嫌烦,可以使用javap工具进行分析(可以参考我另一篇文章,JDK的命令行工具)使用javap的-verbose参数输出。内容如下,太长了我就不截图了,我直接黏贴内容过来,看看就好。
C:\Program Files\Java\jdk1.8.0_191\bin>javap -verbose Main
Classfile /C:/Program Files/Java/jdk1.8.0_191/bin/Main.class
Last modified 2019-12-19; size 965 bytes
MD5 checksum f0e541356c7d5365134c19dd1c16e9ac
Compiled from "Main.java"
public class Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#32 // java/lang/Object."<init>":()V
#2 = Class #33 // com/dbc/leecode/Algorithm/Reclass/ListNode
#3 = Methodref #2.#34 // com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
#4 = Fieldref #2.#35 // com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
#5 = Fieldref #36.#37 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Integer -2147483648
#7 = Methodref #38.#39 // com/dbc/leecode/Algorithm/Solution21_30/Solution30.divide:(II)I
#8 = Methodref #40.#41 // java/io/PrintStream.println:(I)V
#9 = Class #42 // Main
#10 = Class #43 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 LMain;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 listNode1
#23 = Utf8 Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
#24 = Utf8 listNode2
#25 = Utf8 listNode3
#26 = Utf8 listNode4
#27 = Utf8 listNode5
#28 = Utf8 s
#29 = Utf8 [I
#30 = Utf8 SourceFile
#31 = Utf8 Main.java
#32 = NameAndType #11:#12 // "<init>":()V
#33 = Utf8 com/dbc/leecode/Algorithm/Reclass/ListNode
#34 = NameAndType #11:#44 // "<init>":(I)V
#35 = NameAndType #45:#23 // next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
#36 = Class #46 // java/lang/System
#37 = NameAndType #47:#48 // out:Ljava/io/PrintStream;
#38 = Class #49 // com/dbc/leecode/Algorithm/Solution21_30/Solution30
#39 = NameAndType #50:#51 // divide:(II)I
#40 = Class #52 // java/io/PrintStream
#41 = NameAndType #53:#44 // println:(I)V
#42 = Utf8 Main
#43 = Utf8 java/lang/Object
#44 = Utf8 (I)V
#45 = Utf8 next
#46 = Utf8 java/lang/System
#47 = Utf8 out
#48 = Utf8 Ljava/io/PrintStream;
#49 = Utf8 com/dbc/leecode/Algorithm/Solution21_30/Solution30
#50 = Utf8 divide
#51 = Utf8 (II)I
#52 = Utf8 java/io/PrintStream
#53 = Utf8 println
{
public Main();
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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LMain;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=7, args_size=1
0: new #2 // class com/dbc/leecode/Algorithm/Reclass/ListNode
3: dup
4: iconst_1
5: invokespecial #3 // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
8: astore_1
9: new #2 // class com/dbc/leecode/Algorithm/Reclass/ListNode
12: dup
13: iconst_2
14: invokespecial #3 // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
17: astore_2
18: new #2 // class com/dbc/leecode/Algorithm/Reclass/ListNode
21: dup
22: iconst_3
23: invokespecial #3 // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
26: astore_3
27: new #2 // class com/dbc/leecode/Algorithm/Reclass/ListNode
30: dup
31: iconst_4
32: invokespecial #3 // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
35: astore 4
37: new #2 // class com/dbc/leecode/Algorithm/Reclass/ListNode
40: dup
41: iconst_5
42: invokespecial #3 // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
45: astore 5
47: aload_1
48: aload_2
49: putfield #4 // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
52: aload_2
53: aload_3
54: putfield #4 // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
57: aload_3
58: aload 4
60: putfield #4 // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
63: aload 4
65: aload 5
67: putfield #4 // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
70: bipush 6
72: newarray int
74: dup
75: iconst_0
76: iconst_1
77: iastore
78: dup
79: iconst_1
80: iconst_0
81: iastore
82: dup
83: iconst_2
84: iconst_m1
85: iastore
86: dup
87: iconst_3
88: iconst_0
89: iastore
90: dup
91: iconst_4
92: bipush -2
94: iastore
95: dup
96: iconst_5
97: iconst_2
98: iastore
99: astore 6
101: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
104: ldc #6 // int -2147483648
106: iconst_m1
107: invokestatic #7 // Method com/dbc/leecode/Algorithm/Solution21_30/Solution30.divide:(II)I
110: invokevirtual #8 // Method java/io/PrintStream.println:(I)V
113: return
LineNumberTable:
line 11: 0
line 12: 9
line 13: 18
line 14: 27
line 15: 37
line 16: 47
line 17: 52
line 18: 57
line 19: 63
line 21: 70
line 22: 101
line 23: 113
LocalVariableTable:
Start Length Slot Name Signature
0 114 0 args [Ljava/lang/String;
9 105 1 listNode1 Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
18 96 2 listNode2 Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
27 87 3 listNode3 Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
37 77 4 listNode4 Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
47 67 5 listNode5 Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
101 13 6 s [I
}
SourceFile: "Main.java"
•访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型、如果是类的话,是否声明为final等,具体的看下面的这个表,依旧是对应表去看十六进制数。
•类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,class文件中由着三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于java不能多重继承,所以父类索引只有一个,除了java.lang.Object 之外,所有的java类都有父类,因此除了Object外,所有的java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序依次在接口缩影集合中。类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义的CONSTANT_Utf8_info类型的常量中的全限定名字符串。整个过程和上面的查表过程是一样的,这里就不重复推了。
•字段表集合
字段表用于描述接口或者类中声明的变量,字段包括类级变量以及实力级变量,但不包括在方法内部声明的局部变量。我们可以想一想在java中描述一个字段可以包含什么信息?可以包含的信息有:字段的作用域、实例变量还是类变量、可见性、并发可见性、是否强制从主内存读写、可否被序列化、字段数据类型、字段名称。上面的这些信息都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义了什么数据类型,这些都无法固定,只能引用常量池中的常量来描述
这里解释一下全限定名、简单名称、描述符三个概念,全限定名和简单名称很好理解,看之前我用javap里面的结果,“com/dbc/leecode/Algorithm/Reclass/ListNode”是这个类的全限定名,仅仅是把类的全名中的"."换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般都会加入一个“;”表示全限定名结束。简单名称是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别是"inc"和“m”。相对于全限定名和简单名称来说,方法和字段的描述符就要复杂一些,描述符的作用是用来描述字段的数据类型、方法的参数列表和返回值,具体看上面的描述符字符含义。
这里讲一些特殊的类型,数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/String”,一个整型数组“int[]”将被标记为“[I”。描述方法时,按照先参数列表,后返回值的顺序描述,参数列表的参数严格顺序放在一组小括号“()”之内,如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String”,方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target, int tarOffset, int targetCount, int fromIndex)的描述为“([CII[CIII)I”
•方法表集合
方法表的命名和字段表差不多,这里我就不多讲,直接贴上查表。