本篇文章的思惟导图
html
JVM (java virtual machine),java虚拟机,是一个虚构出来的计算机,可是有本身完善的硬件结构:处理器、堆栈、寄存器等。java虚拟机是用于执行字节码文件的。java
首先咱们能够问一个这样的问题,为何 C 语言不能跨平台?以下图:
编程
C语言在不一样平台上的对应的编译器会将其编译为不一样的机器码文件,不一样的机器码文件只能在本平台中运行。数组
而java文件的执行过程如图:
java经过javac将源文件编译为.class文件(字节码文件),该字节码文件遵循了JVM的规范,使其能够在不一样系统的JVM下运行。oracle
小结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
的结构:this
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