以前只知道,java文件运行javac命令后 将会生成一个.class文件,继而运行java命令运行该class文件即可。也曾打开过class文件,里面一堆16进制数字,肉眼一瞟,什么**玩意?页面停留3秒,close~
那就来探索一下,这些个16进制数据究竟是什么鬼怪;
类的数据性结构
第一步,要认知到,所有的这些的class,都是严格按照如下一张表属性,前后顺序排列。
类的文件结构主要体现为这张表,从这张表开始认识:
1566711447(1).jpg
U: U1,U2,U4,U8,类型分别表示对应多少字节,这个用于在那些一堆16进制数字中,套取对应的值。例如:第一个属性 magic为U4,则该属性对应的值就是class文件那一堆数字中,最前面4个字节表示的16进制值。以此类推,后边的属性值,顺序读取。
_info: 后面有加info类型,则表示该属性是由多个组数据集合而成,例如:第一个info---constant_pool,则需要与U2---constant_pool_count 结合使用,字面意思就能理解,表示有多少个count 的constant; 每个constant按照该属性规则去划分多少字节。
这样就能把这个class的所有16进制数字,分别按照这个字节规则去从前往后划分,机器自然就能读出当前属性是什么,代表什么含义;机器是笨的,只会按照规则行事,创造这些规则的才是智者
属性
-
magic与version
每个Class文件的头4个字节称为魔数(magic),它的唯一作用是判断该文件是否为一个能被虚拟机接受的Class文件。它的值固定为0xCAFEBABE。紧接着d的4个字节是Class文件的次版本号和主版本号,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件。(copy 来的介绍)
-
constant_pool(常量池)
前面的属性位数读取完后,就是该属性的开始,(占用空间最大的属性);
主要分为两大类常量:
-
字面量 :和java层类似,例如Int,Float,double等等;
-
符号引用
-
类和接口的全限定名:也就是Ljava/lang/String;
-
字段的名称和表述(private,public等)
-
方法的名称和表述(private,public等)
-
符号引用和直接引用的区别与关联??难以理解,待解答
引用指的是需要通过其他的常量,显示当前对应的属性,存放的其他常量的地址 -
常量池中的每一项常量都是一个表(网上盗图--对应表--6-3)
1566725611(1).jpg每个常量都有自己的数据结构,字面量里的数据结构放着就是标志和值 (tag -- bytes),符号 引用里的则是存放另一个常量的地址,(tag -- index --index)有些符号引用数据结构中有两个index引用,例如:Methodref_info等,具体可参照书中 表6-6。
注: 由于所有的class名或者字段名,用到的描述都是第一个属性Utf-8_info,而它的结构(tag--length--bytes),最大占用字节为U2,所以方法名的长度不能超过64KB。不过哪有方法名这么长的???
通过这个表和查找方法就可以将class中所有常量,都能一一找出来,既然这是机器所做的操作,人为一个个去找实在太傻,自然也有机器的方法:。 Java类分解器 javap -verbose 能帮你解读当前的class文件,再也不用去看那些16进制的鬼东西
写了一个demo:
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
编译后进行 javap -verbose
C:\Users\Administrator\Desktop\aaaa>javap -verbose TestClass.class
Classfile /C:/Users/Administrator/Desktop/aaaa/TestClass.class
Last modified 2019-8-25; size 296 bytes
MD5 checksum d59bab4f31f6cbf544dcfeb5a3471d2c
Compiled from "TestClass.java"
public class com.porterking.clazz.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/porterking/clazz/TestClass.m:I
#3 = Class #17 // com/porterking/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/porterking/clazz/TestClass
#18 = Utf8 java/lang/Object
{
public com.porterking.clazz.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
SourceFile: "TestClass.java"
用这个显示去读class文件,不要太友好。个人认为,校对每个属性的表结构,然后一一去解读每位16进制的所代表的意思,能帮你充分理解上述代码如何生成,但具体去分析class文件,只需要读懂当前解析过之后的上述代码即可;
java字节代码解析主要分为三块,常量池,字段,方法
1. 常量池: 这里罗列了18项常量,每一项结合对应属性表的结构,包括引用属性 通过“#“,引用到对应的常量,一幕了然。还看到,常量里的utf8 属性展示的一些名称,<init>
()V
等 这些都是其他类属性所需要用到的常量,所以这个常量池是参与其他项目关联最多的,所以也是占用class文件控件最大的项目之一
感悟:java在编译时,根据当前代码,就帮你先声明好一块区域,也就是常量池,里面放的都是静态的一些值,例如方法名,字段名等等这些例如我们肉眼看到的字符,都给你编译成了那些16进制数字。而放入常量池中,在运行编译时,就可以通过地址获取到值。结合String str = "abc"; String str = new String("abc")
两者实现在字节码上的实现不同点, 能有效的理解常量池的作用。运行时时常量,有待深入考察。
2. 字段: 测试代码中,并没有将 字段m设为public属性,因此没有这里申明,书上未做解释,个人认为,只有public或者protected 属性,需要被外部引用的,才会在字节码上有申明,不然也就只是在常量中存在。根据属性表的结构:1. descriptor--类型 2.flags--访问标志 3.ConstantValue--属性表集合(只有当前字段被final static修饰时才会有)
descriptor--类型,这个标识描述符的作用则是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序等)和返回值的。根据描述符规则,详细的描述符标示字的含义如下表所示,这与JNI中的全限定名如同一辙:
1566735098(1).jpg3. 方法: 数据结构与字段一致,就是attribute属性表所带不同,每个方法名一般都带有Code属性(除接口和者抽象方法)
Code数据结构:该属性为class代码最重要的属性,用于描述方法,阅读懂它就能知道它所描述的方法是什么
-
stack:操作数栈,方法套嵌的层数。
-
locals :局部变量所需要的空间单位Slot,基本类型一个变量1Slot,double、long为2Slot
-
args_size:参数个数,对于实例方法每个方法至少带有一个this的参数,static方法args_size从0开始计数
-
操作指令:目前Java虚拟机规范定义了其中200条指令,具体每一条指令是什么,《深入理解Javan虚拟机》中并没有一一阐述,看来是需要某搜索引擎查询; 上述指令中,aload_0 :含义为将第0个Slot中reference类型的变量推送到操作数栈顶;invokespecial :以栈顶reference类型数据作为接受者,调用实例构造方法,这个操作需要说明是什么方法,则指向了常量中的 #1 Methodref方法引用 return:返回指令
Code中的操作指令,也就给机器规定好一条一条的规则,逐条操作指令。这一块就很机器化了。
方法中还有其他的基本属性,
Exception属性:列举出方法中可能抛出的受查异常
LineNumberTable属性:描述Java源码行号与字节码行号之间的对应关系。
LocalVariableTable属性:描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的对应关系。
SourceFile属性:记录生成这个Class文件的源码文件名称。
属性表还有很多属性,就不全部展示
总结
学完这一章后,对于这个class文件,亲切了许多,不再是一头雾水。也能看的懂 javap 编译后的文件。 算有所收获吧,也能为后面的类加载机制垫底一些基础。
网友评论