字节码概念
字节码文件里有什么?
源代码经过编译器编译之后便会生成一个字节码文件,字节码文件是一种二进制的类文件,它的内容是JVM指令。
什么是字节码指令?
Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的操作码以及跟随其后的零至多个代表此操作所需参数的操作数所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
Class类的本质
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class文件是一组以8位字节位基础单位的二进制流。
Class文件格式
Class的结构没有任何的分隔符号,所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许被改变。
Class文件结构
Class文件的总体结构如下::
- 魔数
- Class文件版本
- 常量池
- 访问标志
- 类索引,父类标志,接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
-
魔数
- 每个Class文件开头的4个字节的无符号整数称为魔数(Margin Number)
- 它的唯一作用是确定这个文件是否位一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。
- 魔数值固定为0XCAFEBABE,不会改变
- 如果一个Class文件不以0XCAFEBABE开头,虚拟机在进行文件校验的时候会直接抛出错误。
- 使用魔数而不是拓展名来进行识别主要是基于安全方面的考虑,因为文件拓展名可以随意地改动。
class文件版本号
- 紧接着魔数4个字节储存的是Class文件的版本号。同样也是4个字节,第5,6个字节为副版本号,第7,8个为主版本号。
- 它们共同构成了class文件的格式版本号,譬如整个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M,m
- 版本号和Java编译器的对应关系如下表:
-
- 版本可以向下兼容,无法向上兼容
常量池
常量池计数器(constant_pool_count)
- 在版本号后,紧跟着的是常量池的数量,以及若干个常量池表项。
- 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(16个比特为65536),但是常量池最大只能到65535,因为它容量的计数是从1开始,而不从0开始。即表示constant_pool_count=1表示常量池中有0个常量项。
- 如果为
00 16
,也就是22,实际上只有21个常量,索引范围是1-21。 - 因为它把0位置空出来了,这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目的含义”,这种情况可用索引值0来表示。
常量池表(constant_pool)
- 常量池主要存放两类常量:字面量(Listen)和符号引用(System References)
- 字面量:文本字符串,声明为final的常量值
- 符号引用:类和接口的权限定名,字段的名称和描述符,方法的名称和描述符
- 权限定名:com/feng/test/Demo,仅仅是把包名的“.”替换成“/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”,表示权限定名结束。
- 简单名称:是指没有类型和参数修饰的方法和名称。
- 描述符:作用是用来描述字段的数据类型,方法的参数列表
-
- 它包含了class文件结构及其子结构中引用的所有字符串常量,类或接口名,字段名和其他常量。常量池中的每一项都具备相同的特征。第一个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte。(标记字节)
-
补充说明
虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再类加载过程中解析阶段将其替换为直接引用,并翻译到具体的内存地址中。 这里说明下符号引用和直接引用:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用时与虚拟机实现的内存布局相关,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不同。如果有了直接引用,那说明引用目标已经在内存中。
访问标识(2个字节)
- 在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口:是否定义为public类型:是否定义为abstact类型,如果是类的话,是否被声明为final
-
- 类的访问权限通常为ACC_ 开头的常量
- 每一种类型的表示都是通过设置访问标识的32位中的特定位来实现的。比如,若是public final的类,则该标记为ACC_PUBLIC|ACC_FINAL
类索引(this_class),父类索引(super_class),接口集合(interface)
- 类索引(u2),这是指向常量池的索引。它提供了类的权限定名。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口。
- 父类索引(u2),这是指向常量池的索引。它提供了当前类的父类的权限定名。如果没有继承任何类,其默认继承的是java/lang/Object类。且父类不会是final
- interface_count(u2接口计数器),代表当前类或接口的直接超类接口数量
- interfaces[] (ux接口索引集合),interfaces[]中每个成员的值必须是对常量池中某项的有效索引值,它的长度为interface_count.每个成员interface[i]必须为CONSTANT_Class_info结构。
字段表集合
fields
- 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例变量,但是不包括方法内部,代码块内部声明的局部变量。
- 字段的名字和类型引用常量池中的常量来描述。
- 它指向常量池索引集合,它描述了每个字段的完整信息。(字段的标识符,修饰符,类变量还是实例变量,是否常量)
fields_count(u2)
- fileds_count的值表示当前class文件fields的成员个数。
- fields表中的每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,但是不包括方法内部,代码块内部声明的局部变量,也不包括从父类继承的那些字段。
field_info()
方法表集合
methods:指向常量池索引集合,它完整描述了每个方法的签名。
- 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符,方法的返回值类型以及方法的参数信息等。
- 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。
- 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法,另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息初始化( < client > )和实例初始化< init >()方法。
methods_count(u2)
- methods_count的值表示当前class文件methodss的成员个数。
- methods表中的每个成员都是一个methods_info结构。
methods_info()
属性表集合
方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class文件的源文件的名称。以及任何带有RetensionPolicy.Class或者RetensionPolicy.RUNTIME的注解。这类信息通常被用于java虚拟机的验证和运行,以及Java程序的调试。
属性表计数器(attributes_count )
attributes_count的值表示当前class文件属性表的成员个数。属性表中每一项都是一个attribute_info结构。
属性表(attributes_info)
-
- code属性表
-
class文件结构图
解析Class文件操作
javac -g操作
- 解析字节码文件得到的信息中,有些信息(如局部变量表,指令和代码行偏移量映射表,常量池中方法的参数名称等等)需要再使用javac编译成class文件时制定参数才能输出。
- 使用javac xx.java 就不会生成对应的局部变量表,使用javac -g xx.java 就可以生成
javap 操作
- -public 仅显示公共类和成员
- -protected 显示受保护的/公共类和成员
- -p -private 显示所有类和成员
- -package 显示程序包/受保护的/公共类和成员(默认)
- -sysinfo 显示正在处理的类的系统信息(路径,大小,日期,MD5散列,源文件名)
- -constants 显示静态最终常量
- -s 输出内部类型签名
- -l 输出行号和本地变量表
- -c 对代码进行反汇编
- -v -verbose 输出附加信息(包括行号,本地变量表,反汇编等详细信息)