源代码通过编译器编译之后生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不是C/C++经由编译器直接生成机器码。
Java虚拟机由一个字节长度的,代表着某种特定操作含义的操作码(opcode) 以及跟随其后的0至多个代表此操作所需参数的操作数。
虚拟机中许多指令并不包含操作数 只有一个操作码。
操作码 + 操作数 = 字节码指令
JClassLib插件,在Idea中安装JClassLib插件即可。
使用JDK自带的Javap工具来进行解析字节码文件。
javap -v xxx.class
使用Binary Editor 插件,这个解析出来的字节码是最原生的状态。
任何一个Class文件都对应着唯一的类或接口,但反过来说Class文件实际上它并不是以磁盘文件的形式存在。也就是说可能是从网络中传输的。
Class的结构不像XML等描述语言,由于它没有任何分割符号,所以在其中的数据项,无论是字节顺序还是数量,都是被严格规定的,那个字节代表什么含义,长度是多少,先后顺序如何,都不允许被改变。
CA FE BA BE 表示魔数,其实意思就是Java Class文件的一个标识,主要目的是为了让Java虚拟机识别,并且安全。让Java虚拟机觉得你这个Class文件是可以识别的。
使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意改动。
紧接着魔数后面的4个字节存储的是Class的版本号,同样也是4个字节,第五个字节和第二个字节表示编译的副版本号minor_version,而第7个和第八个字节就是编译的主版本号。
16进制的34转换为10进制就是52可以对应下图的表进行对应。
注意:
不同版本的Java编译器编译的Class文件对应的版本是不一样的,目前,高版本的java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行高版本编译器生成的Class文件,否则会抛出异常: java.lang.UnsupportedClassVersionError
常量池中存储着Class文件中的字段和方法。
在版本号之后,紧跟着的是常量池的数量以及若干个常量池表项。
常量池中常量的数量是不固定的,所以需要再常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值,也就是常量池计数器。
常量池表中,用于存放编译时期生成的各种字面量以及符号引用,这部分内容将会在类加载之后进入方法区的的运行时常量池。
比如常量池中有十项,那么常量池计数器是不是就是10?那显然是不是的。
例如如果我们常量池计数器为1 那么常量池中是没有项的,他是有一个偏差的。
比如如下例子:
可以看到常量池计数器的值为22,但是22是十六进制的,它对应的十进制就是34。那么常量池计数器的值为34,常量池中就有33项
常量池表中存储着两大类量,字面量和符号引用。
它包含class文件结构以及子结构引用的所有字符串常量,类或接口名,字段名,和其他常量。常量池中的每一项都具备想通的特征。
第一个字节作为类型标记,用于确定该项的格式,他这个字节称为tag byte。
例如OA,这里的十进制就是10,那么10代表什么呢?如下:
那么我们可以通过对照表得知,在10这个标识他表示CONSTANT_Methodref_info 类中方法的符号引用,所以我们就可以通过第一个表示来确定这一块的内容是什么。
字面量和符号引用
字面量比如: String str = "hello" 这里str就是一个字面量 存放在常量池表中。
符号引用:类和接口的全限定类名,字段的名称,方法的名称等等。比如com/nanchensec/ 这样就是全限定名
名称就是方法的名字和属性的名字。
描述符就是方法的参数列表中的数量 类型 以及顺序和返回值等。
常量池中主要存放字面量和符号引用
他表示的就是如下图:
这里从0A开始进行解析,0A的十进制是10,10表示方法的引用。他占用5个字节所以到了09。
09表示Fieldref_info也就是方法引用,他也是5个字节所以到了08。
08表示String_info 就是字符串类型字面量 占用3个字节 所以到了0A。
0A表示方法的引用,他占用5个字节所以到了07。
07表示类和接口的符号引用他占用3个字节,所以到了07。
07还是一样占用3个字节,所以来到了01。
01很特殊他表示字符串,第一个u1表示他的值为1,第二个u2表示字符串所占用的长度。
那么我们来看:这里01表示字符串 00 和 06表示字符串的长度,也就是说他的长度为6
所以这里的01 00 06 3C 69 6E 69 74 3E 表示的就是我们的字符串。这里的06表示的是我们字符串的长度,所以需要划分三个u1来表达。
后面也是一样的字符串。
01 00 03 28 29 56 表示这个字符串
后面依旧对照表进行解析。
常量类型和结构表
经过我们上面的分析和使用jclasslib可视化工具查看可以看到是一一对应的,都是我们解析的。
那我们从0A 00 06 00 14开始分析吧。
06表示06这个索引项,06的索引项是Class_info 06索引项又需要去找27索引项,所以拿到字符串字面量为:java/lang/Object
14的十进制的20 20表示20的索引项,20是NameAndType_info。
其实最后指向还是字符串面量。
09 00 15 00 16 解析:
这里的09表示的就是第9项,00 15 表示一个u2 00 16 表示一个u2 ,这里的u2就是他所对应的意思。
可以参考类似这样的表: 这里看到这里的tag的值代表的就是几项,我们上面的09表示第九项,也就是CONSTANT_Fieldref_info
那么我们第一个u2 也就是00 15 表示只想声明字段的类或者接受描述符。
第二个u2表示的就是指向字段描述符CONSTANT_NameAndType_info的索引项。
那么00 15这个u2,15的十进制就是21,所以我们需要找到21项。
那么21项表示的是CONSTANT_Class_info这个项,对照如上表他的值为7。也可以使用jclassLib来查看。
我们可以看到CONSTANT_Class_info这个项,他又指向了十进制的28项,而28就是我们的字符串字面量,他的值为:java/lang/System
那么继续解析00 16 ,16的十进制是22,所以我们需要找到22的索引项。
22的索引项又找到了29 和 30索引项,而29 和 30索引项是2个字符串字面量,分别为:out/Ljava/io/PrintStream;
小总结:
总的来说就是一个引用一个最后都会指向字面量。
在常量池之后,紧跟着的就是访问标识,说白了就相当于权限修饰符,该标识使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型还是抽象类型,如果是类的话是否被声明为final等等。
如下图:
类的访问权限通常为ACC_开头的常量。
可以看到这里的00 21 2个字节表示的就是标识,但是21似乎在我们的表中是不存在的,21这个项是我们通过 加法进行合成的一项,ACC_PUBLIC + ACC_SUPER ACC_SUPER是一个默认的标识。所以加起来就是21。
在访问标识之后会指定类的类别 父类类别和实现的接口。注意是在标识符后面。
下图u2表示2个字节,this_class表示当类的Class。
紧接着后面两个字节表示的就是他父类的class,也就是super_class。
再接着两个字节表示的就是接口计数器。
最后一个标识接口表示接口索引集合。
我们来看下第一个this_class,可以看到在标识符后面 00 03 表示的就是this_class。
那么这个03 表示的就是第三项。
第三项表示的就是我们本类。
Super_class:可以看到他表示的是 00 07 那么07表示的就是07项。07项就是Object。
fields
1.表示接口或类中声明的变量,字段(field)包括类变量以及实例变量但是不包括方法内部,代码块内部声明的局部变量。
2.字段叫什么,字段被定义什么数据类型 这些都是无法固定的,只能引用常量池中的常量来描述。
3.它只指向常量池索引集合,他描述了每个字段的完整信息,比如字段的标识符,访问修饰符,(public provate 或protected) 是类变量还是实例变量(static修饰符) 是否是常量final修饰符。
Fields_count(字段计数器)
Fields_count的值表示当前class文件felds表的成员个数 使用两个字节来表示。
Fields表每个成员都是一个Fields_info结构,用于表示类或借口所声明的所有类字段或实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。
如下图表示字段计数器: 可以看到有一个字段。
如下图就是字段表的结构: 可以看到前四个都占用2个字节。
如图:
接着00 02 表示第二项,02表示num。
00 08表示的就是描述符,也就是
紧接着就是属性计数器和属性集合,因为涉及到集合了,所以需要提供属性计数器。
我们可以看到属性机器的值是09。
后续就是属性的情况了。
我们可以在jclassLib也可以查看到属性。
method表中的每个成员都必须是一个method_info结构,用于表示当前类或借口某个方法的完整描述。
method_info结构可以表示类和接口定义的所有方法,包括实例方法,类方法,实例初始化方法和类接口初始化方法。
方法表的结构实际跟字段表的结构是一样的。
如果遇到哪里写的不对 可以联系我 Get__Post