zoukankan      html  css  js  c++  java
  • 玩命学JVM(一)—认识JVM和字节码文件

    本篇文章的思维导图
    Alt

    一、JVM的简单介绍

    1.1 JVM是什么?

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

    1.2 JAVA为什么能跨平台?

    首先我们可以问一个这样的问题,为什么 C 语言不能跨平台?如下图:
    Alt

    C语言在不同平台上的对应的编译器会将其编译为不同的机器码文件,不同的机器码文件只能在本平台中运行。

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

    小结

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

    1.3 JVM为什么跨语言

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

    1.4 JDK、JRE、JVM的关系

    我们看一下官方给的图:
    Alt

    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为例分析:
      Alt
      因此 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 的类文件格式。范围列表如下:
      Alt
    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 表条目均具有以下常规格式:
      cp_info {
          u1 tag;
          u1 info[];
      }
      

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

    1. access_flags
      access_flags 项的值是标志的掩码,用于表示对该类或接口的访问权限和属性。设置后,每个标志的解释在下表中指定。
      Alt

    2. this_class
      this_class 项目的值必须是指向 constant_pool 表的有效索引。该索引处的 constant_pool 条目必须是代表此类文件定义的类或接口的 CONSTANT_Class_info 结构。

      CONSTANT_Class_info {
            u1 tag;
            u2 name_index;
      }
      
    3. 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 结构。

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

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

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

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

            field_info {
                u2             access_flags;
                u2             name_index;
                u2             descriptor_index;
                u2             attributes_count;
                attribute_info attributes[attributes_count];
            }
      
    8. methods_count
      方法计数器的值表示方法表中 method_info 结构的数量。

    9. 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];
          }
      
    10. attributes_count
      属性计数器的值表示当前类的属性表中的属性数量。

    11. attributes[]
      注意,这里的属性并不是Java代码里面的类属性(类字段),而是Java源文件便已有特有的一些属性(不要与 fields 混淆),属性的结构:
      xml attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
      属性列表:
      Alt

    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按照规范解析字节码文件为机器可执行的指令。

    三、参考文献

    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

  • 相关阅读:
    知识图谱学习与实践(4)——通过例句介绍Sparql的使用
    知识图谱学习与实践(3)——知识表示
    知识图谱学习与实践(2)——知识图谱数据模型的构建
    知识图谱学习与实践(1)——知识图谱的演化过程
    NIO客户端主要创建过程
    NIO服务端主要创建过程
    Relative path in absolute URI: ${system:java.io.tmpdir%7D/$%7Bhive.session.id%7D_resources
    ubuntu中mysql安装失败
    使用ant build build.xml报“includeantruntime was not set”警告及"Class not found: javac1.8"问题
    maven编译报错 -source 1.5 中不支持 lambda(或diamond) 表达式,编码 UTF-8 的不可映射字符
  • 原文地址:https://www.cnblogs.com/cleverziv/p/13751488.html
Copyright © 2011-2022 走看看