Java必知——JVM和字节码文件的详细解析!

逃离我推掉我的手 2022-12-11 11:27 294阅读 0赞

本篇文章的思维导图
在这里插入图片描述

一、JVM的简单介绍

1.1 JVM是什么?

JVM (java virtual machine),java虚拟机,是一个虚构出来的计算机,但是有自己完善的硬件结构:处理器、堆栈、寄存器等。java虚拟机是用于执行字节码文件的。

1.2 JAVA为什么能跨平台?

首先我们可以问一个这样的问题,为什么 C 语言不能跨平台?如下图:
在这里插入图片描述
C语言在不同平台上的对应的编译器会将其编译为不同的机器码文件,不同的机器码文件只能在本平台中运行。

而java文件的执行过程如图:
在这里插入图片描述
java通过javac将源文件编译为.class文件(字节码文件),该字节码文件遵循了JVM的规范,使其可以在不同系统的JVM下运行。

小结:

java 代码不是直接在计算机上执行的,而是在JVM中执行的,不同操作系统下的 JVM 不同,但是会提供相同的接口。
javac 会先将 .java 文件编译成二进制字节码文件,字节码文件与操作系统平台无关,只面向 JVM, 注意同一段代码的字节码文件是相同的。
接着JVM执行字节码文件,不同操作系统下的JVM会将同样的字节码文件映射为不同系统的API调用。
JVM不是跨平台的,java是跨平台的。

1.3 JVM为什么跨语言

前面提到”.class文件是一种遵循了JVM规范的字节码文件”,那么不难想到,只要另一种语言也同样了遵循了JVM规范,可将其源文件编译为.class文件,就也能在 JVM 上运行。如下图:
在这里插入图片描述

1.4 JDK、JRE、JVM的关系

我们看一下官方给的图:
在这里插入图片描述
三者定义

JDK:JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器(javac)、Java运行时环境(JRE),以及常用的Java类库等。

JRE:JRE( Java Runtime Environment) 、Java运行环境,用于解释执行Java的字节码文件。普通用户而只需要安装 JRE 来运行 Java 程序。而程序开发者必须安装JDK来编译、调试程序。

JVM:JVM(Java Virtual Mechinal),是JRE的一部分。负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。

区别和联系

  • JDK 用于开发,JRE 用于运行java程序 ;如果只是运行Java程序,可以只安装JRE,无需安装JDK。
  • JDk包含JRE,JDK 和 JRE 中都包含 JVM。
  • JVM 是 java 编程语言的核心并且具有平台独立性。

二、字节码文件详解

2.1 字节码文件的结构

  1. ClassFile {
  2. u4 magic;
  3. u2 minor_version;
  4. u2 major_version;
  5. u2 constant_pool_count;
  6. cp_info constant_pool[constant_pool_count-1];
  7. u2 access_flags;
  8. u2 this_class;
  9. u2 super_class;
  10. u2 interfaces_count;
  11. u2 interfaces[interfaces_count];
  12. u2 fields_count;
  13. field_info fields[fields_count];
  14. u2 methods_count;
  15. method_info methods[methods_count];
  16. u2 attributes_count;
  17. attribute_info attributes[attributes_count];
  18. }
  • “ClassFile”中的“u4、u2”等指的是每项数据的所占的长度,u4表示占4个字节,u2表示占2个字节,以此类推。
  • .class文件是以16进制组织的,一个16进制位可以用4个2进制位表示,一个2进制位是一个bit,所以一个16进制位是4个bit,两个16进制位就是8bit = 1 byte。以Main.class文件的开头cafe为例分析:
    在这里插入图片描述
    因此 u4 对应4个字节,就是 cafe babe。

接下来先分析 ClassFile的结构:

1、magic
在 class 文件开头的四个字节, 存放着 class 文件的魔数, 这个魔数是 class 文件的标志,是一个固定的值: 0xcafebabe 。 也就是说他是判断一个文件是不是 class 格式的文件的标准, 如果开头四个字节不是 0xcafebabe , 那么就说明它不是 class 文件, 不能被 JVM 识别。

2、minor_version 和 major_version
次版本号和主版本号决定了该class file文件的版本,如果 major_version 记作 M,minor_version 记作 m ,则该文件的版本号为:M.m。因此,可以按字典顺序对类文件格式的版本进行排序,例如1.5 <2.0 <2.1。当且仅当v处于 Mi.0≤v≤Mj.m 的某个连续范围内时,Java 虚拟机实现才能支持版本 v 的类文件格式。范围列表如下:
在这里插入图片描述
3、constant_pool_count
constant_pool_count 项的值等于 constant_pool 表中的条目数加1。如果 constant_pool 索引大于零且小于 constant_pool_count,则该索引被视为有效,但 CONSTANT_Long_info 和CONSTANT_Double_info 类型的常量除外。

4、constant_pool
constant_pool 是一个结构表,表示各种字符串常量,类和接口名称,字段名称以及在ClassFile 结构及其子结构中引用的其他常量。 每个 constant_pool 表条目的格式由其第一个“标签”字节指示。constant_pool 表的索引从1到 constant_pool_count-1。
Java虚拟机指令不依赖于类,接口,类实例或数组的运行时布局。 相反,指令引用了constant_pool 表中的符号信息。
所有 constant_pool 表条目均具有以下常规格式:

  1. cp_info {
  2. u1 tag;
  3. u1 info[];
  4. }

constant_pool 表中的每个条目都必须以一个1字节的标签开头,该标签指示该条目表示的常量的种类。 常量有17种,在下表中列出,并带有相应的标记。每个标签字节后必须跟两个或多个字节,以提供有关特定常数的信息。 附加信息的格式取决于标签字节,即info数组的内容随标签的值而变化。
在这里插入图片描述
5、access_flags
access_flags 项的值是标志的掩码,用于表示对该类或接口的访问权限和属性。设置后,每个标志的解释在下表中指定。
在这里插入图片描述
6、this_class
this_class 项目的值必须是指向 constant_pool 表的有效索引。该索引处的 constant_pool 条目必须是代表此类文件定义的类或接口的 CONSTANT_Class_info 结构。

  1. CONSTANT_Class_info {
  2. u1 tag;
  3. u2 name_index;
  4. }

7、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 结构。

8、interfaces_count
interfaces_count 项目的值给出了此类或接口类型的直接超接口的数量。

9、interfaces[]
接口表的每个值都必须是 constant_pool 表中的有效索引。interfaces [i]的每个值(其中0≤i <interfaces_count)上的 constant_pool 条目必须是 CONSTANT_Class_info 结构,该结构描述当前类或接口类型的直接超接口。

10、fields_count
字段计数器的值给出了 fields 表中 field_info 结构的数量。 field_info 结构代表此类或接口类型声明的所有字段,包括类变量和实例变量。

11、fields[]
字段表中的每个值都必须是field_info结构,以提供对该类或接口中字段的完整描述。 字段表仅包含此类或接口声明的字段,不包含从超类或超接口继承的字段。
字段结构如下:

  1. field_info {
  2. u2 access_flags;
  3. u2 name_index;
  4. u2 descriptor_index;
  5. u2 attributes_count;
  6. attribute_info attributes[attributes_count];
  7. }

12、methods_count
方法计数器的值表示方法表中 method_info 结构的数量。

13、methods[]
方法表中的每个值都必须是 method_info 结构,以提供对该类或接口中方法的完整描述。 如果在 method_info 结构的 access_flags 项中均未设置 ACC_NATIVE 和 ACC_ABSTRACT 标志,则还将提供实现该方法的Java虚拟机指令;
method_info 结构表示此类或接口类型声明的所有方法,包括实例方法,类方法,实例初始化方法以及任何类或接口初始化的方法。 方法表不包含表示从超类或超接口继承的方法。
方法具有如下结构:

  1. method_info {
  2. u2 access_flags;
  3. u2 name_index;
  4. u2 descriptor_index;
  5. u2 attributes_count;
  6. attribute_info attributes[attributes_count];
  7. }

14、attributes_count
属性计数器的值表示当前类的属性表中的属性数量。

15、attributes[]
注意,这里的属性并不是Java代码里面的类属性(类字段),而是Java源文件便已有特有的一些属性(不要与 fields 混淆),属性的结构:

  1. xml attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }

属性列表:
在这里插入图片描述

2.2 实例分析

首先写一段Java程序,我们熟悉的“Hello World”

  1. public class Main {
  2. public static void main(String[] args) {
  3. System.out.println("Hello World");
  4. }
  5. }

使用javac Main.java编译生成Main.class文件:

  1. cafe babe 0000 0034 001d 0a00 0600 0f09
  2. 0010 0011 0800 120a 0013 0014 0700 1507
  3. 0016 0100 063c 696e 6974 3e01 0003 2829
  4. 5601 0004 436f 6465 0100 0f4c 696e 654e
  5. 756d 6265 7254 6162 6c65 0100 046d 6169
  6. 6e01 0016 285b 4c6a 6176 612f 6c61 6e67
  7. 2f53 7472 696e 673b 2956 0100 0a53 6f75
  8. 7263 6546 696c 6501 0009 4d61 696e 2e6a
  9. 6176 610c 0007 0008 0700 170c 0018 0019
  10. 0100 0b48 656c 6c6f 2057 6f72 6c64 0700
  11. 1a0c 001b 001c 0100 044d 6169 6e01 0010
  12. 6a61 7661 2f6c 616e 672f 4f62 6a65 6374
  13. 0100 106a 6176 612f 6c61 6e67 2f53 7973
  14. 7465 6d01 0003 6f75 7401 0015 4c6a 6176
  15. 612f 696f 2f50 7269 6e74 5374 7265 616d
  16. 3b01 0013 6a61 7661 2f69 6f2f 5072 696e
  17. 7453 7472 6561 6d01 0007 7072 696e 746c
  18. 6e01 0015 284c 6a61 7661 2f6c 616e 672f
  19. 5374 7269 6e67 3b29 5600 2100 0500 0600
  20. 0000 0000 0200 0100 0700 0800 0100 0900
  21. 0000 1d00 0100 0100 0000 052a b700 01b1
  22. 0000 0001 000a 0000 0006 0001 0000 0001
  23. 0009 000b 000c 0001 0009 0000 0025 0002
  24. 0001 0000 0009 b200 0212 03b6 0004 b100
  25. 0000 0100 0a00 0000 0a00 0200 0000 0400
  26. 0800 0500 0100 0d00 0000 0200 0e

开始按照以上知识破译上面的Main.class文件
按顺序解析,首先是前10个字节:

  1. cafe babe // 魔法数,标识为.class字节码文件
  2. 0000 0034 //版本号 52.0
  3. 001d //常量池长度 constant_pool_count 29-1=28

接着开始解析常量,先查看往后的第一个字节:0a,对应的常量类型CONSTANT_Methodref,对应的结构为:

  1. CONSTANT_Methodref_info {
  2. u1 tag;
  3. u2 class_index;
  4. u2 name_and_type_index;
  5. }

tag占一个字节,class_index 占2个字节,name_and_type_index 占2个自己,依次往后数,注意0a就是tag,所以往后数2个字节是 class_index

  1. 00 06 // class_index 指向常量池中第6个常量所代表的类
  2. 00 0f // name_and_type_index 指向常量池中第15个常量所代表的方法

通过以上方法逐个解析,最终可得到常量池为:

  1. 0a // 10 CONSTANT_Methodref
  2. 00 06 // 指向常量池中第6个常量所代表的类
  3. 00 0f // 指向常量池中第15个常量所代表的方法
  4. 09 CONSTANT_Fieldref
  5. 0010 // 指向常量池中第16个常量所代表的类
  6. 0011 // 指向常量池中第17个常量所代表的变量
  7. 08 // CONSTANT_String
  8. 00 12 // 指向常量池中第18个常量所代表的变量
  9. 0a // CONSTANT_Methodref
  10. 0013 // 指向常量池中第19个常量所代表的类
  11. 0014 // 指向常量池中第20个常量所代表的方法
  12. 07 // CONSTANT_Class
  13. 00 15 // 指向常量池中第21个常量所代表的变量
  14. 07 // CONSTANT_Class
  15. 0016 // 指向常量池中第22个常量所代表的变量
  16. 01 // CONSTANT_Utf8 标识字符串
  17. 00 // 下标为0
  18. 06 // 6个字节
  19. 3c 696e 6974 3e //<init>
  20. 01 //CONSTANT_Utf8 表示字符串
  21. 00 // 下标为0
  22. 03 // 3个字节
  23. 2829 56 // ()v
  24. 01 //CONSTANT_Utf8 表示字符串
  25. 00 // 下标为0
  26. 04 // 4个字节
  27. 436f 6465 // code
  28. 01 //CONSTANT_Utf8 表示字符串
  29. 00 // 下标为0
  30. 0f // 15个字节
  31. 4c 696e 654e 756d 6265 7254 6162 6c65 //lineNumberTable
  32. 01 //CONSTANT_Utf8 表示字符串
  33. 00 // 下标为0
  34. 04 // 4个字节
  35. 6d 6169 6e //main
  36. 01
  37. 00
  38. 16
  39. 285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 //([Ljava/lang/String;)V
  40. 0100
  41. 0a //10
  42. 53 6f75 7263 6546 696c 65 //sourceFile
  43. 01 00
  44. 09
  45. 4d61 696e 2e6a 6176 61 //Main.java
  46. 0c // CONSTANT_NameAndType
  47. 0007 //nameIndex:7
  48. 0008 //descriptor_index:8
  49. 07 //CONSTANT_Class
  50. 00 17 // 第21个变量
  51. 0c
  52. 0018
  53. 0019
  54. 0100
  55. 0b
  56. 48 656c 6c6f 2057 6f72 6c64 // Hello World
  57. 07
  58. 00 1a
  59. 0c 001b 001c
  60. 0100
  61. 04
  62. 4d 6169 6e //main
  63. 01 00
  64. 10
  65. 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 //java/lang/Object
  66. 0100
  67. 10
  68. 6a 6176 612f 6c61 6e67 2f53 7973 7465 6d // java/lang/System
  69. 01 00
  70. 03
  71. 6f75 74 // out
  72. 01 00
  73. 15
  74. 4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b //Ljava/io/PrintStream;
  75. 01 00
  76. 13
  77. 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d // java/io/PrintStrea
  78. 01 00
  79. 07
  80. 7072 696e 746c 6e //println
  81. 01 00
  82. 15
  83. 284c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 56 // (ljava/lang/String/String;)V

常量池往后的结构可继续按照这种方式进行解析。现在我们采用java自带的方法来将.class文件反编译,并验证我们以上的解析是正确的。
使用javap -v Main.class可得到:

  1. Last modified 2020-9-29; size 413 bytes
  2. MD5 checksum 8b2b7cdf6c4121be8e242746b4dea946
  3. Compiled from "Main.java"
  4. public class Main
  5. minor version: 0
  6. major version: 52
  7. flags: ACC_PUBLIC, ACC_SUPER
  8. Constant pool:
  9. #1 = Methodref #6.#15 // java/lang/Object."<init>":()V
  10. #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
  11. #3 = String #18 // Hello World
  12. #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
  13. #5 = Class #21 // Main
  14. #6 = Class #22 // java/lang/Object
  15. #7 = Utf8 <init>
  16. #8 = Utf8 ()V
  17. #9 = Utf8 Code
  18. #10 = Utf8 LineNumberTable
  19. #11 = Utf8 main
  20. #12 = Utf8 ([Ljava/lang/String;)V
  21. #13 = Utf8 SourceFile
  22. #14 = Utf8 Main.java
  23. #15 = NameAndType #7:#8 // "<init>":()V
  24. #16 = Class #23 // java/lang/System
  25. #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
  26. #18 = Utf8 Hello World
  27. #19 = Class #26 // java/io/PrintStream
  28. #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
  29. #21 = Utf8 Main
  30. #22 = Utf8 java/lang/Object
  31. #23 = Utf8 java/lang/System
  32. #24 = Utf8 out
  33. #25 = Utf8 Ljava/io/PrintStream;
  34. #26 = Utf8 java/io/PrintStream
  35. #27 = Utf8 println
  36. #28 = Utf8 (Ljava/lang/String;)V
  37. {
  38. public Main();
  39. descriptor: ()V
  40. flags: ACC_PUBLIC
  41. Code:
  42. stack=1, locals=1, args_size=1
  43. 0: aload_0
  44. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  45. 4: return
  46. LineNumberTable:
  47. line 1: 0
  48. public static void main(java.lang.String[]);
  49. descriptor: ([Ljava/lang/String;)V
  50. flags: ACC_PUBLIC, ACC_STATIC
  51. Code:
  52. stack=2, locals=1, args_size=1
  53. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  54. 3: ldc #3 // String Hello World
  55. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  56. 8: return
  57. LineNumberTable:
  58. line 4: 0
  59. line 5: 8
  60. }
  61. SourceFile: "Main.java"

对比下可以发现与我们人工解析的结果是一致的。

小结

本文第一部分围绕JVM的几个常见的问题做了一些简单介绍。第二部分详细介绍了ClassFile的结构及 JVM 对 ClassFile 指定的规范(更多详细的规范有兴趣的读者可查看官方文档),接着按照规范进行了部分字节码的手动解析,并与 JVM 的解析结果进行了对比。个人认为作为偏应用层的programer没必要去记忆这些“规范”,而是要跳出这些繁杂的规范掌握到以下几点:

  1. 会借助官方文档对字节码文件做简单阅读。
  2. 理解字节码文件在整个执行过程的角色和作用,其实就是一个“编解码”的过程。javac将.java文件按照JVM的规则生成字节码文件,JVM按照规范解析字节码文件为机器可执行的指令。

最后祝大家都能工作顺利!我这还整理有很多的2020年Java面试题和Java核心学习资料,有需要的朋友可以点击进入,暗号:csgg,免费提供!
在这里插入图片描述
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,294人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM 字节文件

    计算机不能直接运行java代码,要先运行虚拟机,再由java虚拟机运行编译后java代码。 > 为什么不能直接运行java代码? 计算机所有的操作都是通过一个个指令集汇集后