美文网首页Java从入门到实践
Java虚拟机—Class文件结构

Java虚拟机—Class文件结构

作者: Sunflow007 | 来源:发表于2020-03-07 17:53 被阅读0次
    5.jpg

    前言:

    在前几篇文章中:

    Java虚拟机——字节码、机器码和JVM
    Java虚拟机——类加载机制和类加载器
    Java虚拟机—堆、栈、运行时数据区

    我们大概介绍了JVM、字节码、类加载器和JVM运行时数据区的概念,现在让我们进入JVM的重要部分—.class文件的结构。所以本篇文章的主题主要包含以下2个部分:

    1.Java语言的平台无关性和JVM的语言无关性

    2.字节码.class文件的结构


    1.Java语言的平台无关性和JVM的语言无关性

    Java语言的平台无关性

    《深入理解Java虚拟机-第二版》中第6章开头就写到:

    代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

    为什么这么说呢?作者下面也解释的很明白。因为在虚拟机出现之前,程序要想正确运行在计算机上,首先要将代码编译成二进制本地机器码,而这个过程是和电脑的操作系统OS、CPU指令集强相关的,所以可能代码只能在某种特定的平台下运行,而换一个平台或操作系统就无法正确运行了。随着虚拟机的出现,直接将程序编译成机器码,已经不再是唯一的选择了。越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。Java就是这样一种语言。“一次编写,到处运行”于是成立Java的宣传口号。

    正是虚拟机和字节码(ByteCode)构成了平台无关性的基石,从而实现“一次编写,到处运行”

    Java虚拟机将.java文件编译成字节码,而.class字节码文件经过JVM转化为当前平台下的机器码后再进行程序执行。这样,程序猿就无需重复编写代码来适应不同平台了,而是一套代码处处运行,至于字节码怎样转化成对应平台下的机器码,那就是Java虚拟机的事情了。

    JVM的语言无关性

    Java语言通过JVM虚拟机和字节码(ByteCode)实现了平台无关性,那么语言无关性又是什么意思?其实,在Java虚拟机设计之初,作者非常前瞻性的说过:

    "In the future,we will consider bounded extensions to the Java virtual machine to provide better support for other languages" 在未来,我们会对java虚拟机进行适当的拓展,以便更好的支持其他语言运行于JVM之上。

    时至今日,商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Groovy,JRuby,Jython,Scala等等。这些语言通过各自的编译器编译成为.class文件,从而可以被JVM所执行。

    image

    所以,由于Java虚拟机设计之初的定位,以及字节码(ByteCode)的存在,使得JVM可以执行不同语言下的字节码.class文件,从而构成了语言无关性的基础。或许在未来,语言无关性的优势会赶超Java平台无关性的优势。。。

    2.字节码.class文件的结构

    根据Java虚拟机的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有2种数据类型:无符号数和表。

    当然无论是无符号数还是表,Class文件都是以8位(8bit),一个字节为单位存储的,各个数据项目紧密无间隔排列的二进制流。当数据项长度超过8位时,按照高位在前(Big Endian)的方式分隔成若干个8位字节存储。

    无符号数用u表示后面跟1、2、4、8代表1个字节、2个字节、4个字节、8个字节。无符号数用来描述数字、索引引用、数量值、字符串值。

    表则是由无符号数或者其他表作为数据复合而成的数据类型,所有表都习惯以_info结尾。

    整个Class文件实质上就是一张表,其中的数据项由各个子表和无符号数构成。Class文件的格式如下:

    image

    此处需要注意的是,由于class文件没有任何分隔符号,所有.class文件中所有的数据项(表或无符号数)都是按照图表中的顺序依次排列好的,所以我们可以在.class文件中依照字节的顺序来查看对应数据项的详细信息。

    这里我们以一个.class文件为例,看看其具体的字节信息,源码如下:

    package JustCoding.Practise;
    
    public class ConstantPool {
    
        private static String a = "Class";
    
        public int VERSION = 100;
    
        private static void test1(String s){
            String b = "Method ";
            String c = b + s;
            System.out.println("合并后的字符串:"+c);
        }
        public static void main(String[] args){
            test1(a);
        }
    }
    

    用vim打开其.class文件查看其16进制文件如下:

    image

    魔数magic

    如上图所示,是ConstantPool.class文件的16进制表示,前4个字节为ca fe ba be,这个即为表6-1中的magic,magic译为“魔数”,在.class文件的头4个字节,它的唯一作用是确定这个文件是否是能够被虚拟机识别的class文件,其值是固定的为0xCAFEBABE(咖啡宝贝),这个也是Java语言中一段有意思的“黑”历史了,哈哈🙃

    版本声明major_version、minor_version

    紧挨着魔数后的第5、6两个字节存储的是minor_version,即Java的次版本号,第7、8两个字节是major_version主版本号。可以看见这里此版本号为0x0000,主版本号为0x0034。

    每个Java版本都有对应的主、次版本号可以查询。
    例子中的0x0034对应10进制的52,表示JDK的主版本号为1.8。

    常量池计数项constant_pool_count

    版本声明后,是一个2个字节的无符号数u2用于标志常量池容量,此处0x0040,等于10进制下的64,表明常量池中有63项常量。

    (此处有个小设计,容量计数是从1开始而不是从0开始,故64-1=63)

    常量池表constant_pool

    接着就到了常量池表cp_info。此处常量池表就是之前文章Java虚拟机—堆、栈、运行时数据区中提到的,方法区中的运行时常量池。

    5.1运行时常量池
    运行时常量池(Runtime Constant Pool)是.class文件中每一个类或接口的常量池表(constant pool table)的运行时表示形式,属于方法区的一部分。每一个运行时常量池都在Java虚拟机的方法区中分配,在加载类和接口道虚拟机后,就创建对应的运行时常量池。常量池的作用是:
    存放编译器生成的各种字面量和符号引用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址之中。
    字面量(Literal),通俗理解就是Java中的常量,如文本字符串、声明为final的常量值等。
    符号引用(Symbolic References)则是属于编译原理中的概念,包括了下面三类常量:
    1.类和接口的全限定名
    2.字段的名称和描述符
    3.方法的名称和描述符

    image

    常量池可以理解为class文件中的资源仓库,它是class文件结构中与其他项目关联最多的数据类型,也是占用class空间最大的一个数据项。

    因为常量池中常量的数量不是固定的,所以需要2字节的无符号u2(constant_pool_count)代表常量池容量计数值(此处有个小设计,容量计数是从1开始而不是从0开始)。

    常量池中的每一项常量都是一个表。每个常量项表中第一位是一个u1类型的标志位,用于标志常量的类型,具体各个常量表如下图所示(目前有14种类型的常量,表中只列了11项):

    图片引用自:http://www.sohu.com/a/131458551_504186

    到此,我们来看一下用javap -v ConstantPool.class反编译一下.class文件来看看字节码的组成情况:

    Classfile xxx/.../ConstantPool.class
      Last modified 2018年9月20日; size 1063 bytes
      MD5 checksum 024d748f4dc1776164f6c3e8e19cf95b
      Compiled from "ConstantPool.java"
    public class JustCoding.Practise.ConstantPool
      minor version: 0
      major version: 52
      flags: (0x0021) ACC_PUBLIC, ACC_SUPER
      this_class: #14                         // JustCoding/Practise/ConstantPool
      super_class: #15                        // java/lang/Object
      interfaces: 0, fields: 2, methods: 4, attributes: 1
    Constant pool:
       #1 = Methodref          #15.#39        // java/lang/Object."<init>":()V
       #2 = Fieldref           #14.#40        // JustCoding/Practise/ConstantPool.VERSION:I
       #3 = String             #41            // Method
       #4 = Class              #42            // java/lang/StringBuilder
       #5 = Methodref          #4.#39         // java/lang/StringBuilder."<init>":()V
       #6 = Methodref          #4.#43         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       #7 = Methodref          #4.#44         // java/lang/StringBuilder.toString:()Ljava/lang/String;
       #8 = Fieldref           #45.#46        // java/lang/System.out:Ljava/io/PrintStream;
       #9 = String             #47            // 合并后的字符串:
      #10 = Methodref          #48.#49        // java/io/PrintStream.println:(Ljava/lang/String;)V
      #11 = Fieldref           #14.#50        // JustCoding/Practise/ConstantPool.a:Ljava/lang/String;
      #12 = Methodref          #14.#51        // JustCoding/Practise/ConstantPool.test1:(Ljava/lang/String;)V
      #13 = String             #52            // Class
      #14 = Class              #53            // JustCoding/Practise/ConstantPool
      #15 = Class              #54            // java/lang/Object
      #16 = Utf8               a
      #17 = Utf8               Ljava/lang/String;
      #18 = Utf8               VERSION
      #19 = Utf8               I
      #20 = Utf8               <init>
      #21 = Utf8               ()V
      #22 = Utf8               Code
      #23 = Utf8               LineNumberTable
      #24 = Utf8               LocalVariableTable
      #25 = Utf8               this
      #26 = Utf8               LJustCoding/Practise/ConstantPool;
      #27 = Utf8               test1
      #28 = Utf8               (Ljava/lang/String;)V
      #29 = Utf8               s
      #30 = Utf8               b
      #31 = Utf8               c
      #32 = Utf8               main
      #33 = Utf8               ([Ljava/lang/String;)V
      #34 = Utf8               args
      #35 = Utf8               [Ljava/lang/String;
      #36 = Utf8               <clinit>
      #37 = Utf8               SourceFile
      #38 = Utf8               ConstantPool.java
      #39 = NameAndType        #20:#21        // "<init>":()V
      #40 = NameAndType        #18:#19        // VERSION:I
      #41 = Utf8               Method
      #42 = Utf8               java/lang/StringBuilder
      #43 = NameAndType        #55:#56        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      #44 = NameAndType        #57:#58        // toString:()Ljava/lang/String;
      #45 = Class              #59            // java/lang/System
      #46 = NameAndType        #60:#61        // out:Ljava/io/PrintStream;
      #47 = Utf8               合并后的字符串:
      #48 = Class              #62            // java/io/PrintStream
      #49 = NameAndType        #63:#28        // println:(Ljava/lang/String;)V
      #50 = NameAndType        #16:#17        // a:Ljava/lang/String;
      #51 = NameAndType        #27:#28        // test1:(Ljava/lang/String;)V
      #52 = Utf8               Class
      #53 = Utf8               JustCoding/Practise/ConstantPool
      #54 = Utf8               java/lang/Object
      #55 = Utf8               append
      #56 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
      #57 = Utf8               toString
      #58 = Utf8               ()Ljava/lang/String;
      #59 = Utf8               java/lang/System
      #60 = Utf8               out
      #61 = Utf8               Ljava/io/PrintStream;
      #62 = Utf8               java/io/PrintStream
      #63 = Utf8               println
    {
      public int VERSION;
        descriptor: I
        flags: (0x0001) ACC_PUBLIC
    
      public JustCoding.Practise.ConstantPool();
        descriptor: ()V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: bipush        100
             7: putfield      #2                  // Field VERSION:I
            10: return
          LineNumberTable:
            line 3: 0
            line 7: 4
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      11     0  this   LJustCoding/Practise/ConstantPool;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=1, locals=1, args_size=1
             0: getstatic     #11                 // Field a:Ljava/lang/String;
             3: invokestatic  #12                 // Method test1:(Ljava/lang/String;)V
             6: return
          LineNumberTable:
            line 15: 0
            line 16: 6
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       7     0  args   [Ljava/lang/String;
    
      static {};
        descriptor: ()V
        flags: (0x0008) ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: ldc           #13                 // String Class
             2: putstatic     #11                 // Field a:Ljava/lang/String;
             5: return
          LineNumberTable:
            line 5: 0
    }
    SourceFile: "ConstantPool.java"
    

    可以看到,minor version: 0 major version: 52;Constant pool共有63项。和我们之前看16进制码时是一一对应的。

    访问标志access_flags

    在常量池表后面的两个字节代表访问标志,用于标志类或接口层次的访问信息。如:这个Class文件是类还是接口?是否是public?是否为抽象的abstract?是否为final的等。

    类索引this_class、父类索引super_class和接口索引集合intefaces

    类索引用于确定此类的全限定名称:JustCoding/Practise/ConstantPool,父类索引super_class用于确定这个类父类的全限定名:java/lang/Object。接口索引集合intefaces用来描述这个类实现了哪些接口。

    类索引this_class、父类索引super_class都是一个u2类型的数据,接口索引集合包含一个u2类型的接口计数项intefaces_count和若干个u2类型的数据集合。

    字段表集合fields_count+field_info

    字段表集合用于描述接口或类中声明的变量。字段filed包括类变量、实例变量,但不包括方法内部声明的局部变量。字段表结构如下:

    image

    字段表集合中第一项是access_flags,需要注意的是,这里的access_flags和之前类中的access_flags类似,是一个u2类型的数据,表示字段访问标记,可以设置9个标记位用于标记字段是否为:public,private,protected,static,final,volatile,transient,enum,是否由编译器自动产生。

    然后是name_index和descriptor_index,他们分别代表字段的简单名称和字段OR方法的描述符。方法表集合用于存储此类或接口中包含的方法,表结构和字段表类似。简单名称是指没有类型修饰、没有参数修饰的字段OR方法名称。简单名称很好理解,在例子中有:a ,VERSION ,test1。描述符descriptor则稍微麻烦点,描述符的作用是用来描述字段的数据类型、方法参数列表和返回值。例子中描述符有:I , ()V,([Ljava/lang/String;)V这几个。

    最后是属性表集合attributes_count+attribute_info,用于记录一些属性。

    方法表集合methods_count+method_info

    和字段表集合类似,此处需要注意的是,通过访问标志accessflags、名称索引nameindex、描述符索引descriptorindex来定义了方法,方法的实际代码存放在属性表attribute_info中的“Code”属性中。

    属性表集合attributes_count+attribute_info

    属性表在前面已经出现了多次,在class文件、字段表、方法表中都可以包含自己的属性表集合用于描述自己特定的属性。class文件中其他的数据项对顺序、长度和内容要求十分严格,而对属性表则相对宽松,不再要求属性表具有严格顺序,且只要不和已有属性重名,即可向属性表中写入自己定义的属性。JVM运行时会忽略掉它所不认识的属性。

    Java虚拟机规范(Java8)中预定义了23种属性,按照不同分类,大致可分为3类:

    image image

    熟悉了这些属性,学习了class文件结构,这时再用javap -v xxx.class命令来反编译一下字节码,看上去就清晰多了。所以下一篇文章我们就来对着反编译后的.class文件来继续学习和讲解JVM字节码指令,毕竟JVM指令才是整个JVM的核心。

    相关文章

      网友评论

        本文标题:Java虚拟机—Class文件结构

        本文链接:https://www.haomeiwen.com/subject/rpzcdhtx.html