资料来源:《深入理解java虚拟机》
好久没写JVM的笔记了,最近在忙点其他的东西,好在JVM终于是看完第一遍了,赶紧整理总结。
本章节主要讲2部分内容:(1).class文件结构。(2)虚拟机字节码指令:用于解析执行.class文件。通过这一章节的学习,我们就可以了解到为什么java语言是平台无关性的,还有java虚拟机本身在实现平台无关性和语言无关性中扮演了什么重要的角色。
1、三个重要概念
1.1、平台无关性
所谓平台无关性,不外乎两点:pc的操作系统(比如Mac,windows,linux等)和组成pc的硬件。不管这你的机器在这两点的实现上是怎样的,你曾经写好的程序都能正常并且正确的跑起来。这就是平台无关性。“与平台无关”的理想最终实现在操作系统的应用层上面,该应用层就是虚拟机。不同平台的虚拟机可以载入和执行同一种平台无关的字节码。
1.2、语言无关性
所谓语言无关性,就是不管你的开发语言是什么,只要你的机器安装了虚拟机,这种语言就能在虚拟机的帮助下,在你的pc机上正常运行,而不仅仅针对java语言。目前一大批已经可以在java虚拟机上运行的语言,如 Scala, Groovy等等。现在的舍友有2个就是用Scala语言开发的。而且,从JDK1.7-JDK1.8开始,JVM设计者们就通过JSR-292基本实现了语言无关性。
1.3、字节码
实质:程序的一种存储格式。将源程序编译为字节码格式存储。每次程序需要执行时,再将该字节码文件取到虚拟机中,利用虚拟机的字节码指令集进行解析,从而完成执行。能够实现语言无关性,就是将源程序都编译成对应的字节码文件,然后由虚拟机解析执行。
1.4总结:虚拟机执行的是字节码,各平台存储程序用的是字节码格式。所以说:字节码是平台无关性和语言无关性的基石。就像本章开头说的:代码编译结果从本地机器码转为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
注:我在这里一直有个疑问,就是JVM解析字节码是没错的,那么解析完后是不是就变成1010这样的机器码了呢,因为计算机能执行的只有1010这样的机器码啊。后来查了一下,大概有这样的解释说:JVM本身是C写的,JVM解析字节码后在C中运行,JVM并不会将字节码解析为1010这样的机器码。所以说最终解析为机器执行的1010这样的机器码肯定不是JVM做的。
2、Class类文件的结构
2.1、认识结构前的铺垫
★既是存储结构(伪结构),那么就该有对应的数据类型去组成其存储结构。Java虚拟机中,有且仅有2种数据类型:(1)无符号数(2)表。所有的字节码解析都以这两种数据类型为基础。
★无符号数:有4类:u1,u2,u4,u8,分别代表1个字节,2个字节,4个字节,8个字节。
无符号数可以用来描述:数字、索引引用、数量值、按照utf-8编码构成的字符串值。
★表:由无符号数or其他表作为数据项构成的复合数据类型。
表都习惯以_info结尾。
用于描述:有层次关系的复合结构的数据。
整个Class文件本身就是一张表。
★计数器:无论是无符号数or表,当需要描述同一类型但数量不定的多个数据时,经常会用到一个前置的容量计数器加若干个连续数据项的形式。
★Class文件特征:
A、以8位二进制为基础单位的二进制字节流。
B、各个数据项严格按照顺序紧凑排列在class文件中,中间没有任何分隔符。故class文件中内容基本都是需要运行的,没有没用的,也没有空隙。
c、当遇到数据项大小超过8个二进制位时,会按照高位在前的方式分割成多个基础单位进行存储。
2.2、真正认识其结构
总的整体格式序列:<魔数,次版本号,主版本号,常量池,访问标志,类索引,父类索引,接口索引集合,字段表集合,方法表集合,属性表集合>
★魔数:Class文件的头4个字节。
A、作用:唯一作用是:确定这个Class文件能否被虚拟机接受。
B、为什么使用魔数而不是使用扩展名进行身份认证:基于安全性考虑。因为扩展名可以随便改动。魔数存到了Class文件中,而扩展名是我们可以看到的。
★次版本号、主版本号:均为2个字节。依次对应5、6字节,7、8字节。
A、作用:二者联合起来,表示此Class文件能运行的最低JDK版本号。如第5,6字节为0x0000,7、8字节为0x0032。查找版本号对照表,发现十六进制版本号00000032对应的十进制版本号为50.0,对应的JDK版本号为1.6。故这个Class文件可以被JDK1.6或以上版本的虚拟机执行。
★常量池[比较复杂]
A、地位(4个):是Class文件的资源仓库;是Class文件结构中与其他项目关联最多的数据类型;是占用Class文件空间最大的数据项目之一;是Class文件中第一个出现的表类型数据项目。
B、格式:u2类型的容量计数值+计数值个常量。
C、存放内容:字面量、符号引用2类。
注1:字面量~就是java语言层面的常量概念,比如文本字符串or声明为final的变量等。
注2:符号引用~有3类:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符。
注2.1:全限定名:类的绝对路径
注2.2:简单名称:对于字段就是:没有类型的字段名;对于方法就是:没有返回类型、没有参数的方法名。
注2.3: 描述符:对于字段:就是字段类型;对于方法就是:返回类型和参数列表(包括数量、类型、顺序)。
D、常量池中内容的存在意义:java动态链接机制使得:在Class文件中不会保存各个方法、字段的最终内存布局信息,故无法直接被虚拟机使用。所有当虚拟机运行时,则需要从常量池中获得对应的符号引用,再在类创建活着运行时解析、翻译到具体的内存地址。
E、常量存储结构:每一项常量都是一张表。共有14中常量类型,每种常量类型对应的表结构都不一样。但所有表唯一共同点就是:表第一位是一个u1类型的标志位(tag),表示了当前这个常量是哪种常量类型。
F、Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称。所以CONSTANT_Utf8_info型常量的最大长度也就是java中方法、字段名的最大长度。如果java程序中定义了超过64KB英文字符的变量或者方法名,将会无法编译。
G、自动生成的常量:在进行javac编译后,查看字节码,会看到生成了一些在java源程序里面没有定义过的常量。这些常量因为其不方便使用“固定字节”进行表达,但可能会被方法表、字段表、属性表引用到。这些常量可能是:方法返回值,方法参数个数,参数类型等等。所以当你看到这些变量时,不要因为在源码中找不到而困惑就好。
★访问标志
A、长度:2字节。但目前只有8个标志位被使用。
B、作用:用于识别一些类或者接口层次的访问信息。包括:这个class是类还是接口;十分定义为public;是否定义为abstract类型;如果是类,是否被定义为final类型等等。
★类索引、父类索引、接口索引集合
A、长度:2字节,2字节,2字节+
B、功能:Class文件用这3项来确定这个类的继承关系。
C、类索引:用于确定这个类的全限定名
D、父类索引:用于确定这个类的父类的全限定名。
E、接口索引集合:用于描述这个类实现了哪些类。排列顺序为:代码中implements后面的接口顺序。
F、接口索引集合结构:2字节接口计数器+接口索引表。
★字段表集合
A、功能:用于表述接口或类中声明的变量。
B、字段:包括类级变量、实例级变量。不包括局部变量。
C、格式:2字节容量计数器+各个字段
D、是否有继承的字段:不会列出从超类或者父接口中继承而来的字段。但有可能列出java代码中不存在的字段。
★方法表集合
A、存储格式和字段表基本一样。
B、是否有父类方法描述:如果在子类中没有复写父类方法,则不会出现来自父类方法的信息。同样,可能会出现由编译器自动添加的方法。最典型的被添加的方法是:类构造器<clinit>方法;实例构造器<init>方法。
★属性表集合
A、存在位置:字段表、方法表、Class文件中。
B、功能:用于描述某些场景出现的专有的信息。
C、属性表限制宽松:各个属性表顺序不要求严格有序;
D、属性表中关键常用11个属性:
D1: Code属性
使用位置:方法表
功能:java方法体内代码,在javac编译后,最终形成的字节码指令存储在Code属性内。
地位:是Class文件中最重要的一个属性。如果把java程序中信息分为代码(java方法体内的代码)和元数据(包括类、字段、方法定义,其他信息)。则整个Class文件中,Code属性用于描述代码,所有其他数据项都用于描述元数据。
异常表:是方法的显示异常处理表集合,对于Code属性来说不是必须存在的。它的实质是:java代码的一部分,编译器使用异常表而不是跳转指令来实现java异常及finally处理机制。
D2:Exceptions属性
使用位置:方法表
作用:列举出方法中可能抛出的受查异常。也就是throws后面列举的异常。
注意:和Code属性中的异常表完全不是一回事。存在位置和功能都是不一样的。
D3:LineNumberTable属性
使用位置:Code属性
作用:用于描述java源码和字节码行号之间的对应关系。
地位:非运行时必须的属性。但是会默认生成到Class文件中。
D4: LocalVariableTable属性
使用位置:Code属性
作用:用于描述栈帧中,局部变量表中的变量与java源码中定义的变量的关系。
地位:不是运行时必须的属性。但默认会生成到Class文件中。
D5: SourceFile属性
使用位置:类文件
作用:记录生成这个Class文件的源码文件名称。
地位:非必需存在。
D6: ConstantValue属性
使用位置:字段表
作用:通知虚拟机自动为静态变量赋值。只有类变量可以使用此属性。
初始化时间:实例变量:在实例构造器<init>方法中进行;
类变量:有2种方式:变量被final和static修饰,则生成ConstantValue属性进行初始化;如果没有被final修饰,或者并非基本类型及字符串,则会在<clinit>方法中进行初始化。
D7: InnerClasses属性
使用位置:类文件
作用: 用于记录内部类和宿主类之间的关联。
D8: Deprecated及Synthetic属性
使用位置:类、方法表、字段表
作用:这两个属性都属于标志类型的布尔属性,只存在有没有的区别,没有属性值的概念。
@Deprecated注解表示:该类、字段、方法已经被程序作者定为不再推荐使用
Synthetic属性代表此字段、方法并不是由java源码直接产生的,而是由编译器自行添加的。
D9: StackMapTable属性
使用位置:Code属性表的属性表中
作用:这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用。目的是:替代以前比较消耗性能的基于数据流分析的类型推导验证器。
加入JDK时间:1.6开始
工作原理:在编译阶段将一系列的验证类型直接记录在Class文件中,通过检查这些验证类型替代了类型推导过程,从而大幅提升了字节码验证功能。而曾经原始的字节码验证过程是:在运行期通过数据流分析去确认字节码的行为逻辑合法性。
D10: Signature属性
使用位置:类、方法表、字段表
作用:记录“类、接口、方法、成员的泛型签名”的泛型签名信息。为什么要用属性记录泛型类型,因为java语言的泛型采用的是擦除法实现的伪泛型。Java 反射API能够获取泛型类型,最终的数据来源就是这个属性。
地位:非必需存在
D11:BootstrapMethods属性
使用位置:类文件的属性表中
使用时间:JDK1.7开始
作用:用于保存invokedynamic指令引用的引导方法限定符。
3、字节码指令
pc机有自己的机器指令集,虚拟机有自己的字节码指令集。
3.1、长度:1字节
3.2、格式:操作码+操作参数
3.3、指令特点:大多数指令只有操作码,没有操作数。因为java虚拟机采用面向操作数栈而不是寄存器的架构。
3.4、大多数指令都包含了其操作对应的数据类型信息。并非每种数据类型都有一种对应的指令。大多数对于boolean,byte,short,char类型的数据操作,实际上都是使用相应的int类型作为运算类型。
3.5、9类字节码的指令用法
★加载和存储指令
功能:将数据在栈帧中的局部变量表和操作数栈直接来回传输。
★运算指令
功能:对于两个操作数栈上的值进行某种特定运算,并把结果重新存到操作数栈顶。
★类型转换指令
功能:将两种不同的数值类型相互转换。这种操作一般用于用户代码中的显式转换,or 用来处理字节码指令集中数据类型相关指令无法和数据类型一一对应的问题。
★对象创建与访问指令
这里是比较重要的。因为java虚拟机中对象创建和数组创建是不同的字节码指令。
★操作数栈管理指令
功能:直接操作操作数栈上面的数
★控制转移指令
功能:可以让java虚拟机有条件or无条件从指定的位置指令继续程序程序,而无需控制转移指令。实质就是:有条件or无条件的修改PC寄存器的值。
★方法调用和返回指令
★异常处理指令
注意区分:java程序中,显示抛出异常throw,最终由athrow指令完成。
而在java虚拟机中,处理异常(catch)不是由字节码指令来实现的,而是用code属性中的异常表来完成的。
★同步指令
包括2类:方法级同步、方法内部一段指令序列的同步
◆方法级同步:隐式的,不需要字节码控制。
实现位置:方法调用和返回操作之间。
◆同步一段指令序列:通常有java语言中的synchronized语句块来表示。