一直想成为一名优秀的架构师的我,转眼已经工作快两年了,对于java内核了解甚少,闲来时间,看看JVM,吧自己的一些研究写下来供大家参考,有不对的地方请指正。
废话不多说,一起来看看JVM中类文件是如何加载和运行的。
(1)首先,编写简单代码,对其编译生成的class文件进行研究,其java代码如下:
1 public class test { 2 private static int count = 0; 3 public static void recursion(){ 4 count++; 5 recursion(); 6 } 7 8 public static void main (String args[]){ 9 try { 10 recursion(); 11 }catch (Exception ex){ 12 System.out.print("deep of callings:"+count+"\n"); 13 ex.printStackTrace(); 14 } 15 } 16 }
编译之后,用WinHex软件打开其class文件,可以看到其编译的十六进制文件如下:
按照上图分析,开头的前4个字节,是魔数(类似于拼音“咖啡宝贝”),它的用处是标识该文件是否能被java虚拟机识别;
紧接着魔数的4个字节,前两字节0x00代表次版本号(小数点之后的数字),后两字节0x0033代表是class文件的主版本号,换算成十进制是51,标识是JDK1.7可识别的版本(不同的版本可以查看class文件版本号表如下:)
版本号 | 对应十进制 | jdk版本号 |
---|---|---|
2E | 46 | jdk1.2 |
2F | 47 | jdk1.3 |
30 | 48 | jdk1.4 |
31 | 49 | jdk1.5 |
32 | 50 | jdk1.6 |
33 | 51 | jdk1.7 |
34 | 52 | jdk1.8 |
在主版本号字节之后的是常量池,可以理解为Class文件的资源仓库,存储着与class文件相关的数据项。由于不同class文件,常量池数量不同,常量池入口放置两个字节的数据(0x0028)为常量池计数器。十六进制的0x0028为十进制的40(地址偏移量),代表常量池中有39个常量,索引范围为1-40(注:java仅限于class文件结构中容量计数器是从1开始的,java的设计者将索引0拿出来是有特殊考虑的,用来表示不引用任何一个常量池中的项)。
常量池中,存放两类数据:(1)字面量:可以理解为java中的常量,例如:字符串、final修饰常量等。
(2)符号引用:主要包括①类、接口的全限定名②字段的名称和描述符③方法的名称和描述符
在常量池里,存储常量结构如下:u1(常量标志位,用于指明常量的类型,可以查看如下常量池项目类型对应表)+常量信息
让我们以上述class文件为例,索引为1的常量标志位是0x0A(十进制为10),对应上表中的CONSTANT_Methoddef_info类型的常量,参考常量结构表如下图(在jdk1.7中新增了tag=15/16/18的常量类型,更好的支持动态语言的调用,此处就不列举了),
该class文件中,常量池里索引为1的常量(const#1),项目类型标识符为0x0A,二进制为10,查询上表,代表着类方法的符号引用。紧接着两个u2字符代表该常量的信息内容,其中方法描述符0x0006为#6常量,名称及类型描述为0x001A指向#26常量。
紧跟其后的是索引为2的常量(const#2),其标志符为0x09(十进制为9),是字段的符号引用,紧接着的两个u2字符代表其引用索引ID,方法的类描述符指向#27常量,字段描述符指向#28常量;
分析了以上两个字节之后,这里就不一一分析后面的常量了,有兴趣的可以自己分析下。其他的常量池用jdk自带的javap进行生成,在windows中打开cmd(安装jdk并配置环境变量),输入:javap -verbose class文件路径,可以看到编译之后的常量池如下:
将上述class文件中常量池部分标记图如下,红色框代表一个常量池中的项,依次编号为1-39,
我们将上图和javap生成的常量内容对比一下,以const#9为例,#9项为:0x01 (utf8类型) 0x0006(占用字节) 0x3C 0x69 0x6E 0x69 0x74 0x3E(项内容),我们对项内容进行在线转换,将十六进制转换ASCII码值,得到该常量表示:<init>,如下图:
与javap生成的常量文件对比,发现两者完全一致。对字节码有兴趣的朋友可以逐个试一试。
在常量池区域结束之后,紧接着的一个u2(两个字节)类型的字符代表访问标志,它用于识别类或者接口的访问信息,例如:class是类还是接口,访问是private还是public等。访问标志表如下图:
在上述文件中,访问标志为:,即:0x0021,对照上表,只有ACC_PUBLIC和ACC_SUPER为真,其他几项为假。该类为public 能够使用invoke指令。
跟在访问标志之后的分别是类索引、父类索引。由于java不允许多继承,所以类索引和父类索引是一个u2类型的数据。在上述文件中,类索引为#5常量(TestClass),父类索引为#6常量(java/lang/Object);
紧接着类索引和父类索引的是接口索引信息。在java中一个类可以实现多个接口,所以用u2类型的数据集合来表示接口索引。在接口索引的入口,有一项u2类型的接口计数器,计数器为0表示接口的索引表不占用任何字节。
在接口相关描述信息之后的,是字段表集合,用于描述类或者接口中申明的变量(注:此处的变量是指类或者接口级变量,即类变量或者实例级变量,而不包括方法中的局部变量)。这些字段通常包含哪些信息呢?通常有:字段访问域(private、public、protected等)+是实例变量还是类变量(Static)+是否可修改(final)+并发可见性(volatitle,用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最终的值)+可否序列化(transient)+字段类型(基本类型、对象、数组)+字段名称。在JVM中,字段表结构如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
我们来看,字段表集合的入口u2类型数据项是字段表的容量计数器为(0x0001),表示只有一个字段项。紧接着字段表容量计数器的u2类型的数据为0x0002,参考下表,表示该字段为private。字段名称为0x0007,其值为“m”,描述信息0x0008,其值为“I”,可以推断,原代码的定义字段为:“private int m”;
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x00 01 | 字段是否为public |
ACC_PRIVATE | 0x00 02 | 字段是否为private |
ACC_PROTECTED | 0x00 04 | 字段是否为protected |
ACC_STATIC | 0x00 08 | 字段是否为static |
ACC_FINAL | 0x00 10 | 字段是否为final |
ACC_VOLATILE | 0x00 40 | 字段是否为volatile |
ACC_TRANSTENT | 0x00 80 | 字段是否为transient |
ACC_SYNCHETIC | 0x10 00 | 字段是否为由编译器自动产生 |
ACC_ENUM | 0x40 00 | 字段是否为enum |
通常而言,在字段描述之后还有一些属性表信息存储额外的信息,在以上class文件中,属性计数器位0x0000,表示没有额外的属性信息。
在字段表之后的是方法表集合,其表示方法与字段信息表几乎一致。其结构如下表:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
但是,方法表的修饰属性比字段表要多,例如:方法有abstract、synchronize等。在方法表的入口,同样也有一个方法容量计数器,占用u2字节。在上述class中,方法的计数器为,即0x0003表示有三个方法。
第一个方法,function#1的第一、二、三、四、五项u2数据项分别为:0x0001、0x0009、0x000A、0x0001、0x000B,代表方法为public、方法名指向const#9("<init>")、方法描述为const#10(“()V”)、含有一个属性、该属性指向const#11(“Code”)属性,java呈现方法体重的代码经过javac编译之后,就存储在Code属性里。
(ps:今天的class文件探索就写到这里,过几天接着写...休息一下)