Java具有平台无关性,即任何操作系统都能运行Java代码。 之所以能实现这一点,是因为Java运行在虚拟机之上,不同的操作系统都拥有各自的Java虚拟机,因此Java能实现"一次编写,处处运行"。
而JVM不仅具有平台无关性,还具有语言无关性:
但JVM对能运行的语言是有严格要求的。首先来了解下Java代码的运行过程: Java源代码首先需要使用Javac编译器编译成class文件,然后启动JVM执行class文件,从而程序开始运行。 即JVM只认识class文件,它并不管何种语言生成了class文件,只要class文件符合JVM的规范就能运行。
因此目前已经有Scala、JRuby、Jython等语言能够在JVM上运行。它们有各自的语法规则,不过它们的编译器都能将各自的源码编译成符合JVM规范的class文件,从而能够借助JVM运行它们。
Class文件是JVM的输入, Java虚拟机规范中定义了Class文件的结构。Class文件是JVM实现平台无关、技术无关的基础。
class文件包含Java程序执行的字节码,数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符。 文件开头有一个0xcafebabe(16进制)特殊的一个标志。
class文件是一组以8字节为单位的二进制字节流,对于占用空间大于8字节的数据项,按照高位在前的方式分割成多个8字节进行存储。 它的内容具有严格的规范,文件中没有任何分隔符,全是连续的0/1。
class文件中的所有内容被分为两种类型:
javap工具生成非正式的"虚拟机汇编语言” ,格式如下:
<index> <opcode> [<operand1> [<operand2> ...]][<comment>]
<index>
是指令操作码在数组中的下标,该数组以字节形式来存储当前方法的Java虚拟机代码;也可以是相对于方法起始处的字节偏移量<opcode>
是指令的助记码< operand>
是操作数<comment>
是行尾的注释ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
版本号规则: JDK5,6,7,8 分别对应49,50,51,52
class文件的头4个字节称为魔数,唯一作用是确定这个文件是否为一个能被JVM接受的Class文件. 作用就相当于文件后缀名,只不过后缀名容易被修改,不安全. 是用16进制表示的"CAFEBABE".
紧接着魔数的4个字节是版本号.它表示本class中使用的是哪个版本的JDK. 在高版本的JVM上能够运行低版本的class文件,但在低版本的JVM上无法运行高版本的class文件.
紧接着版本号之后的就是常量池。常量池中存放两种类型的常量:
根据常量的数据类型不同,被细分为14种常量类型,都有各自的二维表示结构 每种常量类型的头1个字节都是tag,表示当前常量属于14种类型中的哪一个.
以CONSTANT_Class_info常量为例,它的二维表示结构如下: CONSTANT_Class_info表
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
CONSTANT_Utf8_info表字符串常量
Java中定义的类、变量名字必须小于64K 类、接口、变量等名字都属于符号引用,它们都存储在常量池中 而不管哪种符号引用,它们的名字都由CONSTANT_Utf8_info类型的常量表示,这种类型的常量使用u2存储字符串的长度 由于2字节最多能表示65535个数,因此这些名字的最大长度最多只能是64K
UTF-8编码 VS 缩略UTF-8编码 前者每个字符使用3个字节表示,而后者把128个ASCII码用1字节表示,某些字符用2字节表示,某些字符用3字节表示。
invokeinterface | 用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。(Invoke interface method) |
invokevirtual | 指令用于调用对象的实例方法,根据对象的实际类型进行分派(Invoke instance method; dispatch based on class) |
invokestatic | 用以调用类方法(Invoke a class (static) method ) |
invokespecial | 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。(Invoke instance method; special handling for superclass, private, and instance initialization method invocations ) |
invokedynamic JDK1.7新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在JVM内部,而invokedynamic则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。(Invoke dynamic method)
在常量池结束之后是2字节的访问控制 表示这个class文件是类/接口、是否被public/abstract/final修饰等.
由于这些标志都由是/否表示,因此可以用0/1表示. 访问标志为2字节,可以表示16位标志,但JVM目前只定义了8种,未定义的直接写0.
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_INTERFACE | 是一个接口,而不是一个类 | |
ACC_MODULE | 声明的模块; 可能无法从其模块外部访问。 仅当ClassFile具有Module属性时才可以设置。 | |
ACC_STATIC | 0x0008 | 声明为静态 |
Demo1这个示例中,我们并没有写构造函数。
由此可见,没有定义构造函数时,会有隐式的无参构造函数
表示当前class文件所表示类的名字、父类名字、接口们的名字. 它们按照顺序依次排列,类索引和父类索引各自使用一个u2类型的无符号常量,这个常量指向CONSTANT_Class_info类型的常量,该常量的bytes字段记录了本类、父类的全限定名. 由于一个类的接口可能有好多个,因此需要用一个集合来表示接口索引,它在类索引和父类索引之后.这个集合头两个字节表示接口索引集合的长度,接下来就是接口的名字索引.
用于存储本类所涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。 每一个字段表只表示一个成员变量,本类中所有的成员变量构成了字段表集合。
成员变量(包括静态成员变量和实例变量) 和 方法都有各自的描述符。 对于字段而言,描述符用于描述字段的数据类型; 对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。
在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。 描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且,参数之间无需任何符号。
在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。 方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有所不同。
方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译过后的字节码指令。
stack :方法执行时,操作栈的深度 Locals :局部变量所需的存储空间,单位是slot
我们将JVM运行的核心逻辑进行了详细剖析。
JVM运行原理中更底层实现,针对不同的操作系统或者处理器,会有不同的实现。 这也是JAVA能够实现“
一处编写,处处运行
”的原因。 开发人员理解到这个层次,就足够掌握高深的多线程
参考