Java语言:跨平台的语言(write once,run anywhere),当Java源代码成功编译成字节码后,如果想在不同的平台上面运行, 则无须再次编译,这个优势不再那么吸引人了。Python、PHP、Perl、Ruby、Lisp等有强大的解释器,跨平台似乎已经快成为一门语言必选的特性
Java虚拟机:跨语言的平台
Java虚拟机不和包括Java在内的任何语言绑定,它只与.class文件这种特定的二进制文件格式所关联,无论使用何种语言进行软件开发, 只要能将源文件编译为正确的.class文件,那么这种语言就可以在Java虚拟机上执行,可以说统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁,所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的, 这样一来字节码文件可以在各种JVM上进行。
想要让一个Java程序正确地运行在JVM中,Java源码就是必须要被编译为符合JVM规范的字节码。
源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种十六进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成本地机器码
Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的操作码 (opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码,由于指令只有一个字节大小,所以最多只有256个,对于添加新的指令要非常小心,超过256个可就完蛋了
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说Class文件实际上它并不一定以磁盘文件形式存在。Class文件是一组以字节为基础单位的二进制流,在内存中的表现形式为字节数组
常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用,随着Java虚拟机的不断发展,常量池的内容也日渐丰富,可以说常量池是整个Class文件的基石,在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。
在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息
这三项数据来确定这个类的继承关系以及实现的接口,就是用来描述类的全限定名、父类是谁、实现了多个个接口,具体是哪些接口
fields_count的值表示当前Class文件fields表的成员个数。使用两个字节来表示
fields表中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述
Code属性就是存放方法体里面的代码,但是并非所有方法表都有Code属性,像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了
常见的LineNumberTable属性表
常见的LocalVariableTable属性表
Class文件其实就是对类的整体描述,类有哪些字段?有哪些方法?类的全限定名?类的父类是谁?类实现的接口有哪些?方法中具体的方法体是什么?类的访问权限和修饰符等,Class文件中有一块非常重要的内容就是常量池,通过上面的分析可以得知,常量池中存储的符号引用,其他描述的地方引用常量池中的符号引用,最后都定位到字面量(字符串、基本数据类型)。
cafe babe 0000 0034 0016 0a00 0400 1209
0003 0013 0700 1407 0015 0100 036e 756d
0100 0149 0100 063c 696e 6974 3e01 0003
2829 5601 0004 436f 6465 0100 0f4c 696e
654e 756d 6265 7254 6162 6c65 0100 124c
6f63 616c 5661 7269 6162 6c65 5461 626c
6501 0004 7468 6973 0100 184c 636f 6d2f
6174 6775 6967 752f 6a61 7661 312f 4465
6d6f 3b01 0003 6164 6401 0003 2829 4901
000a 536f 7572 6365 4669 6c65 0100 0944
656d 6f2e 6a61 7661 0c00 0700 080c 0005
0006 0100 1663 6f6d 2f61 7467 7569 6775
2f6a 6176 6131 2f44 656d 6f01 0010 6a61
7661 2f6c 616e 672f 4f62 6a65 6374 0021
0003 0004 0000 0001 0002 0005 0006 0000
0002 0001 0007 0008 0001 0009 0000 0038
0002 0001 0000 000a 2ab7 0001 2a04 b500
02b1 0000 0002 000a 0000 000a 0002 0000
0007 0004 0008 000b 0000 000c 0001 0000
000a 000c 000d 0000 0001 000e 000f 0001
0009 0000 003d 0003 0001 0000 000f 2a2a
b400 0205 60b5 0002 2ab4 0002 ac00 0000
0200 0a00 0000 0a00 0200 0000 0b00 0a00
0c00 0b00 0000 0c00 0100 0000 0f00 0c00
0d00 0000 0100 1000 0000 0200 11
字节码指令对于虚拟机,就好像汇编语言对于计算机,属于基本执行命令,字节码指令由一个字节长度的,代表着某种特定操作含义的数字(称为操作码:Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数:Operands)构成,由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码,由于限制了操作码的长度为一个字节(即0 ~ 255),这意味着指令集的操作码总数不可能超过256条 。
官方文档:
如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解
在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息,例如,iload指令用于从局部变量表中加载int类型的数据到操作数栈中,而fload指令加载的则是float类型的数据
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型,还有一些指令,比如无条件跳转指令goto则是与数据类型无关。
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递
将局部变量表中对应索引位置的变量压入操作数栈栈顶
指令格式:
弹出操作数栈栈顶元素,存储到局部变量表对应索引处,给局部变量赋值
算术指令用于两个操作数栈上的值进行某种特定运算,并且把计算后的结果重新压入操作数栈
Java虚拟机中没有直接支持byte、short、char和boolean类型的算术指令,而是使用int类型的指令来代替处理,在处理这些类型的数组时,也会转换成对应的int类型的字节码指令来处理。
数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数(补码),其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时会导致虚拟机抛出异常ArithmeticException
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持,有一系列指令专门用于对象操作,可进一步细分为创建指令、 字段访问指令、数组操作指令和类型检查指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令
上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也很高
数组操作指令主要有:xastore和xaload指令,其中x为具体的数据类型
xaload指令:
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、 iastore、lastore、fastore、dastore、aastore
方法调用指令主要有5个:invokevirtual、invokeinterface、invokespecial、invokestatic、invokedynamic
方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的,包含xreturn和return指令。
通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃,如果当前返回的是synchronized方法,那么还会执行一 个隐含的monitorexit指令,退出临界区,最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。
如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令
将一个或两个元素从栈顶弹出,并且直接废弃
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶
将栈最顶端的两个Slot数值位置交换:swap,Java虚拟机没有提供交换两个64位数据类型(long、double)数值的指令
指令nop是一个非常特殊的指令,它的字节码为0x00,和汇编语言中的nop一样,它表示什么都不做,这条指令一般可用于调试、占位等
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为比较指令、条件跳转指令、比较条件跳转指令、多条件分支跳转指令、无条件跳转指令等
Java虚拟机支持两种同步结构:方法级同步和方法内部一段指令序列的同步,这两种同步都是使用monitor(本质上是对象中的对象头支持的)来支持的
方法级的同步:是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中
虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法,当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程将先持有同步锁,然后执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁,在方法执行期间,执行线程持有了同步锁,其它任何线程都无法再获得同一个锁(底层依赖操作系统的互斥锁mutex),如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常, 那么这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。
这段代码和普通的无同步操作的代码没有什么不同,没有使用monitorenter和monitorexit进行同步区控制,这是因为,对于同步方法而言,当虚拟机通过方法的访问标识符判断是一个同步方法时,会自动在方法调用前进行加锁,当同 步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁,因此,对于同步方法而言,monitorenter和monitorexit指令是隐式存在的,并未直接出现在字节码中。