JVM详解之java-class文件的密码本

34次阅读

共计 6587 个字符,预计需要花费 17 分钟才能阅读完成。

简介

所有的一切都是从 javac 开始的。从那一刻开始,java 文件就从咱们肉眼可分辨的文本文件,变成了凉飕飕的二进制文件。

变成了二进制文件是不是意味着咱们无奈再深刻的去理解 java class 文件了呢?答案是否定的。

机器能够读,人为什么不能读?只有咱们把握 java class 文件的密码表,咱们能够把二进制转成十六进制,将十六进制和咱们的密码表进行比照,就能够轻松的解密了。

上面,让咱们开始这个激动人心的过程吧。

一个简略的 class

为了深刻了解 java class 的含意,咱们首先须要定义一个 class 类:

public class JavaClassUsage {

    private int age=18;

    public void inc(int number){this.age=this.age+ number;}
}

很简略的类,我想不会有比它更简略的类了。

在下面的类中,咱们定义了一个 age 字段和一个 inc 的办法。

接下来咱们应用 javac 来进行编译。

IDEA 有没有?间接关上编译后的 class 文件,你会看到什么?

没错,是反编译过去的 java 代码。然而这次咱们须要深刻理解的是 class 文件,于是咱们能够抉择 view->Show Bytecode:

当然,还是少不了最纯朴的 javap 命令:

 javap -verbose JavaClassUsage

比照会发现,其实 javap 展现的更清晰一些,咱们临时选用 javap 的后果。

编译的 class 文件有点长,我一度有点不想都列出来,然而又一想只有对能力讲述得更分明,还是贴在上面:

public class com.flydean.JavaClassUsage
  minor version: 0
  major version: 58
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/flydean/JavaClassUsage.age:I
   #8 = Class              #10            // com/flydean/JavaClassUsage
   #9 = NameAndType        #11:#12        // age:I
  #10 = Utf8               com/flydean/JavaClassUsage
  #11 = Utf8               age
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/flydean/JavaClassUsage;
  #18 = Utf8               inc
  #19 = Utf8               (I)V
  #20 = Utf8               number
  #21 = Utf8               SourceFile
  #22 = Utf8               JavaClassUsage.java
{public com.flydean.JavaClassUsage();
    descriptor: ()V
    flags: 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        18
         7: putfield      #7                  // Field age:I
        10: return
      LineNumberTable:
        line 7: 0
        line 9: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/flydean/JavaClassUsage;

  public void inc(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: aload_0
         2: getfield      #7                  // Field age:I
         5: iload_1
         6: iadd
         7: putfield      #7                  // Field age:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/flydean/JavaClassUsage;
            0      11     1 number   I
}
SourceFile: "JavaClassUsage.java"

ClassFile 的二进制文件

慢着,下面 javap 的后果如同并不是二进制文件!

对的,javap 是对二进制文件进行了解析,不便程序员浏览。如果你真的想直面最最底层的机器代码,就间接用反对 16 进制的文本编译器把编译好的 class 文件关上吧。

你筹备好了吗?

来吧,展现吧!

上图右边是 16 进制的 class 文件代码,左边是对 16 进制文件的适当解析。大家能够隐约的看到一点点相熟的内容。

是的,没错,你会读机器语言了!

class 文件的密码本

如果你要理解 class 文件的构造,你须要这个密码本。

如果你想解析 class 文件,你须要这个密码本。

学好这个密码本,走遍天下都 …… 没啥用!

上面就是密码本,也就是 classFile 的构造。

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];
}

其中 u2,u4 示意的是无符号的两个字节,无符号的 4 个字节。

java class 文件就是依照下面的格局排列下来的,依照这个格局,咱们能够本人实现一个反编译器(大家有趣味的话,能够自行钻研)。

咱们比照着下面的二进制文件一个一个的来了解。

magic

首先,class 文件的前 4 个字节叫做 magic word。

看一下十六进制的第一行的前 4 个字节:

CA FE BA BE 00 00 00 3A 00 17 0A 00 02 00 03 07 

0xCAFEBABE 就是 magic word。所有的 java class 文件都是以这 4 个字节结尾的。

来一杯咖啡吧,baby!

如许有诗意的画面。

version

这两个 version 要连着讲,一个是主版本号,一个是次版本号。

00 00 00 3A

比照一下下面的表格,咱们的主版本号是 3A=58,也就是咱们应用的是 JDK14 版本。

常量池

接下来是常量池。

首先是两个字节的 constant_pool_count。比照一下,constant_pool_count 的值是:

00 17

换算成十进制就是 23。也就是说常量池的大小是 23-1=22。

这里有两点要留神,第一点,常量池数组的 index 是从 1 开始到 constant_pool_count- 1 完结。

第二点,常量池数组的第 0 位是作为一个保留位,示意“不援用任何常量池我的项目”,为某些非凡的状况下应用。

接下来是不定长度的 cp_info:constant_pool[constant_pool_count-1] 常量池数组。

常量池数组中存了些什么货色呢?

字符串常量,类和接口名字,字段名,和其余一些在 class 中援用的常量。

具体的 constant_pool 中存储的常量类型有上面几种:

每个常量都是以一个 tag 结尾的。用来通知 JVM,这个到底是一个什么常量。

好了,咱们比照着来看一下。在 constant_pool_count 之后,咱们再取一部分 16 进制数据:

下面咱们讲到了 17 是常量池的个数,接下来就是常量数组。

0A 00 02 00 03

首先第一个字节是常量的 tag,0A=10,比照一下下面的表格,10 示意的是 CONSTANT_Methodref 办法援用。

CONSTANT_Methodref 又是一个构造体,咱们再看一下办法援用的定义:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

从下面的定义咱们能够看出,CONSTANT_Methodref 是由三局部组成的,第一局部是一个字节的 tag,也就是下面的 0A。

第二局部是 2 个字节的 class_index,示意的是类在常量池中的 index。

第三局部是 2 个字节的 name_and_type_index,示意的是办法的名字和类型在常量池中的 index。

先看 class_index,0002=2。

常量池的第一个元素咱们曾经找到了就是 CONSTANT_Methodref,第二个元素就是跟在 CONSTANT_Methodref 前面的局部,咱们看下是什么:

07 00 04

一样的解析步骤,07=7,查表,示意的是 CONSTANT_Class。

咱们再看下 CONSTANT_Class 的定义:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

能够看到 CONSTANT_Class 占用 3 个字节,第一个字节是 tag,前面两个字节是 name 在常量池中的索引。

00 04 = 4,示意 name 在常量池中的索引是 4。

而后咱们就这样一路找上来,就失去了所有常量池中常量的信息。

这样找起来,眼睛都花了,有没有什么简略的方法呢?

当然有,就是下面的 javap -version,咱们再回顾一下输入后果中的常量池局部:

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/flydean/JavaClassUsage.age:I
   #8 = Class              #10            // com/flydean/JavaClassUsage
   #9 = NameAndType        #11:#12        // age:I
  #10 = Utf8               com/flydean/JavaClassUsage
  #11 = Utf8               age
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/flydean/JavaClassUsage;
  #18 = Utf8               inc
  #19 = Utf8               (I)V
  #20 = Utf8               number
  #21 = Utf8               SourceFile
  #22 = Utf8               JavaClassUsage.java

以第一行为例,间接通知你常量池中第一个 index 的类型是 Methodref,它的 classref 是 index=2,它的 NameAndType 是 index=3。

并且间接在前面展现出了具体的值。

描述符

且慢,在常量池中我如同看到了一些不一样的货色,这些 I,L 是什么货色?

这些叫做字段描述符:

上图是他们的各项含意。除了 8 大根底类型,还有 2 个援用类型,别离是对象的实例,和数组。

access_flags

常量池前面就是 access_flags:拜访描述符,示意的是这个 class 或者接口的拜访权限。

先上密码表:

再找一下咱们 16 进制的 access_flag:

没错,就是 00 21。参照下面的表格,如同没有 21,然而别怕:

21 是 ACC_PUBLIC 和 ACC_SUPER 的并集。示意它有两个 access 权限。

this_class 和 super_class

接下来是 this class 和 super class 的名字,他们都是对常量池的援用。

00 08 00 02

this class 的常量池 index=8,super class 的常量池 index=2。

看一下 2 和 8 都代表什么:

   #2 = Class              #4             // java/lang/Object
   #8 = Class              #10            // com/flydean/JavaClassUsage

没错,JavaClassUsage 的父类是 Object。

大家晓得为什么 java 只能单继承了吗?因为 class 文件外面只有一个 u2 的地位,放不下了!

interfaces_count 和 interfaces[]

接下来就是接口的数目和接口的具体信息数组了。

00 00

咱们没有实现任何接口,所以 interfaces_count=0,这时候也就没有 interfaces[] 了。

fields_count 和 fields[]

而后是字段数目和字段具体的数组信息。

这里的字段包含类变量和实例变量。

每个字段信息也是一个构造体:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

字段的 access_flag 跟 class 的有点不一样:

这里咱们就不具体比照解释了,感兴趣的小伙伴能够自行体验。

methods_count 和 methods[]

接下来是办法信息。

method 构造体:

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

method 拜访权限标记:

attributes_count 和 attributes[]

attributes 被用在 ClassFile, field_info, method_info 和 Code_attribute 这些构造体中。

先看下 attributes 构造体的定义:

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

都有哪些 attributes, 这些 attributes 都用在什么中央呢?

其中有六个属性对于 Java 虚拟机正确解释类文件至关重要,他们是:
ConstantValue,Code,StackMapTable,BootstrapMethods,NestHost 和 NestMembers。

九个属性对于 Java 虚拟机正确解释类文件不是至关重要的,然而对于通过 Java SE Platform 的类库正确解释类文件是至关重要的,他们是:

Exceptions,InnerClasses,EnclosingMethod,Synthetic,Signature,SourceFile,LineNumberTable,LocalVariableTable,LocalVariableTypeTable。

其余 13 个属性,不是那么重要,然而蕴含无关类文件的元数据。

总结

最初留给大家一个问题,java class 中常量池的大小 constant_pool_count 是 2 个字节,两个字节能够示意 2 的 16 次方个常量。很显著曾经够大了。

然而,万一咱们写了超过 2 个字节大小的常量怎么办?欢送大家留言给我探讨。

本文链接:http://www.flydean.com/jvm-class-file-structure/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

正文完
 0