java虚拟机三之Class文件解读

1.写一个java文件,代码如下:

package classTest;

/**
 * Created by 
 * Date : 2018/7/25 18:20
 */
public class Main {
    public static final int m=0;
    public int inc(){
        return m+1;
    }
}

2.运行javac Main.java,生成Main.class文件。如何配置并运行JAVA环境自己百度去。

3.运行javap -verbose Main.class,注意这儿一定要后缀.class我之前按照文章不加.class,死活运行不起来。

运行的结果如下:这结果先不要管,后面慢慢解读,我们这儿叫这个图为了A图吧,方便后面解说

4.用WinHex工具打开Main.class,如下图所示:WinHex工具自己百度下载去,用此工具是为了将Main.class用十六进制打开。

我们这儿叫这个图为B图吧,方便后面解说

5.在开始解读class文件前,我们先了解一下基本用语:

十六进制转十进制:这个自己百度去。

偏移量:这个是为了我们后面解读class文件时,定位用的。比如B图中的第一行的CA,这个的偏移量就是00000000,就是用OffSet下面的值的最后一位的0替换顶上的0~F,再比如上面第一行的FE,它的偏移量就是00000001。

B图中上面的0~F的十六进制数,每个都代表一个字节,比如0下面的CA,它就是代表一个字节,1下面的FE也是代表一个字节。u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节。我们从偏移量00000000开始,u2就代表CAFE,u4就代表CAFEBABE。如果从偏移量00000117开始,U2就代表0001,u4就代表0001000E。

6.明白了上述的基本用语,下面我们开始解读。

class文件中是没有空格和空行的,它里面的数据都是紧凑的排列在一起的。

我们开始一个字节一个字节解读:

a)最前面的4个字节:魔数,就是class文件的标识,由最前面的4个字节表示,java的魔数就是CAFEBABE(咖啡宝贝),就是说当虚拟机解读到这儿时,它能知道这个是java的class文件

b)紧接着4个字节:java的次版本号与主版本号,前两个字节表示次版本号,后两个字节表示主版本号。最主要就是主版本号,后两个字节0033转换成十进制数为51,意思是java1.7版本,下面列出版本号对应的字节

c)后面跟着的就是常量池了,我们这儿列出现14种常量池的结构

很多内容,不要着急,我们一步一步来。

常量池是从偏移量00000008开始,前面u2两个字节0013表示常量池的数量,0013转十字制是19,也就是说常量池的数量是18个,为什么是18个,而不是19个,因为第0个是空出来的,常量池都会被引用的,这个第0个是满足有些数据不引用常量池的情况。看A图可以看出来具体是哪18个常量。就是#1,#2,#3.。。。

因为有18个常量,

我们看第一个常量,看上表可知,常量池的第一个结构都是tag项目,类型都为u1字节,B图紧跟着的u1字节(编移量:0000000A)为0A(十进制:10),根据上表我们看值为10的常量为“CONSTANT_Methodref_info”,因此常量池的第一个常量为“方法引用常量”。我们再看上表中此常量的对应的剩余结构“index(u2,看描述,这个指向Class_info) index(u2,看描述,这个指向NameAndType)”,其中index是代表索引的意思,我们来看一下这两个字节分别是0003(十进制:3)  0010(十进制:16),也就是说这个“CONSTANT_Methodref_info”它又指向第3个常量和第16个常量。通过A图我们可以提前看到第3个常量为#3=class #18 //java/lang/Object,这个代表指向第18个常量(java/lang/Object对象) ,看到第16个常量为#16=NameAndType #8:#9 //"<init>:()V。这个意思就是第一个常量“CONSTANT_Methodref_info”,它指向的是java.lang.Object对象的 void init()方法。

这儿我解说一下描述符的含义,就像前面的<init>:()V。<init>代表方法名叫init,()代表方法的参数,因为这个参数为空,如果有参数的话()里面也是有描述符的,后面会举例。V则代表返回的类型。

还有基本类型的描述符如下:

还有数组类型的描述,一维数组用[表示,二维数组用[[表示,比如java.lang.String[][]描述为[[Ljava/lang/String,int[]描述为[I。

最后描述符再举个例子(有参数的):int indexOf(int a,char b,char[] c)这个方法描述符为(IC[C)I,方法名是在另外一个地方描述的,等到后面解说会看到这个示例。

常量池第一个常量指向的是java.lang.Object对象的 void init()方法,我们再看看第二个常量(偏移量:0000000F),tag=07(十进制:7,为CONSTANT_Class_info),再看它的结构,只有一个index(u2字节)=0011(十:17),看A图,#2 = Class              #17            //  classTest/Main,说明第二个常量指向的是classTest/Main类。

再看第三个常量:tag=07(7),index=0012(18),看A图, #3 = Class              #18            //  java/lang/Object,说明第三个常量指向的是java/lang/Object类

再看第四个常量:tag=01(1),它的结构是length(u2)和bytes(u1),length=0001(十进制:1,看描述,说明后面用一个字节) ,因为只用一个字节,所以后面的结构bytes=6D,注意,这个bytes看描述(长度为length的UTF-8编码的字符串),所以将这个6D转换为ACSII码为m,再看A图, #4 = Utf8               m,看结果确实为m。

再后面常量就不一一说明了,小伙伴们可以将所有的常量都解读出来。常量池的常量一直要解读到偏移量为000000A7结束。

d)常量池后面,跟着的就是类的索引信息了。

类的索引信息的结构分别为:访问标志(u2),类索引(u2),父类索引(u2),只能继承一个,所以只有一个u2数据)和接口索引(有几个接口,就有几个u2的数据)。

访问标志:看下图,这四个数字,如果有符合条件的,则为对应的值,如果不符合,则为0。

举例说明:public final class A(){}这个类,它的访问标志就为0011,因为有public和final符合条件,所以第3个数字和第4个数字取对应的值,另外两个数字则取0。

我们看B图的访问标志(偏移量:000000A8)=0021,说明这个类是public类型的,并且是jdk1.0.2之后编译的。

再看类索引=0002(十:2),看A图#2,指向的是Main类。

再看父类索引=0003(十:3),看A图#3,指向的是Object类。

再看接口索引=0000,因为没有实现接口,所以这儿为0。

e)类索引信息后跟着的就是字段表信息

这块区域是描述类的变量和实例的变量,但不描述方法中的局部变量。

字段表的结构:

第一个结构是字段数量

fields_count=0001(1),代表只有1个字段

第二个结构是访问标志,这个和类的访问标志类似。

access_flags=0019,代表public static final修饰的字段,9是1和8相加

name_index=0004(4),看A图#4,指向m,代表字段名为m

descriptor_index=0005,看A图#5,指向I,代表类型为int

attributes_count=0001,代表只有一个属性值,关于属性值的结构,后面会有详细介绍

attributes=0006,看A图#6,指向ConstantValue常量,关于ConstantValue属性值的结构为:

attribute_name_index就是attributes=0006

attribute_length=00000002,这个ConstantValue的数值必须固定为2

constantvalue_index=0007(7),看A图#7,代表值为0

因此,综上所述,这个字段所表达的意思就是public static final int m=0;

f)类的方法信息

这块区域描述的是类的方法信息集合。

方法表的结构:

第一个结构是方法的数量

methods_count=0002(偏移量000000C2,十进制2),代表这个class类有两个方法

第二结构是访问标志,访问标志结构如下:

access_flags(u2)=0001,代表为第一个方法为public的方法

name_index(u2)=0008(8), A图#8 = Utf8               <init>,代表方法名为init

descriptor_index(u2)=0009(9),A图 #9 = Utf8               ()V,代表无参返回类型为void的方法

attributes_count(u2)=0001,代表有一个属性

在这儿,我们通过class的解读可以得出,第一个方法为public void init()方法

attributes=000A,A图 #10 = Utf8               Code,代表属性指向Code,Code的结构图:

attribute_name_index对应attributes=000A

attribute_length(u4)=0000001D(29),这个代表第一个方法的属性值在class中的位置从当前位置一直往后的29个字节,也就是从偏移量000000D2~000000ED,这一块区域都是描述第一个方法的属性值

max_stack(u2)=0001(1),代表第一个方法的深度为1

max_locals(u2)=0001(1),代表了方法中变量表所需的存储空间,单位为Slot,这儿表示第一个方法的变量存储空间为1Slot

code_length(u4)=00000005(5),代表后面有5个字节执行方法

code=2A B7 00 01 B1,这儿代表的是字节码指令的执行流程,每个字节都有对应的字节码指令,其实换句话说,这儿就是方法的执行流程。更详细的我们第4章会详解,这儿只是初步了解一下

2A:aload_0 将第一个引用类型本地变量推送至栈顶

B7:invokespecial 调用超类构造方法,实例化初始化方法,私有方法

00:nop 什么都不做

01:aconst_null 将null推送到栈顶

B1:return 从当前方法返回void

以上就是五个字节代表的字节码指令,我们看A图后面的内容,这个方法就是去掉了无效的字节码指令所执行的指令顺序

我们继续,exception_table_length(u2)=0000,代表没有异常

exception_table:因为我们这儿没有异常,所以没有这块字节,但是如果有的话,我们还要按照异常表结构来解读:

attributes_count(u2)=0001(1),这个代表属性表中还包含1个属性表

attributes=000B(11),看A图#11 = Utf8               LineNumberTable,代表这个包含的属性表为LineNumberTable

我们看下LineNumberTable属性表的结构:

attribute_name_index对应attributes=000B

attribute_length(u4)=00000006(6),代表长度为6个字节

line_number_table_length(u2)=0001,代表只有1个LineNumberTable属性

line_number_table=00000007,前面两个字节代表字节码的行号,我们虽然用十六进制工具解析将class文件解析出来,但实际上,字节码是没有空格和换行的,都挤在第一行的(这个是我自己猜的,要不然不能解释为什么字节码行号在第0行)。后面两个字节代表java源码的行号,是在代码中的第7行,因为每一个方法为父类的init()方法,所以第7行指的是public class Main这一行。

到这儿第一个方法也就解读完了,这个方法最后的字节偏移量正好像之前说的这个方法属性占领29个字节,偏移量范围也正好是000000D2~000000ED。

因为有两个方法,紧接着是第二个方法的解读,解读和第一个方法很相似

access_flags(u2)=0001,代表为第二个方法为public的方法

name_index(u2)=000C(12), A图 #12 = Utf8               inc,代表方法名为inc

descriptor_index(u2)=000D(13),A图 #13 = Utf8               ()I,代表无参返回类型为int的方法

attributes_count(u2)=0001,代表有一个属性

在这儿,我们通过class的解读可以得出,第二个方法为public int inc()方法

attributes=000A,A图 #10 = Utf8               Code,代表属性指向Code,Code的结构第一个方法已经做了说明

attribute_name_index对应attributes=000A

attribute_length(u4)=0000001A(26),这个代表第二个方法的属性值偏移量为000000FC~00000116,这一块区域都是描述第二个方法的属性值

max_stack(u2)=0001(1),代表第二个方法的深度为1

max_locals(u2)=0001(1),代表了方法中变量表所需的存储空间,单位为Slot,这儿表示第二个方法的变量存储空间为1Slot

code_length(u4)=00000002(2),代表后面有2个字节执行方法

code=04 AC

04:iconst_1 将int型1推送到栈顶

AC:ireturn 从当前方法返回int

我们也可以看A图验证一下第二个方法字节码执行指令是否正确

exception_table_length(u2)=0000

exception_table:因为我们这儿没有异常,所以没有这块字节

attributes_count(u2)=0001(1),这个代表属性表中还包含1个属性表

attributes=000B(11),看A图#11 = Utf8               LineNumberTable,代表这个包含的属性表为LineNumberTable

attribute_name_index对应attributes=000B

attribute_length(u4)=00000006(6),代表长度为6个字节

line_number_table_length(u2)=0001,代表只有1个LineNumberTable属性

line_number_table=0000000A,前面两个字节代表字节码的行号,我们虽然用十六进制工具解析将class文件解析出来,但实际上,字节码是没有空格和换行的,都挤在第一行的(这个是我自己猜的,要不然不能解释为什么字节码行号在第0行)。后面两个字节代表java源码的行号,是在代码中的第10行

g)最后一个就是类的属性信息

这一块描述的是类的其它属性信息,比如源文件等信息,它的结构如下:

attributes_count(u2)=0001,代表只有一个属性

attribute_name_index=000E(14),看A图 #14 = Utf8               SourceFile,代表指向一个源文件

attribute_name_index对应的是上面的attribute_name_index=000E

attribute_length(u4)=00000002,代表有两个字节

sourcefile_index(u2)=000F(15),看A图#15 = Utf8               Main.java,代表源文件指向的是Main.java

以上就是对class文件的所有解读了,一个字节都没落下。有兴趣的小伙伴们可以自己建个java类,自己试着解读一下。

我们总结一下,这个class文件中包含了哪些信息?

1.java的class文件的标识,java的版本号

2.java的常量池

3.java的类索引,继承关系,类的修饰符

4.类的字段与类实例的字段描述

5.类的方法描述

6.类的其它属性描述

可以说会解读class,即使不看java类,应该自己也能写出这个java类的概况信息了。

最后再把所有的属性表结构列出:

对应的所有属性集合:

exception属性表结构:

猜你喜欢

转载自blog.csdn.net/wangzx19851228/article/details/81222583