玩命学JVM:认识JVM和字节码文件

时间:2022-07-27
本文章向大家介绍玩命学JVM:认识JVM和字节码文件,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

点击上方「蓝字」关注我们

source:https://www.cnblogs.com/cleverziv/p/13751488.html

本篇文章的思维导图

一、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的关系

我们看一下官方给的图:

1.4.1 三者定义

  • 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字节码文件的虚拟计算机。

1.4.2 区别和联系

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

二、字节码文件详解

官方文档地址:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.1

2.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];
}
  • "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 的类文件格式。范围列表如下:
  1. constant_pool_count constant_pool_count 项的值等于 constant_pool 表中的条目数加1。如果 constant_pool 索引大于零且小于 constant_pool_count,则该索引被视为有效,但 CONSTANT_Long_info 和CONSTANT_Double_info 类型的常量除外。
  2. constant_pool constant_pool 是一个结构表,表示各种字符串常量,类和接口名称,字段名称以及在ClassFile 结构及其子结构中引用的其他常量。每个 constant_pool 表条目的格式由其第一个“标签”字节指示。constant_pool 表的索引从1到 constant_pool_count-1。 Java虚拟机指令不依赖于类,接口,类实例或数组的运行时布局。相反,指令引用了constant_pool 表中的符号信息。 所有 constant_pool 表条目均具有以下常规格式: cp_info { u1 tag; u1 info[]; }

constant_pool 表中的每个条目都必须以一个1字节的标签开头,该标签指示该条目表示的常量的种类。常量有17种,在下表中列出,并带有相应的标记。每个标签字节后必须跟两个或多个字节,以提供有关特定常数的信息。附加信息的格式取决于标签字节,即info数组的内容随标签的值而变化。

  1. access_flags access_flags 项的值是标志的掩码,用于表示对该类或接口的访问权限和属性。设置后,每个标志的解释在下表中指定。
  1. this_class this_class 项目的值必须是指向 constant_pool 表的有效索引。该索引处的 constant_pool 条目必须是代表此类文件定义的类或接口的 CONSTANT_Class_info 结构。 CONSTANT_Class_info { u1 tag; u2 name_index; }
  2. 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 结构。
  3. interfaces_count interfaces_count 项目的值给出了此类或接口类型的直接超接口的数量。
  4. interfaces[] 接口表的每个值都必须是 constant_pool 表中的有效索引。interfaces [i]的每个值(其中0≤i <interfaces_count)上的 constant_pool 条目必须是 CONSTANT_Class_info 结构,该结构描述当前类或接口类型的直接超接口。
  5. fields_count 字段计数器的值给出了 fields 表中 field_info 结构的数量。field_info 结构代表此类或接口类型声明的所有字段,包括类变量和实例变量。
  6. fields[] 字段表中的每个值都必须是field_info结构,以提供对该类或接口中字段的完整描述。字段表仅包含此类或接口声明的字段,不包含从超类或超接口继承的字段。 字段结构如下: field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
  7. methods_count 方法计数器的值表示方法表中 method_info 结构的数量。
  8. 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]; }
  9. attributes_count 属性计数器的值表示当前类的属性表中的属性数量。
  10. attributes[] 注意,这里的属性并不是Java代码里面的类属性(类字段),而是Java源文件便已有特有的一些属性(不要与 fields 混淆),属性的结构: xml attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; } 属性列表:

2.2 实例分析

首先写一段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没必要去记忆这些“规范”,而是要跳出这些繁杂的规范掌握到以下几点:

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

扫码二维码

获取更多精彩

Java乐园

有用!分享+在看☟