要了解JVM的运行机理,从class文件入手最好不过了,下面就简单梳理了一下阅读一个简单的java 字节码文件的过程。个人感受,阅读这个难度到不是很大,重要的是要查阅好Oracle的JVM标准文件以及对工具的使用。
【Oracle官方文档】:https://docs.oracle.com/javase/specs/index.html
使用工具:IDEA
使用的插件:jclasslib 和 BinEnd
JDK:1.8
我使用的源代码是最简单的JAVA程序,仅用来梳理整个class文件的框架 。
package com.example.demo;
public class helloword {
}
这是一个巨简单的文件,主要为了说明编译后的class文件的格式。编译之后的helloword.class文件使用BinEnd工具打开,得到如下(我这里用16进制展示) :
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 001c 4c63 6f6d
2f65 7861 6d70 6c65 2f64 656d 6f2f 6865
6c6c 6f77 6f72 643b 0100 0a53 6f75 7263
6546 696c 6501 000e 6865 6c6c 6f77 6f72
642e 6a61 7661 0c00 0400 0501 001a 636f
6d2f 6578 616d 706c 652f 6465 6d6f 2f68
656c 6c6f 776f 7264 0100 106a 6176 612f
6c61 6e67 2f4f 626a 6563 7400 2100 0200
0300 0000 0000 0100 0100 0400 0500 0100
0600 0000 2f00 0100 0100 0000 052a b700
01b1 0000 0002 0007 0000 0006 0001 0000
0003 0008 0000 000c 0001 0000 0005 0009
000a 0000 0001 000b 0000 0002 000c
我使用jclasslib 解析的class文件整理成xmind参考
BinEnd处理的文件如下 :
Class文件是一组以8位字节为基础单位的二进制流,包含多个数据项目(数据项目的顺序,占用的字节数均由规范定义)。
数据项目分为2种基本数据类型(以及由这两种基本数据类型组成的集合):
1,无符号数,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数
2,表,以“_info”结尾,由多个无符号数或其它表构成的复合数据类型
整体文件结构如下(官方提供):
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
下面是一个表格形式说明
类型 | 名称 | 数量 |
---|---|---|
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 | 1 |
attribute_info | attributes | attributes_count |
由于不包含任何分隔符,故表中的数据项,无论是数量还是顺序,都是被严格限定的
这里的阅读过程按照上面的文件结构,从上到下,从外到内嵌套阅读,要注意阅读顺序
每个Class文件的头四个字节称为魔数,它的唯一作用是用来确定该文件是否为一个能被虚拟机接受的Class文件。
次版本占用u2(两字节),这里minor_version=0x0000
主版本占用u2(两字节),这里major_version=0x0034(翻译成十进制就是52)
(JDK1.5:0x0031,JDK1.6:0x0032,JDK1.7:0x0033,JDK1.8:0x0034)
由于常量池中常量的数目不是固定的,所以在常量池入口首先使用一个2字节长的无符号数constatn_pool_count来代表常量池计数值。
constant_pool_count:占2字节,0x0010,转化为十进制为15,即说明常量池中有15个常量(常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~15。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示
参照jclasslib生成的结果:
常量池中的常量索引最大为constant_pool_count-1,即例如上面有16个常量,最大索引是15
表类型数据集合,即常量池中每一项常量都是一个表,共有14种结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构参见:
类型 | 标志位 |
---|---|
CONSTANT_Class_info | 7 |
CONSTANT_Fieldref_info | 9 |
CONSTANT_Methodref_info | 10 |
CONSTANT_InterfaceMethodref_info | 11 |
CONSTANT_String_info | 8 |
CONSTANT_Integer_info | 3 |
CONSTANT_Float_info | 4 |
CONSTANT_Long_info | 5 |
CONSTANT_Double_info | 6 |
CONSTANT_NameAndType_info | 12 |
CONSTANT_Utf8_info | 1 |
CONSTANT_MethodHandle_info | 15 |
CONSTANT_MethodType_info | 16 |
CONSTANT_InvokeDynamic_info | 18 |
可以看到,字节码中接下来对应的tag是0a(十进制是10),标志位tag对应的类型是:CONSTANT_Methodref_info,
CONSTANT_Methodref_info对应的文件结构如下:
CONSTANT_Methodref_info {
u1 tag; #0a
u2 class_index; #00 03
u2 name_and_type_index; #00 0d
}
然后开始对CONSTANT_Methodref_info文件进行class文件的解析,如上面所示:class_index
和name_and_type_index
都是引用常量池,可以从jclasslib的处理结果看出
然后再分别取对应,即可以发现刚好印证了我们的想法。
关于常量池这里不再赘述过多,可以参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
进行后面的解析,这里贴出关于常量池的跟踪结果:
0a #表开始的u1类型标志位 tag=10,对应CONSTANT_Methodref_info表
00 03 #里面具体的引用#3
00 0d #里面具体的引用#13
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
000e #里面的具体引用#14
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
00 0f #里面的具体引用#15
01 00 06 3c69 6e69 743e #[04]表开始的u1类型标志位 tag=1,对应CONSTANT_Utf8_info表 内容:
01 00 0328 2956 #[05]内容:()V
01 00 0443 6f64 65 #[06]内容:Code
01 00 0f 4c69 6e65 4e75 6d62 6572 5461 626c 65 #[07]内容:LineNumberTable
01 00 12 4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65 #[08]内容:LocalVariableTable
01 00 0474 6869 7301 0 #[09]内容:this
01 c 4c63 6f6d 2f65 7861 6d70 6c65 2f64 656d 6f2f 6865 6c6c 6f77 6f72 643b #[10]内容:Lcom/example/demo/helloword;
01 00 0a53 6f75 7263 6546 696c 65 #[11]内容:SourceFile
01 000e 6865 6c6c 6f77 6f72 642e 6a61 7661 #[12]内容:helloword.java
0c 00 04 00 05 #[13]里面的具体引用#4 #5
01 00 1a 636f 6d2f 6578 616d 706c 652f 6465 6d6f 2f68 656c 6c6f 776f 7264 #[14]内容:com/example/demo/helloword
01 00 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74 #[15]内容:java/lang/Object
访问标志位占用u2(2个字节),关于访问标志位的对应表如下
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2以后编译出来的类这个标志为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口和抽象类,此标志为真,其它类为假 |
ACC_SYNTHETIC | 0x1000 | 标识别这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
查看字节码中的标志位如下:
访问标志位是:0x0021,从上面的标志位映射表中找不到对应的数值,这是什么原因的?先使用javap -v xx.class文件
E:\projs\demo\target\classes\com\example\demo>javap -v helloword.class
Classfile /E:/projs/demo/target/classes/com/example/demo/helloword.class
Last modified 2020-5-30; size 286 bytes
MD5 checksum b2654afcd52ddbe82eecd1b919012f4e
Compiled from "helloword.java"
public class com.example.demo.helloword
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#13 // java/lang/Object."":()V
#2 = Class #14 // com/example/demo/helloword
#3 = Class #15 // java/lang/Object
#4 = Utf8
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/example/demo/helloword;
#11 = Utf8 SourceFile
#12 = Utf8 helloword.java
#13 = NameAndType #4:#5 // "":()V
#14 = Utf8 com/example/demo/helloword
#15 = Utf8 java/lang/Object
{
public com.example.demo.helloword();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/demo/helloword;
}
SourceFile: "helloword.java"
从这里可以看出对应的flags: ACC_PUBLIC, ACC_SUPER
是两个权限,结果就来了,这里使用或
运算进行计算:测试类的访问标志为ACC_PUBLIC | ACC_SUPER = 0x0001 | 0x0020 =1 | 32 =33 = 0x0021
官方文档解释如下:
The value of the this_class item must be a valid index into the
constant_pool table. The constant_pool entry at that index must be a
CONSTANT_Class_info structure (§4.4.1) representing the class or interface
defined by this class file.
这里是引用常量池的,对应的值如下:
对应的值是:0x0002,从jclasslib结果中可以看到 ,结果就是自己
官方文档解释如下:
For a class, the value of the super_class item either must be zero or
must be a valid index into the constant_pool table.
也是从常量池中的引用过来的
应用的是常量池中的0x0003,查看jclasslib结果
接口数量为:0x0000,这个为0,后面的interfaces[interfaces_count];
也就不存在了,直接看fields_count
字段数量也是0x0000,跳过fields[fields_count]
直接看方法数量methods_count
从反编译的字节码可以看出里面包含一个默认构造方法
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.example.demo;
public class helloword {
public helloword() {
}
}
所以数量应该是1
方法表的结构如下:
method_info {
u2 access_flags; #00 01
u2 name_index;#00 04
u2 descriptor_index;#00 05
u2 attributes_count;#00 01
attribute_info attributes[attributes_count];
}
方法表的access_flag对应关系如下:
标志名称 | 标志值 | 含义 |
---|---|---|
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_STRICTFP | 0x0800 | 字段是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生 |
name_index
和descriptor_index
都是引用常量池的内容,这里不赘述,验证方法同上
attributes_count
属性有一个,接下来看属性详情attribute_info
,
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
从上面可以看到只有一个属性,接下来应该是属性attribute_name_index
引用常量池#6,即只有Code属性
接下来attribute_length
有4个字节00 0000 2f
即长度47,可以从jclasslib结果看出
然后查看Code
属性Code_attribute
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
对应的结果如下:
00 06 #attribute_name_index #6, 即Code
00 0000 2f #code_length 47
00 01 #操作数栈深度为1
00 01 #方法局部变量占用的空间为1
00 0000 05 #接下来又5个字节的指令,见下
2a #指令:aload_0
b7 0001 #指令:invokespecial 引用#1
b1 #指令:return
0000 #Code属性异常表集合为空
0002 #说明Code属性带有2个属性
code通过jclasslib可以看到有2个属性LineNumberTable
和local_variable_info
#LineNumberTable 属性结构
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
#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];
}
对应的分析结果如下:
0007 #Code属性第一个属性的属性名称,指向常量池中第7个常量:LineNumberTable
0000 0006 #表示LineNumberTable属性值所占字节长度为6
0001 #即该line_number_table中只有一个line_number_info表
0000 #start_pc为0x0000
0003 #line_number为0x0003 ===到这里line_number属性结束
0008 #Code属性第二个属性的属性名,指向常量池中第8个常量
0000 000c #该属性值所占的字节长度为12
0001 #说明local_variable_table中只有一个local_variable_info表
0000 #start_pc为0x0000
0005 #length为0x0005
0009 #name_index为0x0009,指向常量池中第9个常量 this
000a #descriptor_index为0x000a,指向常量池中第10个常量:Lcom/example/demo
0000 #index
可以看到只有一个属性,属性的结构如下:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
分析结果如下:
000b #attribute_name_index #11 SourceFile
0000 0002 #attribute_length 长度为2
000c #sourcefile_index #12 helloword.java
从jclasslib的结果可以印证这一点
整个文件的解读结果如下:
cafe babe #魔数
0000 #小版本
0034 #大版本
0010 #counstant_pool_count 15个
0a #表开始的u1类型标志位 tag=10,对应CONSTANT_Methodref_info表
00 03 #里面具体的引用#3
00 0d #里面具体的引用#13
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
000e #里面的具体引用#14
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
00 0f #里面的具体引用#15
01 00 06 3c69 6e69 743e #[04]表开始的u1类型标志位 tag=1,对应CONSTANT_Utf8_info表 内容:
01 00 0328 2956 #[05]内容:()V
01 00 0443 6f64 65 #[06]内容:Code
01 00 0f 4c69 6e65 4e75 6d62 6572 5461 626c 65 #[07]内容:LineNumberTable
01 00 12 4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65 #[08]内容:LocalVariableTable
01 00 0474 6869 7301 0 #[09]内容:this
01 c 4c63 6f6d 2f65 7861 6d70 6c65 2f64 656d 6f2f 6865 6c6c 6f77 6f72 643b #[10]内容:Lcom/example/demo/helloword;
01 00 0a53 6f75 7263 6546 696c 65 #[11]内容:SourceFile
01 000e 6865 6c6c 6f77 6f72 642e 6a61 7661 #[12]内容:helloword.java
0c 00 04 00 05 #[13]里面的具体引用#4 #5
01 00 1a 636f 6d2f 6578 616d 706c 652f 6465 6d6f 2f68 656c 6c6f 776f 7264 #[14]内容:com/example/demo/helloword
01 00 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74 #[15]内容:java/lang/Object
0021 # ACCESS_FLAG:0021
0002 # THIS_CLASS:引用cp #2
0003 # SUPER_CLASS:引用cp #3
00 00 #Interface_count:0
00 00 #Field_count
00 01 #method_count
00 01 #access_flag
00 04 #name_index
00 05 #descriptor_index
00 01 #attribute_count
00 06 #attribute_name_index #6, 即Code
00 0000 2f #code_length 47
00 01 #操作数栈深度为1
00 01 #方法局部变量占用的空间为1
00 0000 05 #接下来又5个字节的指令,见下
2a #指令:aload_0
b7 0001 #指令:invokespecial 引用#1
b1 #指令:return
0000 #Code属性异常表集合为空
0002 #说明Code属性带有2个属性
0007 #Code属性第一个属性的属性名称,指向常量池中第7个常量:LineNumberTable
0000 0006 #表示LineNumberTable属性值所占字节长度为6
0001 #即该line_number_table中只有一个line_number_info表
0000 #start_pc为0x0000
0003 #line_number为0x0003 ===到这里line_number属性结束
0008 #Code属性第二个属性的属性名,指向常量池中第8个常量
0000 000c #该属性值所占的字节长度为12
0001 #说明local_variable_table中只有一个local_variable_info表
0000 #start_pc为0x0000
0005 #length为0x0005
0009 #name_index为0x0009,指向常量池中第9个常量 this
000a #descriptor_index为0x000a,指向常量池中第10个常量:Lcom/example/demo
0000 #index
0001 #attributes_count=1
000b #attribute_name_index #11 SourceFile
0000 0002 #attribute_length 长度为2
000c #sourcefile_index #12 helloword.java
官方文档