java class字节码文件解读

 

java程序员基本都知道java代码需要经过javac编译,编译后的文件我们称为class字节码文件,然后这个class文件就可以被jvm虚拟机加载然后运行程序,可以看出要真正理解java语言原理,我们得先了解下这个class文件。

 

就目前成熟阶段的计算机发展来说,我们知道大部分跟计算机有关的设计都逃不出一个概念叫:规则,没有规则不成方圆,所以了解class字节码文件也是一样的套路,得了解它的规则。


class字节码规则

首先我们知道,计算机中存储任何数据都得告诉有个长度或大小的概念,最小单位是位(bit)。很多开发语言都是通过类型的方式来描述变量数据的长度,比如int长度为32位(8个字节),long长度为64位。class文件中也一样,通过u1表示无符号类型1个字节(1个子节=8位),u2表示无符号类型2个子节(16位),u4表示4个子节,u8表示8个子节。下面是class字节码文件的规则(粗略过一遍就行了):

类型

名称

数量

u4

magic(魔数)

1

u2

minor_version(次版本)

1

u2

major_version(主版本)

1

u2

constant_pool_count(常量池数量)

1

cp_info

constant_pool(常量池信息)

constant_pool_count - 1

u2

access_flags(类访问标记)

1

u2

this_class(类索引)

1

u2

super_class(父类索引)

1

u2

interfaces_count(接口数量)

1

u2

interfaces(接口索引集合)

interfaces_count

u2

fields_count(类变量和成员变量数量)

1

field_info

fields(类变量和成员变量集合)

fields_count

u2

methods_count(类方法和成员方法数量)

1

method_info

methods(类方法和成员方法集合)

methods_count

u2

attributes_count(jvm自定义其它属性数量)

1

attribute_info

attributes(jvm自定义其它属性集合)

attributes_count

 

上图光看中文描述部分看是不是很多关键字挺熟悉,父类、属性、方法等等,其实class字节码规则就是通过二进制映射到java语法信息的方式形成一张大且层次结构复杂的映射表,有点类似java语言中的枚举,更准确说像计算机的类似ASCII编码表,只是class映射表是复合结构层次的,更复杂些。有了这个概念,我们通过下面的样例代码开始来分析class字节码的映射关系。


样例代码

在某个目录创建3个类,一个接口、一个父类、一个子类,代码分别如下:

父类ParentClass.java

 

public class ParentClass {




    private String name;


    public String getName() {
        return name;
    }


    public void setName(String name) {
        this.name = name;
    }
}

接口类ParentInterface.java

public interface ParentInterface {


    public String getMyName();


}

子类SubClass.java

public class SubClass extends ParentClass implements ParentInterface {




    private String mobile;


    public int test1(int i, int j) {
        int k = i + j;
        int sum = k + 10;
        return sum;
    }


    public boolean test2(String s1, String s2) {
        return s1.equals(s2);
    }


    public String getMyName() {
        return this.getName();
    }
}

 

 

编译样板代码

编译命令如下:

javac .\ParentClass.java .\ParentInterface.java .\SubClass.java

下面是SubClass.class字节码文件内容截图(笔者通过Sublime Text 2工具打开):

第一眼看到这个截图很多读者就开始郁闷了,这看起来不像是二进制啊,不是说class是二进制字节码文件吗?这确实不是二进制,而是被转为了十六进制,为啥要转为十六进制呢,原因很简单就是二进制需要4个(1010)数字而十六进制只需要1个字符(a)就可以了,这样可以使class文件变得更加紧凑些。

下面开始根据对照表以及编译后的十六进制字节码进行解读。

解读class映射表

解读class文件前要了解class文件中十六进制转字节的公式:

  • 1个十六进制 = 4位(bit)

  • 1个字节(byte)=8位(bit)

  • 1个字节(byte)= 8 / 4 = 2个十六进制

由此可得知:

  • u1类型代表1个字节=2个十六进制

  • u2类型代表2个字节=4个十六进制

  • u4类型代表4个字节=8个十六进制

  • u8类型代表8个字节=16个十六进制

解读魔数

类型

名称

数量

u4

magic(魔数)

1

 

从上表规则来看魔数排在class最开始,是一个u4类型也就是8个十六进制,对应到class字节码文件就是下面这8个十六进制:

魔数的作用是表示文件的类型,比如PNG图片文件、MP4可播放文件、PDF等文件基本都有自己的特殊的魔数,第三方解析器例如浏览器就可以通过魔数字符识别出文件的类型然后进行对应的逻辑解析处理,我们这里只要记住class文件的魔数数字就是cafe babe。

解读版本

类型

名称

数量

u2

minor_version(次版本)

1

u2

major_version(主版本)

1

 

魔数紧接着2个字节也就是对应的4个十六进制便是class次版本信息:

紧接着2个字节也就是对应的4个十六进制便是class主版本信息:

关于版本信息简单介绍如下:

编译器版本(不带-target)

十六进制

十进制

JDK1.6.0_01

0000 0032

50

JDK1.7.0

0000 0033

51

1.8.0_152

0000 0034

52

 

从解读的class字节码来看本次使用编译器版本十六进制为:0000 0034,对应是JDK1.8。

解读常量池

类型

名称

数量

u2

constant_pool_count(常量池数量)

1

cp_info

constant_pool(常量池信息)

constant_pool_count - 1

 

紧接着版本后的u2类型的2个字节也就是4个十六进制数为常量池数量,对应class文件如下:

常量池数量是指后面有多少项常量,我们将0020转为十进制结果为:32,说明后面有32-1=31项常量项,不同的class文件的常量项是不固定的,所以常量池前面会放个数量说明来控制解析常量项的结束位置,注意常量项是从1开始的而不是0,所以需要减去1。

常量池数量紧接着的是常量池项开始部分,常量池的作用主要是为保存类相关的各种信息,可以通过索引指向的方式来找到对应的确定值,例如父类全限定名,接口全限定名称等。

类型凡是带_info后缀的均为复合结构类型,也就是存在子规则,下面我们继续介绍常量池cp_info的子规则如下:

类型

标志

描述

CONSTANT_Utf8_info

1

UTF-8编码的字符串

CONSTANT_Integer_info

3

整形字面量

CONSTANT_Float_info

4

浮点型字面量

CONSTANT_Long_info

5

长整型字面量

CONSTANT_Double_info

6

双精度浮点型字面量

CONSTANT_Class_info

7

类或接口的符号引用

CONSTANT_String_info

8

字符串类型字面量

CONSTANT_Fieldref_info

9

字段符号引用

CONSTANT_Methodref_info

10

类中方法符号引用

CONSTANT_InterfaceMethodref_info

11

接口中方法符号引用

CONSTANT_NameAndType_info

12

字段或方法的部分符号引用

 

常量池子规则的类型大体可以分为字面量类型和符号引用类型,一共11种(1和3-12,注意没有2),常量池类型的复杂之处在于上表的每一种类型又有自己的不同子规则,在每一项类型前面放置一个u1类型的标志说明紧接着后面的常量项的类型,比如标志1代表CONSTANT_Utf8_info类型,标志3代表CONSTANT_Integer_info类型,以此类推。这样我们只要了解每一个常量类型的子规则,然后通过标志就能找到对应的解读规则了。下面我们通过一边分析class字节码文件一边介绍类型规则的方式来解读对应的每一个常量项。

解读第一个常量项

第一项常量

找到常量池数量紧接着的下一个常量标志位u1类型的十六进制为0a:

0a为转为十进制为:10,对应上表找到对应10的标志类型为:

CONSTANT_Methodref_info

10

类中方法符号引用

 

可以看到这是个方法符号引用,CONSTANT_Methodref_info类型子规则如下:

类型

描述

class_index

u2

指向声明方法的类描述符CONSTANT_Class_info的索引项

nameandtype_index

u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

紧接着u2类型的4个十六进制为0005:

 

转为十进制为5,代表指向常量池中第5项CONSTANT_Class_info类型常量,这点留给后面我们分析到第5项时再确认。然后我们继续紧接着u2类型的4个十六进制为0015:

 

转为十进制为21,代表指向常量池中第21项CONSTANT_NameAndType_info类型常量。第一项常量我们暂时分析到此。

第二项常量

紧接着我们分析第二项常量u1类型的标志十六进制值为0a也是CONSTANT_Methodref_info类型,0016转为十进制为:22代表第二项CONSTANT_Methodref_info类型指向第22项CONSTANT_Class_info类型常量,0017转为十进制为:23代表第二项CONSTANT_Methodref_info类型指向第23项CONSTANT_NameAndType_info类型常量,第二项常量暂时分析到此。

第三项常量

 

第三项常量标志十六进制为:0a对应的也是CONSTANT_Methodref_info类型常量,0004转为十进制为:4代表第三项CONSTANT_Methodref_info类型指向第4项CONSTANT_Class_info类型常量,0018转为十进制为:24代表第三项CONSTANT_Methodref_info类型指向第23项CONSTANT_NameAndType_info类型常量。

第四项常量

上面第三项CONSTANT_Methodref_info类型指向第4项CONSTANT_Class_info类型常量,我们现在刚好可以验证下该项是否是CONSTANT_Class_info类型。

 

第四项常量的u1类型的标志位为07,转为十进制为:7,对应常量类型正是CONSTANT_Class_info类型:

CONSTANT_Class_info

7

类或接口的符号引用

 

CONSTANT_Class_info类型子规则如下:

类型

描述

name_index

u2

指向常量池CONSTANT_Utf8_info类型常量

 

CONSTANT_Class_info类型只有一个u2类型指向CONSTANT_Utf8_info类型常量项的索引,这个索引代表了这个类或接口的全限定名,我们来对应下这个utf8字符串值:

0019转为十进制为25表示指向常量池第25项CONSTANT_Utf8_info类型常量项。下面我们就不一一分析完31项常量,这里给出每个常量类型的子规则,有了这个子规则以及前面的分析套路相信读者可以基本理解class字节码常量池这块的完整解读过程。

 

CONSTANT_Utf8_info类型子规则:

 

类型

描述

length

u1

UTF-8编码的字符串占用字节数

bytes

u1

长度为length的UTF-8编码的字符串

 

CONSTANT_Integer_info类型子规则:

 

类型

描述

bytes

u4

按照高位在前存储的int值

 

CONSTANT_Float_info类型子规则:

 

类型

描述

bytes

u4

按照高位在前存储的float值

 

CONSTANT_Long_info类型子规则:

 

类型

描述

bytes

u8

按照高位在前存储的long值

 

CONSTANT_Double_info类型子规则:

 

类型

描述

bytes

u8

按照高位在前存储的double值

 

CONSTANT_Class_info类型子规则:

 

类型

描述

index

u2

指向第index项CONSTANT_Utf8_info类型

 

CONSTANT_String_info类型子规则:

 

类型

描述

index

u2

指向第index项字符串字面量类型

 

CONSTANT_Fieldref_info类型子规则:

 

类型

描述

index

u2

指向声明字段的类或接口CONSTANT_Class_info索引项

index

u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

 

CONSTANT_Methodref_info类型子规则:

 

类型

描述

index

u2

指向声明方法的类CONSTANT_Class_info类型索引

index

u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

 

 

CONSTANT_InterfaceMethodref_info类型子规则:

 

类型

描述

index

u2

指向声明方法的接口CONSTANT_Class_info的索引项

index

u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

 

CONSTANT_NameAndType_info类型子规则:

 

类型

描述

index

u2

指向该字段或方法名称常量项的索引

index

u2

指向该字段或方法描述符常量项的索引

 

javap命令工具解读

通过十六进制挨个挨个分析只是为了理解原理,我们真的不需要这么去折腾自己,JDK提供有现成的一个命令行可以更好的帮我展示和分析好class字节码,下面我们执行下面命令展示:

javap -verbose SubClass.java

生成内容常量池部分截图如下:

 

可以看到从Constant Pool开始部分为常量池开始,下面我们拿第一项常量进行讲解:

#1 = Methodref          #5.#21         // com/lazy/test/classbyte/ParentClass."<init>":()V

1、#1代表是第一项常量 #2为第二项,以此类推。

2、Methodref代表对应常量为CONSTANT_Methodref_info类型。

3、#5.#21对应CONSTANT_Methodref_info类型子规则指向的两项常量池索引。

类型

描述

index

u2

指向声明方法的类CONSTANT_Class_info类型索引

index

u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

4、// com/lazy/test/classbyte/ParentClass."<init>":()V对应子规则指向索引最终解析的字面量信息注释。com/lazy/test/classbyte/ParentClass表示指向第#5项常量CONSTANT_Class_info类型解析的字面量描述。"<init>":()V表示指向第21项常量对应的CONSTANT_NameAndType_info类型的字面量描述。我们找到对应的第5项和第21项常量可以看到确实为对应的类型。

至此,常量池部分讲解就到此为止,有兴趣的读者可以自行去尝试按照上面给出的规则解析下看是否一一对应。

类访问标志解读

常量池部分解读完之后紧接着的u2类型的2个字节代表访问标志,这个标识的作用是标记当前class文件的类和接口层次信息,比如这个类是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类的话是否被声明final等,解读javap解析出来的如下图:

 

下表为访问标志描述:

访问名称

标志值

含义

ACC_PUBLIC

0x0001

是否为public类型

ACC_FINAL

0x0010

是否声明为final

ACC_SUPER

0x0020

是否允许使用invokespecial字节码指令,JDK1.2之后编译出来的均为true

ACC_INTERFACE

0x0200

标识为是一个接口

ACC_ABSTRACT

0x0400

是否为abstract

ACC_SYNTHETIC

0x1000

标识这个类并非由用户代码产生

ACC_ANNOTATION

0x2000

标识这是一个注解

ACC_ENUM

0x4000

标识这是一个枚举

 

类继承关系解读

类继承关系对应对应class规则表下面灰色这部分解读:

 

类型

名称

数量

u4

magic(魔数)

1

u2

minor_version(次版本)

1

u2

major_version(主版本)

1

u2

constant_pool_count(常量池数量)

1

cp_info

constant_pool(常量池信息)

constant_pool_count - 1

u2

access_flags(类访问标记)

1

u2

this_class(类索引)

1

u2

super_class(父类索引)

1

u2

interfaces_count(接口数量)

1

u2

interfaces(接口索引集合)

interfaces_count

......

 

this_class表示类索引位置,类索引用来找到这个类的全限定名,super_class表示父类索引,父类索引用来找到父类全限定名,this_class和super_class都是u2类型的数据,u2类型转为十进制后代表指向前面常量池中CONTANST_class_info类型的常量项索引。

interfaces是接口索引集合,它是数量为interfaces_count个u2类型集合,接口索引集合用来找到类实现的接口集合,集合每一项十进制值也是指向前面常量池中CONTANST_class_info类型的常量项索引。由于每个类的接口数量是不确定的,所以类似常量池一样在前面放置一个u2类型interfaces_count标志作为接口数量的标识符。

属性规则解读

属性规则的解读对应前面给出的class文件表规则灰色部分如下:

类型

名称

数量

u4

magic(魔数)

1

..............省略其它

u2

fields_count(类变量和成员变量数量)

1

field_info

fields(类变量和成员变量集合)

fields_count

u2

methods_count(类方法和成员方法数量)

1

method_info

methods(类方法和成员方法集合)

methods_count

u2

attributes_count(jvm自定义其它属性数量)

1

attribute_info

attributes(jvm自定义其它属性集合)

attributes_count

 

属性规则描述的是类属性、成员属性、接口属性等信息,由于不同类的属性也是不确定的,所以在前面先通过一个u2类型的filed_count描述属性的数量,紧接着我们看到又是一个带_info后缀的规则,这种规则为复合类型的规则,也就是类似常量池类型一样,存在子规则。

类型

名称

数量

标志名称

标志值

含义

u2

access_flags

1

ACC_PUBLIC

0x0001

是否public

ACC_PRIVATE

0x0002

是否private

ACC_PROTECTED

0x0004

是否protected

ACC_STATIC

0x0008

是否static

ACC_FINAL

0x0010

是否final

ACC_VOLATILE

0x0040

是否volatile

ACC_TRANSIENT

0x0080

是否transient

ACC_SYNTHETIC

0x1000

是否编译器创建

ACC_ENUM

0x4000

是否enum

u2

name_index

1

指向常量池引用代表属性名称

u2

descriptor_index

1

指向常量池引用代表属性描述

u2

attributes_count

1

其它属性数量,例如某个属性被修饰为final

attribute_info

其它属性集合

attributes_count

其它属性复合结构

 

关于属性信息的规则如上表,这里就不一一去翻译class字节码进行详细解读,读者有兴趣可以自行去尝试解析,说实在解析class字节码并没有什么高深的算法,只是一个结构规则的对照关系,复杂的是编译器的实现,需要将java语言转为class字节码的结构。

方法规则解读

方法表的规则跟属性的规则非常类似,如下表:

类型

名称

数量

标志名称

标志值

含义

u2

access_flags

1

ACC_PUBLIC

0x0001

是否public

ACC_PRIVATE

0x0002

是否private

ACC_PROTECTED

0x0004

是否protected

ACC_STATIC

0x0008

是否static

ACC_FINAL

0x0010

是否final

ACC_SYNCHRONIZED

0x0020

是否synchronized

ACC_BRIDGE

0x0040

是否桥架方法

ACC_VARARGS

0x0080

是否接受不定参数

ACC_NATIVE

0x0100

是否native

ACC_ABSTRACT

0x0400

是否abstract

ACC_STRICT

0x0800

是否strictfp

ACC_SYNTHETIC

0x1000

是否编译器创建

u2

name_index

1

指向常量池引用代表属性名称

u2

descriptor_index

1

指向常量池引用代表属性描述

u2

attributes_count

1

其它属性数量,例如某个方法被修饰为final

以及存放方法体的代码指令Code属性等

attribute_info

其它属性集合

attributes_count

其它属性复合结构

 

上表为方法的访问标志、名称索引、描述符索引、参数等信息的描述,方法里面的代码编译器将其编译为指令单独放到attribute_info一个叫Code的属性中,下面单独解读attribute_info。由于篇幅原因这里就不再继续介绍attribute_info结构规则,我们只需要大概知道class字节码是怎么回事,它的组成规则套路原理即可。在实际工作中我们也会经常对业务代码进行类似的映射设计,比如订单系统和物流系统的地址code对照表,客户编码对照表等都是映射表的一种设计思路。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值