无关性的基石
实现语言无关性的基础是虚拟机和字节码存储格式。Java虚拟机不与任何语言绑定(包括Java),它只与‘Class文件’这种特定的二进制文件格式所关联。Class文件中包含了Java虚拟机指令集、符号表以及其它辅助信息。出于安全考虑,《Java虚拟机规范》要求在Class文件中必须应用许多强制性的语法和结构化约束,但图灵完备的字节码格。
Class类文件的结构
Class文件是一组以8个字节为基础的二进制流,各项数据项目严格按照顺序紧凑的排列在文件中,中间没有任务分隔符。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储。
高位在前: 高位的字节存储在内存中的低位。
例如:‘123456’ 那么1位高位的字节,6为低位的字节,加入有内存地址 012345,那么0中存1,1中存2.。。。5中存6,这种模式叫做高位在前,也叫大端。
Class文件结构中包括两种数据结构:‘无符号数’和‘表’。
- 无符号数是基本数据类型。用u1,u2,u3,u4来分别表示1个字节、2个字节、3个字节、4个字节。
- 表是由一系列无符号数或者其他表组成的复合数据结构。所有的表都习惯性的以”_info“结尾。
Class文件格式表: P215
魔数
魔数用来确定该文件是否为能被JVM所接受的Class文件。
现写两个Java类
package j1;
public class Test {
private String item = "itemStr";
public String nothing(){
return item;
}
}
package j1;
public class Test1 {
private int count = 1;
public int inc() {
return count + 1;
}
}
将其编译后,用notepad++或者sublime打开
可以看到在十六进制下,两个Class文件的头四个字节是cafebabe,这四个字节就是魔数。为什么说cafebabe是四个字节呢,因为这里是16进制,1个字节占8位,需要用两位十六进制符号表示,例如 11001101用十六进制表示应该为0XCD,简写为CD.
次版本号
紧跟着魔数后面的两个字节为次版本号
主版本号
紧跟着次版本号后面的为主版本号,0034由16进制转换为十进制后为52,查询Class文件版本号可以找到JDK版本为JDK8.所以该文件能被JDK8及以上版本的虚拟机所执行。
常量池
主版本号后为常量池的的入口。常量池的入口放着一个u2类型的数据,用来表示常量池中常量的数目。
0013表示常量池中有18项常量,索引值范围为1-19。为什么0X0013转换为十进制后为19,但是这里说常量池中有18项常量呢?因为第一个常量存在索引为1的位置,这么做是考虑到后面某些指向常量池的索引值的数据在特定的情况下需要表达“不引用任何一个常量池项目”的含义,这个时候将索引值设为0即可来表示。
Class文件结构中只有常量池的容量计数是从1开始的,其余都是从0开始。
常量池中主要存放两大类常量: 字面量和符号引用
字面量
- 文本字符串
- 被final修饰的常量等
符号引用
符号引用输入编译原理方面的概念
- 被模块导出或开放的包Packge
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
?描述符是什么?方法句柄是什么?动态调用是什么?动态常量是什么
截止JDK13一共有17中不同类型的常量。现总结为如下:
我们以Test1.class文件来分析
红框内的内容为常量池,其内容按照常量池项目图来按图索骥即可。
利用命令javap -v Test1.class
或者javap -verbose Test1.class
来反编译字节码文件,可以得到下图:
其中Constant pool下项目与16进制文件中红框内容对应。
访问标志
常量池结束后,紧接着的2个字节代表访问标志(access_flags),这个标志用来表示一些类或者接口层次的访问信息。
比如说这个Class是类还是接口,是否定义为Public类,是否为Abstract类,如果是类的话是否被声明为final等等。
access_flags一共有16个标志位可用,当前定义了其中的9个。没有使用到的标志位为0.因此,标志位的值也可以理解为是使用到的标志的位运算或。
图见: p224
类索引、父类索引、接口索引集合
类索引及父类索引都是一个u2类型的数据,接口索引集合是一组u2类型的数据的集合。这三项数据确定了改类型的继承及实现关系。
类索引确定了这个类的全限定名,父类索引确定了该类的父类的全限定名。除了java.lang.Object外其余类的父类索引都不为0.接口索引集合用来描述该类实现了哪些接口,这些被实现的接口安装implements关键字(若该类表示的是一个接口,那么应该为extends)后的接口顺序从左到右排列在索引集合中。对于接口索引集合,入口的第一项u2类型的数据为接口的计数器,表示索引表的容量,也就是实现了几个接口,若没有实现接口,则值为0。可以看到这里有点类似常量池的结构。
字段表集合
图见: p226-227
字段表用来描述接口或者类中声明的变量。包括类级变量以及实例级变量,但是不包括方法内部的局部变量。
全限定名: 将类的全限定名中".“用”/"代替后的字符串。
简单名称: 没有类型和参数修饰的方法或者字段。例如有个一方法public void test(int a,int b){}
,则该方法的简单名称为test。一个字段int a = 1;
其简单名称为”a“。
描述符: 按照一定的规则来描述字段的类型、方法的参数列表(数量,类型及顺序)和返回值。
描述符标识字符含义见P227
描述符的一些规则
- 对于数组,每增加一个维度就使用一个前置的“[”来表述,例如int[ ] [ ]对应的描述符为“[[I”。
- 用描述符来描述方法时,按照先参数列表后返回指的顺序来描述。例如java.lang.String.toString()的描述符为()Ljava/lang/String。
字段表中可能会有一个属性集合表,用来存储一些额外的数据。
字段表集合中不会出现父类或者父接口中继承而来的字段,但可能出现一些代码中不存在的字段。对于Class文件格式来讲,只要两个字段的描述符不是完全相同的,那字段重名就是合法的。
方法表集合
结构与字段表集合类似。图见P229
方法中的Java代码存在方法属性表集合中一个名为“Code”的属性里面。
tip
- 若父类方法没有在子类中重写,方法表集合中就不会出现来自父类的方法信息。
- 在编译时,编译器可能向方发表自动添加方法,比如类构造器(“()”)、实例构造器("()")
- Java中重载一个方法,需要与原方法具有相同的简单名称及与原方法不同的特征签名。特征签名是指一个方法中各个参数在常量池中的字段符号引用集合。返回值不包括在特征签名中。
属性表集合
这段内容有点烦躁,回头再看