简介

所有的一切都是从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_SUPERConstant 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/

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

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