我们都知道java程序经过javac xxx.java编译后会生成xxx.class文件,接下来就要解析这个class类文件。
首先它的结构是什么呢?
1.Class文件是一组以8个字节为基础单位的二进制流(可能是磁盘文件,也可能是类加载器直接生成的),各个数据项目严格按照顺序- 紧凑地排列,中间没有任何分隔符。
2.Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,只有两种数据类型:无符号数和表。
3.无符号数属于基本的数据类型,以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字- 、索引引用、数量值或者按照UTF-8编码构成字符串值。
4.表是由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯以“_info”结尾。
5.无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项- 的形式,这时称这一系列连续的某一类型的数据未某一类型的集合。
.class文件结构表:
魔数:
CAFE BABE -> 固定值如果不是这个虚拟机会拒绝执行。
次版本号:
00 00 -> 0x00 -> 10进制结果就是0。
主版本号:
00 34 -> 0x34 -> 10进制结果就是52。
java主版本就是1.8。
00 13 -> 0x13 -> 10进制结果就是19 但是需要减去1。得到常量池大小18个。
可通过命令javap -verbose Demo.class查看得到结果:
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."":()V
#2 = Fieldref #3.#16 // com/neo/asmtest/Bytecode/Demo.age:I
#3 = Class #17 // com/neo/asmtest/Bytecode/Demo
#4 = Class #18 // java/lang/Object
#5 = Utf8 age
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 getAge
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 Demo.java
#15 = NameAndType #7:#8 // "":()V
#16 = NameAndType #5:#6 // age:I
#17 = Utf8 com/neo/asmtest/Bytecode/Demo
#18 = Utf8 java/lang/Object
常量表:
具体结构图:
描述符对照表:
常量池:
紧接着主版本号的就是常量池,常量池可以理解为class文件的资源仓库,它是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,也是class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量不是固定的,所以常量池入口需要放置一项u2类型的数据,代表常量池中的容量计数。不过,这里需要注意的是,这个容器计数是从1开始的而不是从0开始,也就是说,常量池中常量的个数是这个容器计数-1。将0空出来的目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。class文件中只有常量池的容量计数是从1开始的,对于其它集合类型,比如接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近Java语言的常量概念,如文本字符串、声明为final的常量等。而符号引用则属于编译原理方面的概念,它包括三方面的内容:
1.类和接口的全限定名(Fully Qualified Name);
2.字段的名称和描述符(Descriptor);
3.方法的名称和描述符;
常量池中的每一项都是一个表,在JDK1.7之前有11中结构不同的表结构,在JDK1.7中为了更好的支持动态语言调用,又增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。
访问标识
00 21 -> 标示表示类或者接口的访问信息。
如下:
00 21 -> 0x01 + 0x20 -> ACC_SUPER + ACC_PUBLIC
可通过取与操作,判断这个标识符是否被某标示符修饰。
例如:
0x21的二进制100001 -> ACC_PUBLIC的二进制1 -> 100001&1 = 1 ->是包含ACC_PUBLIC访问表示符。
类索引
00 03 -> 指向该类的CONSTANT_Class常量 -> 0x0003 -> 3 -> 常量3 结果是com/neo/asmtest/Bytecode/Demo 类索引指向了该类的全限定名。
父类索引
00 04 -> 指向该类的CONSTANT_Class常量 -> 0x0004 -> 4 -> 常量4 结果是java/lang/Object Demo类没有继承任何类,所以其默认的父类是Object类。
接口计数器
接口索引集合
接口索引集合是一个集合,包含了所有实现的接口的索引,每个接口索引占用2个字节,指向常量中的接口。
由于Demo.java没有实现任何接口,所以不存在这部分的值。
字段个数
00 01 -> 0x0001 -> 之后有1个字段 -> 字段主要用来描述类或者接口中声明的变量(包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量)。
00 00 -> 表示该类实现了几个接口,即implements了几个接口。由于Demo没有实现接口,其值为0x00 00,转换为十进制也为0。
field_info{
u2 access_flags;//访问标志
u2 name_index;//字段名索引
u2 descriptor_index;//描述符索引
u2 attributes_count;//属性计数器
attribute_info attributes;//属性集合
}
00 02 00 05 00 06 00 00
access_flags = 0x0002 -> 2 -> 0010 -> ACC_PRIVATE -> private
name_index = 0x00 05 -> 5 -> 指向常量5 -> age
descriptor_index = 0x00 06 -> 6 -> 指向常量6 -> I ->表示int类型
attributes_count = 0x00 00 -> 0 -> attribute_info不存在
结果:
private int a
方法计数器
00 02 -> 0x00 02 -> 2个方法
无参构造函数与getAge()。
方法表
结构为:
method_info{
u2 access_flags; //方法访问标志
u2 name_index; //方法名称索引
u2 descriptor_index; //方法描述符索引
u2 attributes_count; //属性计数器
struct attribute_info{
u2 attribute_name_index; //属性名的索引
u4 attribute_length; //属性的长度
u2 max_stack;//操作数栈深度的最大值
u2 max_locals;//局部变量表所需的存续空间
u4 code_length;//字节码指令的长度
u1 code; //code_length个code,存储字节码指令
u2 exception_table_length;//异常表长度
exception_info exception_table;//exception_length个exception_info,组成异常表
u2 attributes_count;//属性集合计数器
attribute_info attributes;//attributes_count个attribute_info,组成属性表
}
}
方法1
00 01 00 07 00 08 00 01 00
access_flags = 0x00 01 -> 1 -> public
name_index = 0x00 07 -> 7 -> 指向常量7 ->
descriptor_index = 0x00 08 -> 8 ->指向常量8 -> ()V
attributes_count = 0x00 01 -> 1 -> 记录着该方法有几个属性 -> 1个属性
00 09 00 00 00 1D 00 01 00 01 00 00 00 05
attribute_name_index = 0x00 09 -> 9 -> 指向常量9 -> Code
attribute_length = 0000001D -> 29 -> 29个字节
max_stack = 0x00 01 -> 1
max_locals = 0x00 01 -> 1
code_length = 00000005 -> 5个字节
2A B7 00 01 B1
code -> 0x2A B7 00 01 B1 ->
不能转换成十进制取看类,需要把十六进制转换成指令集。
结果为:
2A -> aload_0 //从局部变量0中装载引用类型值入栈
B7 -> invokespecia //编译时方法绑定调用方法
00 -> nop //空操作
01 -> aconst_null //null值入栈
B1 -> return //void函数返回。
00 00 00 01
exception_table_length = 0x00 -> 0
这里存放的是处理异常的信息。
每个exception_table表项由start_pc,end_pc,handler_pc,catch_type组成。start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理;handler_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有的异常,这个可以用来实现finally的功能
attributes_count = 0x00 01 -> 1 -> 表示有一个附加属性
attribute_info 结构如下:
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u2 line_tab_table_length
}
line_tab_table_length{
u2 start_pc;
u2 line_number;
}
00 0A 00 00 00 06 00 01 00 00 00 03
attribute_name_index = 0x00 0A -> 10 -> 指向常量10 -> LineNumberTable
attribute_length = 0x00 00 00 06-> 6个字节 -> 后面有6个字节的属性
line_tab_table_length = 0x00 01 -> 1代表LineNumberTable有一项值
start_pc = 0x00 00 -> 0,代表字节码行号
line_number = 0x 00 03 -> 3 -> 代表java源码的行号为第3行
方法2
00 01 00 0B 00 0C 00 01
access_flags = 0x00 01 -> 1 -> public
name_index = 0x00 0B -> 11 -> 指向常量11 -> getAge
descriptor_index = 0x00 0C -> 12 ->指向常量12 -> ()I
attributes_count = 0x00 01 -> 1 -> 记录着该方法有几个属性 -> 1个属性
00 09 00 00 00 1D 00 01 00 01 00 00 00 05
attribute_name_index = 0x00 09 -> 9 -> 指向常量9 -> Code
attribute_length = 0x0000001D -> 29 -> 29个字节
max_stack = 0x00 01 -> 1
max_locals = 0x00 01 -> 1
code_length = 0x00000005 -> 5个字节
2A B4 00 02 AC
code -> 0x2A B4 00 02 AC ->
不能转换成十进制取看类,需要把十六进制转换成指令集
->结果如下:
2A -> aload_0 //从局部变量0中装载引用类型值入栈
B4 -> getfield // 获取对象字段的值
00 -> nop // 空操作
02 -> iconst_m1 //-1(int)值入栈
AC -> ireturn //返回int类型值
00 00 00 01
exception_table_length = 0x00 -> 0
attributes_count = 0x00 01 -> 1 -> 表示有一个附加属性
00 0A 00 00 00 06 00 01 00 00 00 08
attribute_name_index = 0x00 0A -> 10 -> 指向常量10 -> LineNumberTable
attribute_length = 0x00 00 00 06-> 6个字节 -> 后面有6个字节的属性
line_tab_table_length = 0x00 01 -> 1代表LineNumberTable有一项值
start_pc = 0x00 00 -> 0,代表字节码行号
line_number = 0x 00 08 -> 8 -> 代表java源码的行号为第8行
附加属性-属性个数
00 01 -> 1 -> 表示后面有1个附加属性值。
附加属性-属性结构
00 0D 00 00 00 02 00 0E 结构为:
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
attribute_name_index = 0x00 0D = 13 -> 指向常量13 -> SourceFile
attribute_length = 0x00 00 00 02 = 2 -> 后面有2字节内容
sourcefile_index = 0x00 0E = 14 -> 指向常量14 -> Demo.java
代表着个class字节码文件的源码名为Demo.java。
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式:
1.隐式装载: 程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
2.显式装载, 通过class.forname()等方法,显式加载需要的类。
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
Java的类加载器有三个,对应java三种类:
1.系统类
2.扩展类
3.程序员自定义类
Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
|
- - ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
|
- - AppClassLoader // 负责加载应用类(程序员自定义的类)
三个加载器各自完成自己的工作,那么它们是如何协调工作呢?哪一个类该由哪个类加载器完成呢?为了解决这个问题,Java采用了委托模型机制。
当类加载器需要加载类的时候,先请示其Parent(即上一层加载器)在其搜索路径载入,如果找不到,才在自己的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,因为加载器必须保证基础类的加载。之所以是这种机制,还有一个安全上的考虑:如果某人将一个恶意的基础类加载到jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,自然就不会将该类加载进来。
获取类加载器:
ClassLoader loader = ClassName.class.getClassLoader();
ClassLoader ParentLoader = loader.getParent();
注意:
Java在逻辑上并不存在BootstrapKLoader的实体!因为它是用C++编写的,所以打印其内容将会得到null。