Java技术能够一直保持非常好的向后兼容性,这点Class文件结构的稳定性功不可没。Java目前已经发展到JDK14,但是class文件结构的内容,绝大部分在JDK1.2时代就已经定义好了。虽然JDK1.2的内容比较古老,但是java发展经历了十余个大版本,但是每次基本上只是在原有结构基础上新增内容、扩充功能,并未对定义的内容做修改。
Class文件是一组以8位字节为基础单位的二进制流(实际上任何文件都是以二进制流形式存在的)。
工欲善其事,必先利其器。下面先介绍几个实用的工具。
Sublime/HexView:用来查看文件的16进制内容。
javap:JDK自带的反编译工具。能将.class字节码文件解析成可读的文件格式。
jclasslib:在javap的基础上提供了可视化界面,能够更加直观的查看字节码中的内容。它还分门别类的对类中的各个部分进行了整理,非常的人性化。同时,它还提供了Idea的插件,你可以从plugins中搜索到它。
演示代码:
package com.morris.jvm.bytecode;
public class ByteCode {
}
对应的字节码如下:
cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 0022 4c63 6f6d
2f6d 6f72 7269 732f 6a76 6d2f 6279 7465
636f 6465 2f42 7974 6543 6f64 653b 0100
0a53 6f75 7263 6546 696c 6501 000d 4279
7465 436f 6465 2e6a 6176 610c 0004 0005
0100 2063 6f6d 2f6d 6f72 7269 732f 6a76
6d2f 6279 7465 636f 6465 2f42 7974 6543
6f64 6501 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0021 0002 0003 0000 0000
0001 0001 0004 0005 0001 0006 0000 002f
0001 0001 0000 0005 2ab7 0001 b100 0000
0200 0700 0000 0600 0100 0000 0300 0800
0000 0c00 0100 0000 0500 0900 0a00 0000
0100 0b00 0000 0200 0c
各个数据项严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节(一个字节是由两位16进制数组成)、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由下面几个部分组成:
类型 | 名称 | 解释 | 数量 |
---|---|---|---|
u4 | magic | 魔数 | 1 |
u2 | minor_version | 次版本号 | 1 |
u2 | major_version | 主版本号 | 1 |
u2 | constant_pool_count | 常量池常量个数 | 1 |
constant_pool_info | constant_pool | 常量池 | constant_pool_count -1 |
u2 | access_flags | 访问标记 | 1 |
u2 | this_class | 类索引 | 1 |
u2 | superclass | 父类索引 | 1 |
u2 | interfaces_count | 接口索引数量 | 1 |
u2 | interfaces | 接口内容 | interfaces_count |
u2 | field_count | 字段表字段数量 | 1 |
field_info | fields | 字段表 | field_count |
u2 | methods_count | 方法表方法数量 | 1 |
method_info | methods | 方法表 | methods_count |
u2 | attributes_count | 属性表属性数量 | 1 |
attribute_info | attributes | 属性表 | attributes_count |
每个Class文件的头4个字节0xcafe babe
称为魔数(Magic Number),它的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。
紧接着魔数后的4个字节存储的是Class文件的版本号:第5、6个字节是次版本号(Minor Version),第7、8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布,主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
0x0000
表示次版本号为0,0x0034
表示主版本号为52,也就是JDK1.8。
下表列出了各个版本JDK的十六进制版本号信息:
JDK版本 | 次版本号 | 主版本号 | 十进制 |
---|---|---|---|
JDK1.1 | 0000 | 002D | 45 |
JDK1.2 | 0000 | 002E | 46 |
JDK1.3 | 0000 | 002F | 47 |
JDK1.4 | 0000 | 0030 | 48 |
JDK1.5 | 0000 | 0031 | 49 |
JDK1.6 | 0000 | 0032 | 50 |
JDK1.7 | 0000 | 0033 | 51 |
JDK1.8 | 0000 | 0034 | 52 |
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括了三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
0x0010
表示有15个常量(从1开始计数),15个常量的内容如下。
#1 = Methodref #3.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // com/morris/jvm/bytecode/ByteCode
#3 = Class #15 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/morris/jvm/bytecode/ByteCode;
#11 = Utf8 SourceFile
#12 = Utf8 ByteCode.java
#13 = NameAndType #4:#5 // "<init>":()V
#14 = Utf8 com/morris/jvm/bytecode/ByteCode
#15 = Utf8 java/lang/Object
对应的二进制内容如下:
0a 0003 000d // Methodref #3 #13
07 000e // Class # 14
07 000f // Class #15
01 0006 3c696e69743e // Utf8 <init>
01 0003 282956 // Utf8 ()V
01 0004 436f6465 // Utf8 Code
01 000f 4c696e654e756d6265725461626c65 // Utf8 LineNumberTable
01 0012 4c6f63616c5661726961626c655461626c65 // Utf8 LocalVariableTable
01 0004 74686973 // Utf8 this
01 0022 4c636f6d2f6d6f727269732f6a766d2f62797465636f64652f42797465436f64653b // Utf8 Lcom/morris/jvm/bytecode/ByteCode;
01 000a 536f7572636546696c65 // Utf8 SourceFile
01 000d 42797465436f64652e6a617661 // Utf8 ByteCode.java
0c 0004 0005 // NameAndType #4 #5
01 0020 636f6d2f6d6f727269732f6a766d2f62797465636f64652f42797465436f6465 // Utf8 com/morris/jvm/bytecode/ByteCode
01 0010 6a6176612f6c616e672f4f626a656374 // Utf8 java/lang/Object
用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
0x0021
表示ACC_SUPPER和ACC_PUBLIC标志位为1。
具体的标志位以及标志的含义见下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespedal字节码指令的新语义,invokespecial指令的语义在JDK1.2发生过改变,为了区别这条指令使用哪种语义,JDK1.2之后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句后的接口顺序从左到右排列在接口索引集合中。
0x0002
表示类的索引为#2。
0x0003
表示父类的索引为#3。
0x0000
表示类没有实现任何接口。
描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
0x0000
表示类的字段个数为0。
字段表的每个字段用一个名为field_info的表来表示,field_info表的数据结构如下所示:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flags | 1 | 字段访问标识 |
u2 | name_index | 1 | 字段名称索引项 |
u2 | descriptor_index | 1 | 字段描述符索引项 |
u2 | attributes_count | 1 | 属性表计数器 |
attribute_info | attributes | attribute_count | 属性表 |
描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”。
0x0001
表示方法的个数。
0x0001
表示方法的访问标识,ACC_PUBLIC。
0x0004
表示方法名的索引为#4。
0x0005
表示方法的描述符的索引为#5,也就是参数和返回值。
方法表中的每个方法都用一个method_info表示,其数据结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flags | 1 | 方法访问标识 |
u2 | name_index | 1 | 方法名称索引项 |
u2 | descriptor_index | 1 | 方法描述符索引项 |
u2 | attributes_count | 1 | 属性表计数器 |
attribute_info | attributes | attribute_count | 属性表 |
存储Class文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在Code属性表中。
0x0001
表示属性表的计数为1。
属性表的表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | attribute_info | attribute_length |
Code 属性的表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
0x0006
对应常量池中的索引为6,Code。
0x0000 0x002f
表示属性表的长度为2f。
0x0001
表示操作数栈的最大深度为1。
0x0001
表示本地变量表的槽slot数量为1。
0x0000 0005
表示字节码的长度为5。
0x2a
对应的字节码指令为aload_0
,将本地变量表的第一个值push到栈顶。
0xb7 0001
对应的字节码指令为invokespecial #1 <java/lang/Object.<init>>
,调用父类的构造方法。
0xb1
对应的字节码指令为return
,将栈顶的值返回。
0x0000
表示异常表的数量。
0x0002
表示属性表的长度。
属性表的表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | attribute_info | attribute_length |
0x0007
表示属性名的索引为#7,LineNumberTable。
0x00000006
表示属性长度为6个字节。
0x0001
表示LineNumberTable的长度为1。
LineNumberTable表的表结构如下图所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
接着跟着1个line_number_info类型的数据,下面是line_number_info表的结构,其包含了start_pc和line_number两个u2类型的数据项。前者是字节码行号,后者是Java源码行号。
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | line_number | 1 |
0x0000
:start_pc,字节码行号。0x0003
:line_number,Java源码行号。
接下来是局部变量表:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
0x0008
表示属性名的索引为#8,LocalVariableTable。0x0000000c
表示局部变量表的长度为12。
local_variable_info的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
0x0001
表示局部变量的个数。
0x0000
表示这个局部变量生命周期开始的字节码偏移量。
0x0005
表示这个局部变量作用范围的长度。
0x0009
表示这个局部变量的名称的索引为#9,也就是this。
0x000a
表示这个局部变量的描述符的索引为#10,对应为Lcom/morris/jvm/bytecode/ByteCode。
0x0000
表示这个局部变量在局部变量表中槽的位置。
最后一个就是属性表集合。
0x0001
表示属性表的长度为1。
接下来是SourceFile,SourceFile属性的表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_info | attribute_length |
0x000b
表示属性名称的索引为#11,也就是SourceFile。
0x00000002
表示属性长度为2。
0x000c
表示SourceFil 的常量池索引,即该字节码文件的源文件名称ByteCode.java。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://morris131.blog.csdn.net/article/details/107785178
内容来源于网络,如有侵权,请联系作者删除!