本篇文章的思维导图
JVM (java virtual machine),java虚拟机,是一个虚构出来的计算机,但是有自己完善的硬件结构:处理器、堆栈、寄存器等。java虚拟机是用于执行字节码文件的。
首先我们可以问一个这样的问题,为什么 C 语言不能跨平台?如下图:
C语言在不同平台上的对应的编译器会将其编译为不同的机器码文件,不同的机器码文件只能在本平台中运行。
而java文件的执行过程如图:
java通过javac将源文件编译为.class文件(字节码文件),该字节码文件遵循了JVM的规范,使其可以在不同系统的JVM下运行。
小结
前面提到".class文件是一种遵循了JVM规范的字节码文件",那么不难想到,只要另一种语言也同样了遵循了JVM规范,可将其源文件编译为.class文件,就也能在 JVM 上运行。如下图:
我们看一下官方给的图:
官方文档地址:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.1
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
.class文件是以16进制组织的,一个16进制位可以用4个2进制位表示,一个2进制位是一个bit,所以一个16进制位是4个bit,两个16进制位就是8bit = 1 byte。以Main.class文件的开头cafe为例分析:
cafe babe接下来先分析 ClassFile的结构:
class file文件的版本,如果 major_version 记作 M,minor_version 记作 m ,则该文件的版本号为:M.m。因此,可以按字典顺序对类文件格式的版本进行排序,例如1.5 <2.0 <2.1。当且仅当v处于 Mi.0≤v≤Mj.m 的某个连续范围内时,Java 虚拟机实现才能支持版本 v 的类文件格式。范围列表如下:
cp_info {
u1 tag;
u1 info[];
}
constant_pool 表中的每个条目都必须以一个1字节的标签开头,该标签指示该条目表示的常量的种类。 常量有17种,在下表中列出,并带有相应的标记。每个标签字节后必须跟两个或多个字节,以提供有关特定常数的信息。 附加信息的格式取决于标签字节,即info数组的内容随标签的值而变化。
access_flags
access_flags 项的值是标志的掩码,用于表示对该类或接口的访问权限和属性。设置后,每个标志的解释在下表中指定。
this_class
this_class 项目的值必须是指向 constant_pool 表的有效索引。该索引处的 constant_pool 条目必须是代表此类文件定义的类或接口的 CONSTANT_Class_info 结构。
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
super_class
对于一个类,父类索引的值必须为零或必须是 constant_pool 表中的有效索引。 如果super_class 项的值非零,则该索引处的 constant_pool 条目必须是 CONSTANT_Class_info 结构,该结构表示此类文件定义的类的直接超类。 直接超类或其任何超类都不能在其 ClassFile结构的 access_flags 项中设置 ACC_FINAL 标志。如果 super_class 项的值为零,则该类只可能是 java.lang.Object ,这是没有直接超类的唯一类或接口。对于接口,父类索引的值必须始终是 constant_pool 表中的有效索引。该索引处的 constant_pool 条目必须是 java.lang.Object 的CONSTANT_Class_info 结构。
interfaces_count
interfaces_count 项目的值给出了此类或接口类型的直接超接口的数量。
interfaces[]
接口表的每个值都必须是 constant_pool 表中的有效索引。interfaces [i]的每个值(其中0≤i <interfaces_count)上的 constant_pool 条目必须是 CONSTANT_Class_info 结构,该结构描述当前类或接口类型的直接超接口。
fields_count
字段计数器的值给出了 fields 表中 field_info 结构的数量。 field_info 结构代表此类或接口类型声明的所有字段,包括类变量和实例变量。
fields[]
字段表中的每个值都必须是field_info结构,以提供对该类或接口中字段的完整描述。 字段表仅包含此类或接口声明的字段,不包含从超类或超接口继承的字段。
字段结构如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
methods_count
方法计数器的值表示方法表中 method_info 结构的数量。
methods[]
方法表中的每个值都必须是 method_info 结构,以提供对该类或接口中方法的完整描述。 如果在 method_info 结构的 access_flags 项中均未设置 ACC_NATIVE 和 ACC_ABSTRACT 标志,则还将提供实现该方法的Java虚拟机指令;
method_info 结构表示此类或接口类型声明的所有方法,包括实例方法,类方法,实例初始化方法以及任何类或接口初始化的方法。 方法表不包含表示从超类或超接口继承的方法。
方法具有如下结构:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
attributes_count
属性计数器的值表示当前类的属性表中的属性数量。
attributes[]
注意,这里的属性并不是Java代码里面的类属性(类字段),而是Java源文件便已有特有的一些属性(不要与 fields 混淆),属性的结构:xml attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
属性列表:
首先写一段Java程序,我们熟悉的“Hello World”
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
使用javac Main.java编译生成Main.class文件:
cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 0009 4d61 696e 2e6a
6176 610c 0007 0008 0700 170c 0018 0019
0100 0b48 656c 6c6f 2057 6f72 6c64 0700
1a0c 001b 001c 0100 044d 6169 6e01 0010
6a61 7661 2f6c 616e 672f 4f62 6a65 6374
0100 106a 6176 612f 6c61 6e67 2f53 7973
7465 6d01 0003 6f75 7401 0015 4c6a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
3b01 0013 6a61 7661 2f69 6f2f 5072 696e
7453 7472 6561 6d01 0007 7072 696e 746c
6e01 0015 284c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b29 5600 2100 0500 0600
0000 0000 0200 0100 0700 0800 0100 0900
0000 1d00 0100 0100 0000 052a b700 01b1
0000 0001 000a 0000 0006 0001 0000 0001
0009 000b 000c 0001 0009 0000 0025 0002
0001 0000 0009 b200 0212 03b6 0004 b100
0000 0100 0a00 0000 0a00 0200 0000 0400
0800 0500 0100 0d00 0000 0200 0e
开始按照以上知识破译上面的Main.class文件
按顺序解析,首先是前10个字节:
cafe babe // 魔法数,标识为.class字节码文件
0000 0034 //版本号 52.0
001d //常量池长度 constant_pool_count 29-1=28
接着开始解析常量,先查看往后的第一个字节:0a,对应的常量类型CONSTANT_Methodref,对应的结构为:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
tag占一个字节,class_index 占2个字节,name_and_type_index 占2个自己,依次往后数,注意0a就是tag,所以往后数2个字节是 class_index
00 06 // class_index 指向常量池中第6个常量所代表的类
00 0f // name_and_type_index 指向常量池中第15个常量所代表的方法
通过以上方法逐个解析,最终可得到常量池为:
0a // 10 CONSTANT_Methodref
00 06 // 指向常量池中第6个常量所代表的类
00 0f // 指向常量池中第15个常量所代表的方法
09 CONSTANT_Fieldref
0010 // 指向常量池中第16个常量所代表的类
0011 // 指向常量池中第17个常量所代表的变量
08 // CONSTANT_String
00 12 // 指向常量池中第18个常量所代表的变量
0a // CONSTANT_Methodref
0013 // 指向常量池中第19个常量所代表的类
0014 // 指向常量池中第20个常量所代表的方法
07 // CONSTANT_Class
00 15 // 指向常量池中第21个常量所代表的变量
07 // CONSTANT_Class
0016 // 指向常量池中第22个常量所代表的变量
01 // CONSTANT_Utf8 标识字符串
00 // 下标为0
06 // 6个字节
3c 696e 6974 3e //<init>
01 //CONSTANT_Utf8 表示字符串
00 // 下标为0
03 // 3个字节
2829 56 // ()v
01 //CONSTANT_Utf8 表示字符串
00 // 下标为0
04 // 4个字节
436f 6465 // code
01 //CONSTANT_Utf8 表示字符串
00 // 下标为0
0f // 15个字节
4c 696e 654e 756d 6265 7254 6162 6c65 //lineNumberTable
01 //CONSTANT_Utf8 表示字符串
00 // 下标为0
04 // 4个字节
6d 6169 6e //main
01
00
16
285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 //([Ljava/lang/String;)V
0100
0a //10
53 6f75 7263 6546 696c 65 //sourceFile
01 00
09
4d61 696e 2e6a 6176 61 //Main.java
0c // CONSTANT_NameAndType
0007 //nameIndex:7
0008 //descriptor_index:8
07 //CONSTANT_Class
00 17 // 第21个变量
0c
0018
0019
0100
0b
48 656c 6c6f 2057 6f72 6c64 // Hello World
07
00 1a
0c 001b 001c
0100
04
4d 6169 6e //main
01 00
10
6a61 7661 2f6c 616e 672f 4f62 6a65 6374 //java/lang/Object
0100
10
6a 6176 612f 6c61 6e67 2f53 7973 7465 6d // java/lang/System
01 00
03
6f75 74 // out
01 00
15
4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b //Ljava/io/PrintStream;
01 00
13
6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d // java/io/PrintStrea
01 00
07
7072 696e 746c 6e //println
01 00
15
284c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 56 // (ljava/lang/String/String;)V
常量池往后的结构可继续按照这种方式进行解析。现在我们采用java自带的方法来将.class文件反编译,并验证我们以上的解析是正确的。
使用javap -v Main.class可得到:
Last modified 2020-9-29; size 413 bytes
MD5 checksum 8b2b7cdf6c4121be8e242746b4dea946
Compiled from "Main.java"
public class Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Main
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Main.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Main
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
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 1: 0
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 #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 4: 0
line 5: 8
}
SourceFile: "Main.java"
对比下可以发现与我们人工解析的结果是一致的。
本文第一部分围绕JVM的几个常见的问题做了一些简单介绍。第二部分详细介绍了ClassFile的结构及 JVM 对 ClassFile 指定的规范(更多详细的规范有兴趣的读者可查看官方文档),接着按照规范进行了部分字节码的手动解析,并与 JVM 的解析结果进行了对比。个人认为作为偏应用层的programer没必要去记忆这些“规范”,而是要跳出这些繁杂的规范掌握到以下几点:
参考文献:
https://blog.csdn.net/peng_zhanxuan/article/details/104329859
https://docs.oracle.com/javase/specs/jvms/se11/html/index.html
https://blog.csdn.net/weelyy/article/details/78969412