欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java虚拟机的平台无关性

程序员文章站 2022-07-13 14:10:48
...
我们知道,java 语言一次编写,到处运行,那它是如何做到的了?早在java语言设计之初,设计者就考虑到了这方面的事,他们将java规范拆分为java语言规范和java虚拟机规范,而虚拟机只和class文件进行绑定,所以不管是java语言,还是c语言,只要有编译器把*.java或*.c文件编译成*.class文件即可,然后class文件运行在 java虚拟机上就可以运行了.

那下面说下 class 的文件结构

任何一个class文件都对应唯一的一个类或接口,但是类或接口不一定要定义在 class 文件中. Class 文件是一组以 8 字节为基础单位的二进制流,各个数据项严格按照顺序紧密排列,中间没有任何分割符。如果要存储超过 8 字节的数据的话,那么就按照高字节在前,低字节在后进行存储。

Class 文件结构类似于 C 语言中的结构体,它只包含两种数据类型,无符号数和表,无符号数u1、u2、u4、u8 分别代表1字节、2字节、4字节和8字节,表是复合数据类型,由无符号数和表组成。所以从本质上来看,class 文件本质上是一张表。

无符号数多用于描述索引引用、数值、UTF-8编码的字符串,而表多以 "_info" 结尾,用于描述带有层级关系的数据。

有小伙伴会问如何存储数组类型了?

class 文件对于相同数据类型结构的存储,会在前面放一个长度计数器,后跟该数据类型(ps: 这个跟对象头很像哦)

下面正式介绍 class 文件格式.

使用 javap -verbose xxx.class

xhdeMacBook-Pro:fadp xh$ javap -verbose /Users/xh/workspace/jvm-read/target/classes/com/hanlin/fadp/StringGC.class
Classfile /Users/xh/workspace/jvm-read/target/classes/com/hanlin/fadp/StringGC.class
  Last modified 2020-2-5; size 720 bytes
  MD5 checksum 8c8a2d157d5cb8fe7ea8543bef59ed02
  Compiled from "StringGC.java"
public class com.hanlin.fadp.StringGC
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#27         // java/lang/Object."<init>":()V
   #2 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #30            // abcccds
   #4 = Methodref          #31.#32        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Long               10000l
   #7 = Methodref          #33.#34        // java/lang/Thread.sleep:(J)V
   #8 = Class              #35            // com/hanlin/fadp/StringGC
   #9 = Class              #36            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/hanlin/fadp/StringGC;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               i
  #20 = Utf8               I
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               Exceptions
  #24 = Class              #37            // java/lang/InterruptedException
  #25 = Utf8               SourceFile
  #26 = Utf8               StringGC.java
  #27 = NameAndType        #10:#11        // "<init>":()V
  #28 = Class              #38            // java/lang/System
  #29 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;
  #30 = Utf8               abcccds
  #31 = Class              #41            // java/io/PrintStream
  #32 = NameAndType        #42:#43        // println:(Ljava/lang/String;)V
  #33 = Class              #44            // java/lang/Thread
  #34 = NameAndType        #45:#46        // sleep:(J)V
  #35 = Utf8               com/hanlin/fadp/StringGC
  #36 = Utf8               java/lang/Object
  #37 = Utf8               java/lang/InterruptedException
  #38 = Utf8               java/lang/System
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               println
  #43 = Utf8               (Ljava/lang/String;)V
  #44 = Utf8               java/lang/Thread
  #45 = Utf8               sleep
  #46 = Utf8               (J)V
{
  public com.hanlin.fadp.StringGC();
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hanlin/fadp/StringGC;

  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        10
         5: if_icmpge     28
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #3                  // String abcccds
        13: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: ldc2_w        #5                  // long 10000l
        19: invokestatic  #7                  // Method java/lang/Thread.sleep:(J)V
        22: iinc          1, 1
        25: goto          2
        28: return
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 16
        line 6: 22
        line 11: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      26     1     i   I
            0      29     0  args   [Ljava/lang/String;
    Exceptions:
      throws java.lang.InterruptedException
}
SourceFile: "StringGC.java"

下面是用文本计数器打开 class 文件的截图:


从该文件中我们是不是没有发现魔数 CAFEBABE 这4字节了?但是我在 class 源文件中确实看到了。


先不管这个了,继续往下看是两个版本号,minor version 和 major version.

minor version 小版本(u2)
major version 大版本(u2)



接下来是常量池

常量池肯定存放多个元素,必然是 len(u2) + 元素 这种形式. 值得注意的是,常量池的下标是从 1 开始的.
002f -> 47,我们用 javap 查看到,一共 46 个元素,下标从 1 开始的正好核上了. 那空出来的 0 号位有啥用了?不是越紧促越好吗?设计者将 0 号位用于实现指向常量池数据但需要表达不指向任何一个常量池项目的含义.
常量池主要存放字面量和符号引用,字面量例如:文本字符串,被 fianl 修饰的常量,基本数据类型的值.
符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符.

常量池中每一项都是一个表,jdk1.7 之前共有 12 个结构.



0a -> 10  Constant_Methodref_info 类中方法的符号引用.
tag
index1 指向声明方法的类或接口信息 Constant_Class_info
index2 指向名称及类型描述符索引项 Constant_NameAndType_info

08 -> 8 Constant_String_Info 表示 String 类型的常量
tag
index 指向字符串字面量的索引

Constant_Utf8_info 表示 Utf8 编码的字符串(基石).
tag
length
bytes

Constant_Fieldref_info 类中的字段
tag
index1 指向声明该字段的类或接口描述符 Constant_Class_info
index2 指向字段描述符的索引项 Constant_NameAndType_info

Constant_Class_info
tag
index 指向全限定名称的索引

Constant_Integer_info、Constant_Double_info、Constant_Float_info、Constant_Long_info 都是 tag + bytes

Constant_NameAndType_info
tag
index 指向该字段名称或方法的索引项
index 指向该字段名称或方法描述的索引项

Constant_InterfaceMethod_info
tag
index 指向声明方法的接口描述符 Constant_Class_info 的索引项
index 指向名称及类型描述符 Constant_NameAndType_info 的索引项

以一个为例进行分析:

Constant_Methodref_Info 有两个索引项,一个执行声明该字段或方法的表,另一个指向该字段或方法的名称及类型描述表.
我们重点看下名称及类型描述表,根据名称我们就可以得出这个表必然有两个索引项,一个是指向名称,一个是指向描述。关于方法和字段的描述后面会详细讲。




jdk1.7 新增的表(后面统一说明)

Constant_MethodHandle_info 方法句柄
Constant_MethodType_info 方法的描述
Constant_InvokeDynamic_info

接着后面是访问标志

在常量池结束后,紧接着是两个字节的访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是
接口,是否定义为 public 类型,是否定义为 abstract 类型,如果是的话,是否被声明为 final 等.
access_flags 中一共有 16 个标志位可以使用,当前只定义了其中 8 个,没有使用到的标志位一律为 0.
例如:ACC_PUBLIC、ACC_SUPER、ACC_FINAL、ACC_INTERFACE 等.

类索引 & 父类索引 & 接口索引

类索引(this_class) 和父类索引(super_class) 都是一个 u2 类型的数据,而接口索引集合(interfaces) 是一组 u2 类型的数据的集合,
Class 文件中由这三项数据来确定这个类的继承关系. 类索引确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名. Java 是不允许
多重继承,所以父类索引只有一个. 接口索引集合用来描述这个类实现了那些接口.

类索引和父类索引指向一个类型为 Constant_Class_Info 的类描述符常量,通过 Constant_Class_Info 类型的常量中的索引值可以找到定义
在 Constant_Utf8_info 类型的常量中的全限定名字符串.

对于接口索引集合,入口的第一项是 u2 类型的数据为接口计数器(最大为 65535,所以在看 JDK Proxy 的时候,会发现它有限制接口数组的长度
为 65535 哦,原因就在这里).