解读字节码之旅
接下来我们采用一个示例代码,编译成字节码文件进行解读解读
public class Demo{
private int num = 1;
public int add(){
num = num + 2;
return num;
}
}
我们将当前Demo类编译成字节码文件,并且使用notepad++和对应的插件打开翻译翻译,如下图
接下来我们把这些信息复制粘贴到一个text文档中,并在空格上进行,切割转为csv的格式
由于一些csv的特性,我们在新建到一个excel文档中,并将20替换成00
接下来开始解读上面这一坨字节码,我们根据下面这副图,进行解读看看
魔数:Class文件的标志、Magic Number(魔数)
================================
每个Class文件开头的4个字节的无符号整数称为魔数(Magic Number)
它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。
即:魔数是Class文件的标识符,并且魔数值固定为GxCAFEBABE。不会改变。
使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动
如果一个Class文件不以exCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
Error: A JNI error has occurred,please check your installation and try again
Exception in thread "main"java.lang.ClassFormatError: Incompatible magic value 1885430635 in classfile StringTest
根据前面的这些信息,那么我们的字节码文件进行标出信息
Class文件版本号
================================
紧接着魔数的4个字节存储的是 Class文件的版本号,同样也是4个字节
第5个和第6个字节所代表的含义就是编译的副版本号minor_version
而第7个和第8个字节就是编译的主版本号major_version
它们共同构成了class文件的格式版本号,譬如某个 Class 文件的主版本号为M,副版本号为 m,那么这个Class 文件的格式版本号就确定为M.m。
版本号和ava编译器的对应关系如下表:
- Java 的版本号是从45开始的,JDK 1.1之后的每个DK大版本发布主版本号向上加1
- 不同版本的Java编译器编译的Class文件对应的版本是不一样的。
目前,高版本的Java虚拟机可以执行由低版本编译器生成Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVMN会抛出java.lang.UnsupportedClassVersionError异常。
在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。
因此,需要我们在开发时,特别注意开发编译的JDK版本和生产环境中的JDK版本是否一致。
虚拟机JDK版本为1.k (k >= 2)时,对应的class文件格式版本号的范围为45.0 - 44+k.0(含两端)
。
我们也可以使用Binary Viewer插件打开Demo的字节码文件,可以查看到对应十六进制翻译过来数值
常量池:存放所有常量
================================
常量池是Class文件中内容最为丰富的区域之一
常量池对于Class文件中的字段和方法解析也有着至关重要的作用。随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石。
官方文档中也有提到在版本号之后,紧跟着的是常量池的数量,以及若任个常量池表项
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。
与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。
由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。
常量池表项中用于存放编译时期生成的各种字面量和符号引用
这部分内容将在类加载后进入方法区的运行时常量池中在放,在JDK7 以后吧字符串常量池放堆空间
常量池计数器(constant_poo1_count)
由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值
常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。
提示:即constant_pool_count=1,那么表示常量池中有0个常量项
那么Demo的字节码信息,我们可以看看它的常量池计数器是什么了
其值为0x0016,掐指一算,也就是22。需要注意的是,这实际上只有21项常量。索引为范围是1-21。
通常我们写代码时都是从O开始的,但是这里的常量池却是从1开始,因为它把第O项常量空出来了。
这是为了满足后面某些指?常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示
。
constant_pool [](常量池)
constant_pool是一种表结构,以1 ~constant_pool_count - 1为索引。表明了后面有多少个常量项
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。
针对于类的全类目,我们针对于Demo类,可能是com.atguigu/test/Demo
那么针对于类的全限定名,我们仅仅是把包名的"."替换成"/",并且使用;代表结束
那么针对于字段的名称、方法的名称,我们称呼为简单名称。指的是没有类型和参数修饰的方法或者字段名称,在Demo类里add方法的简单名称为add、num字段简单名称为num
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。
- 如方法java.lang. String toString()的描述符为()Ljava/lang/String;
- 如方法int abc(int[] x, int y)的描述符为([II)I
我们可以采用示例代码演示一下这些类型的描述
public class ArrayTest i
public static void main( String[ ] args) {
Object[] arr = new Object[10];
system.out.println(arr);
String[] arr1 = new String[10];
system.out.println( arr1);
long[][] arr2 = new long[10][];
system.out.printin(arr2);
}
}
//运行结果如下:
[Ljava.lang.Object;@1540e19d
[Ljava.lang.String;@677327b6
[[J@14ae5a5
虚拟机在加载Class文件时才会进行动态链接
,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息。因此这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的
。
当虚拟机运行时,需要从常量池中获得对声的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中
这里说明下符号引用和直接引用的区别与关联:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
符号引用与虚拟机实现的内存布局无关
,引用的目标并不一定已经加载到了内存中。- 直接引用:直接引用可以是
直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
直接引用是与虚拟机实现的内存布局相关的
,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定己经存在于内存之中
了。
在字节码中使用第1个字节作为类型标记,用于确定该项的格式
我们喜欢这个字节称为tag byte (标记字节、标签字节)
,具体什么类型看下图所示
接下来我们针对于Demo类的字节码文件,进行解读看看第一个类型标记是什么
我们可以看到第一个类型是u1、u2、u2,占据5个字节,那么对应的占位是如下图
接下来我们看仅接其后的第二个类型是什么?
我们可以看到第二个类型是u1、u2、u2,占据5个字节,那么对应的占位是如下图
接下来我们看仅接其后的第三个类型是什么?
我们可以看到第三个类型是u1、u2,占据3个字节,那么对应的占位是如下图
接下来我们看仅接其后的第四个类型也是07,那么对应的占位是如下图
接下来我们看仅接其后的第五个类型是什么?
这里字符串用两个字节来表示长度,那么对应的如下所示
我们可以看到第五个类型对应的占位是如下图
接下来我们看仅接其后的类型也是01,那么对应的占位是如下图
接下来我们看仅接其后的类型也是01,那么对应的占位是如下图
接下来我们看仅接其后的类型也是01,那么对应的占位是如下图
接下来我们看仅接其后的类型是01,那么对应的占位是如下图
接下来我们看仅接其后的类型是01,长度为of(十进制:15)那么对应的占位是如下图
接下来我们看仅接其后的类型是01,那么对应的占位是如下图
接下来我们看仅接其后的类型是01,那么对应的占位是如下图
接下来我们看仅接其后的类型是01,长度为18(十进制:24)那么对应的占位是如下图
接下来我们看仅接其后的类型是01,那么对应的占位是如下图
接下来我们看仅接其后的类型是01,那么对应的占位是如下图
接下来我们看仅接其后的类型是01,长度为0a(十进制:10)那么对应的占位是如下图
接下来我们看仅接其后的类型是01,那么对应的占位是如下图
接下来我们看仅接其后的类型是0c,那么对应的占位是如下图
接下来我们看仅接其后的类型是0c,那么对应的占位是如下图
接下来我们看仅接其后的类型是01,长度为16(十进制:22)那么对应的占位是如下图
接下来我们看仅接其后的类型是01,长度为10(十进制:16)那么对应的占位是如下图
针对于这些我们可以使用javap命令解析: javap -verbose Demo.class或jclasslib工具会更好
常量池总结:
这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag)
,代表当前这个常量项使用的是哪种表结构,即哪种常量类型。
在常量池列表中,CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式,来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。
这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么?
因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定
,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8编码,就可以知道其长度。
常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型后面的很多数据类型都会指向此处),也是占用class文件空间最大的数据项目之一。
常量池中为什么要包含这些内容?
Java代码在进行Javac编译的时候,并不像c和C++那样有“连接”这一步骤(翻译机器指令)
而是在虚拟机加载Class文付的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息
,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用
。
当虚拟机运行时,需要从常量池获得对应的符号引用再在类创建时或运行时解析、翻译到具体的内存地址之中
。关于类的创建和动态链接的内容,在虚拟机类加载过程时再进行详细讲解
访问标识
================================
在常量池后,紧跟着访问标记。该标记使用两个字节表示
用于识别一些类或者接口层次的访问信息
常见信息一般包括:
- 这个Class是类还是接口;
- 是否定义为 public类型;
- 是否定义为 abstract类型;
- 如果是类的话,是否被声明为 final。
具体还有其他的各种访问标记如下所示:
一般类的访问权限通常为ACC_ 开头的常量
,每一种类型的表示都是通过设置访问标记的32位中的特定位
来实现的。比如:若是public final
的类,则该标记为ACC_PUBLIC | ACC_FINAL
使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置使用这个
我们可以看看上面的Demo的字节码文件,是什么信息?
那么我们上面的表格里没有21,那么这是因为21是加起来后的数值,可以看看具体有哪些组成?
带有ACC_INTERFACE标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。
如果一个class文件被设置了ACC_INTERFACE
标志,那么同时也得设置ACC_ABSTRACT
标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或ACC_ENUM
标志。
如果没有设置ACC_INTERFACE
标志,那么这个class文件可以具有上表中 除ACC_ANNOTATTON外
的其他所有标志。当然,ACC_FINAL
和ACC_ABSTRACT
这类互斥的标志除外。这两个标志不得同时设置。
ACC_SUPER
标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义
针对Java虚拟机指令集的编译器都应当设置这个标志
。对于Java SE 8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本号是多少
Java虚拟机都认为每个class文件均设置了ACC_SUPER标志
ACC_SUPER
标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的ACC_SUPER
标志在由JDK 1.0.2之前的编译器所生成的access_flags中是没有确定含义
的,如果设置了该标志,那么oracle的Java虚拟机实现会将其忽略
ACC_SYNTHETIC
标志意味着该类或接口是由编译器生成的,而不是由源代码生成的
注解类型必须设置ACC_ANNOTATION
标志。如果设置了ACC_ANNOTATION
标志,那么也必须设置ACC_INTERFACE
标志、ACC_ENUM
标志表明该类或其父类为枚举类型。
类索引、父类索引、接口索引集合
================================
在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:
这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名
由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang .object 之外,所有的Java类都有父类。因此除了java.lang.0bject 外,所有Java类的父类索引都不为0。
- 接口索引集合就用来描述这个类实现了哪些接口
这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends语句)后的接口顺序从左到右排列在接口索引集合中
接下来我们看看Demo类的字节码文件里面是哪些信息?
this_class(类索引)
2字节无符号整数,指向常量池的索引。它提供了类的全限定名如:com/atguigu/java1/Demo
this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口。
super_class(父类索引)
2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。
如果我们没有继承任何类,其默认继承的是java/lang/0bject类。同时,由于Java不支持多继承,所以其父类只有一个,以及.superclass指向的父类不能是final
interfaces
指向常量池索引集合,它提供了一个符号引用到所有已实现的接口
由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(当然这里就必须是接口,而不是类)。
interfaces_count(接口计数器)
interfaces_count项的值表示当前类或接口的直接超接口数量。
interfaces [](接口索引集合)
interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,长度为 interfaces_count
每个成员interfaces[i]必须为CONSTANT_Class_info结构,其中 0 <= i <interfaces_count
在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口。
字段表集合(fields)
================================
用于描述接口或类中声明的变量
字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量
字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)
等。
注意事项:
字段表集合中不会列出从父类或者实现的接口中继承而来的字段
,但有可能列出原本Java代码之中不存在的字段
。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段
。
在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称
,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
fields_count(字段计数器)
fields_count的值表示当前class文件fields表的成员个数
。使用两个字节来表示
fields表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段
fields [](字段表)
每个成员都必须是一个fields_infc结构的数据项,用天表示当前类或接口中某个字段的完整描述
一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有要么没有
。
- 作用域( public、private、protected修饰符)>
- 是实例变量还是类变量(static修饰符)
- 可变性(final)
- 并发可见性(volatile修饰符,是否强制从主内存读写)
- 可否序列化(transient修饰符)
- 字段数据类型(基本数据类型、对象、数组)
- 字段名称
字段表作为一个表,同样也有它自己的结构,如下图:
字段表的访问标识
我们知道字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected)、 static修饰符、final修饰符、volatile修饰符
等等。
因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些:
字段名索引
根据字段名索引的值,查询常量池中的指定索引项即可
接下来我们分析一下Demo类里的对应字段标识是哪个?
指向的是常量池第5,那么对应的常量池十进制也是05,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的字节码是是什么
描述符索引
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
根据描述符规则,基本数据类型(byte,char ,double ,float ,int ,long,short , boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符L加对象的全限定名来表示,如下所示:
接下来我们分析一下Demo类里的对应描述符是哪个?
指向的是常量池第6,那么对应的常量池十进制也是73,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
属性计数器
若我们当前字段有属性的话,就会记录个数以及描述,我们分析一下Demo类有什么?00代表没有
属性表集合
一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。
属性个数存放在attribute_count中,属性具体内容存放在attributes数组中
以常量属性为例,结构为:
ConstantValue_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
说明:对于常量属性而言,attribute_ 1ength值恒为2
我们可以修改Dmeo类里的num值,将它修改为final修饰,并且查看对应的信息
接下来我们对该常量属性的结构进行分析看看,如下图所示
方法表集合(methods)
================================
指向常量池索引集合,它完整描述了每个方法的签名。
在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息
。比如方法的访问修饰符(public,private或protected),方法的返回值类型以及方法的参数信息等。
如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。
一方面methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法
。
一方面methods表有可能会出现由编译器自动添加的方法
,比较典型的便是
编译器产生的方法信息(比如:类(接口)初始化方法<clinit>()和实例初始化方法<init>())。
注意事项:
在Java语言中要重载(Overload)一个方法,除了要与原方法具有相同的简单名称
之外,还要求必须拥有一个与原方法不同的特征签名
。
特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合
,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载
我们看以下的示例代码就知道了,无法根据返回值来区分重载
public void method1(int num){
}
public int method1(int num){
}
但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存
如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件
也就是说尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法
,但是和ava语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法
,唯一的条件就是这些方法之间的返回值不能相同
methods_count(方法计数器)
methods_count的值表示当前class文件methods表的成员个数,使用两个字节来表示
methods表中每个成员都是一个method info结构
接下来我们分析一下Demo类里的对应是哪个?
methods [](方法表)
methods表中的每个成员都必须是一个method_info结构,具体可看下图所示:
method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法,一般用于表示当前类或接口中某个方法的完整描述。
如果某个method_info结构的access_flags
项既没有设置ACC_NATTV
标志也没有设置ACC_ABSTRACT
标志,那么该结构中也应包含实现这个方法所用的Java虚拟机指令
方法表访问标志
跟字段表一样,方法表的标志有部分相同,部分则不同,方法表的具体访问标志如下:
根据我们前面方法表的method_info结构,我们先来看第一个访问标志
该对应的是01,那么与我们方法表的具体访问标志是public
方法名索引
根据方法名索引的值,查询常量池中的指定索引项即可
根据我们前面方法表的method_info结构,我们先来看第二个方法名索引
指向的是常量池第7,那么对应的常量池的位置,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
描述符索引
描述符的作用是用来描述方法的参数列表(包括数量、类型以及顺序)和返回值
及代表无返回值的void类型都用一个大写字符来表示
根据我们前面方法表的method_info结构,我们先来看第三个描述符索引
指向的是常量池第8,那么对应的常量池的位置,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
方法属性计数器
根据我们前面方法表的method_info结构,我们先来看第四个属性计数器
方法属性表
属性表的每个项的值必须是attribute_info结构
属性表的结构比较灵活,各种不同的属性只要满足以下结构即可
方法属类型
方属性表实际上可以有很多类型,Java8里面定义了23种属性,下面这些是虚拟机中预定义的属性:
方法属性名索引
根据我们前面方法属性表的结构,我们来解读看看方法属性名索引
指向的是常量池第9,那么对应的常量池的位置,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
针对于Code属性,它代表的是存放方法体里面的代码。但是并非所有方法表都有Code属性
像接口或者抽象方法,他们没有具体的方法体因此也就不会有code属性
Code属性的机构图,如下图所示
可以看到Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。
Code的属性长度
根据我们前面Code属性的表的构,我们来解读看看Code的属性长度
指向的是38,那么对应的十进制是56,那么就是如下这些位置
Code的操作数栈深度的最大值
根据我们前面Code属性的表的构,我们来解读看看Code的操作数栈深度的最大值
Code的局部变量表所需的存续空间
根据我们前面Code属性的表的构,我们来解读看看Code的局部变量表所需的存续空间
Code的字节码指令长度
根据我们前面Code属性的表的构,我们来解读看看Code的字节码指令长度
指向的是0a,那么对应的十进制是10,那么就是如下这些位置
Code的异常表的长度
根据我们前面Code属性的表的构,我们来解读看看Code的异常表的长度
Code的属性的集合计数器
根据我们前面Code属性的表的构,我们来解读看看Code的属性的集合计数器
Code的属性名索引
根据我们前面Code属性的表的构,我们来解读看看Code的属性名索引
指向的是0a,那么对应的十进制是10,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
LineNumberTable属性
LineNumberTable属性是可选变长属性,位于Code结构的属性表
LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系
。
这个属性可以用来在调试的时候定位代码执行的行数
。
LineNumberTable属性表结构:
LineNumberTable_attribute {
u2 atribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
}line_number_table[line_number_table_length];
}
- start_pc,即字节码行号
- line_number,即Java源代码行号
在Code属性的属性表中LineNumberTable属性可以按照任意顺序出现
此外多个LineNumberTable属性可以共同表示一个行号在源文件中表示的内容
即 LineNumberTable 属性不需要与源文件的行一一对应。
根据我们前面LineNumberTable 属性结构,我们来解读看看属性长度
指向的是0a,那么对应的十进制是10,那么就是如下这些位置
针对于这十个,我们再分析分析前面LineNumberTable 属性结构的line_number长度
针对于这十个,我们再分析分析前面LineNumberTable 属性结构的字节码行号
针对于这十个,我们再分析分析前面LineNumberTable 属性结构的Java源代码行号
因为LineNumberTable 属性结构的line_number长度为2,所以就有二组,看图所示
如果我们使用软件查看Demo类的字节码分析的话,就可以看到下面所图所示的样子
接下来我们看code属性的第二个属性
指向的是0b,那么对应的十进制是11,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
LocalVariableTable 属性
LocalVariableTable是可选变长属性,位于Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息
在Code属性的属性表中,LocalVariableTable属性可以按照任意顺序出现
Code属性中的每个局部变量最多只能有一个LocalVariableTable属性
LocalVariableTable属性表结构:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
- start pc + length表示这个变量在字节码中的生命周期起始和结束的偏移位置(this生命周期从头0到结尾10
- index就是这个变量在局部变量表中的槽位(槽位可复用)
- name就是变量名称
- Descriptor表示局部变量类型描述
根据我们前面LineNumberTable 属性结构,我们来解读看看属性长度
针对于这LocalVariableTable,我们再分析分析属性结构的table_number长度
针对于这LocalVariableTable,我们再分析分析属性结构的字节码行号
针对于这LocalVariableTable,我们再分析分析属性结构的长度
针对于这LocalVariableTable,我们再分析分析属性结构的变量名称索引
指向的是0C,那么对应的十进制是12,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
针对于这LocalVariableTable,我们再分析分析属性结构的描述符索引
指向的是0C,那么对应的十进制是13,那么就是如下这些位置
我们可以使用Binary Viewer 软件查看对应的ASCII码是是什么
对应我们的Demo类的字节码文件分析可以看到如下图所示所示
属性表集合(attributes)
================================
方法表集合之后的属性表集合,指的是class文件所携带的辅助信息.
比如该 class文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者 RetentionPolicy.RUNTINE的注解。
这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解
此外,字段表、方法表都可以有自己的属性表(比如常量)。用于描述某些场景专有的信息。
属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序。
并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息
,但Java虚拟机运行时会忽略掉它不认识
的属性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。