Java具有跨平台的特性,可以一次编译,到处运行。在一个平台下编写的程序,无需任何改变就可以在另一个平台下运行。跨平台是如何实现的?答案就是通过JVM (Java Virtual Machine)。我们编写.java文件,编译后会生成一种.class文件,称为字节码文件(byte code)。JVM就是负责将字节码文件翻译成特定平台下的机器码然后运行。也就是说,只要在不同平台上安装对应的JVM,就可以运行字节码文件,达到运行我们编写的Java程序的目的。JVM在这个过程中扮演一个中间件的角色。
Java规范包括两部分:Java语言规范和Java虚拟机规范。实现语言无关性的基础是虚拟机和字节码储存格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干辅助信息。
使用Java编译器可以将Java代码编译成Class文件,使用JRuby等其他语言的编译器也可以将程序编译成Class文件,虚拟机并不关心Class文件的来源是何种语言。
Java语言中的各种变量,关键字和运算符号的语义最终都是由多条字节码命令组合而成。
Class文件是一组以8位字节为基础单位的二进制流,它不含任何分隔符,格式采用一种类似于C语言结构体的伪结构来储存数据,这种伪结构只有两种数据类型:无符号表和表。
- 无符号表 - 基本的数据类型,以u1, u2, u4, u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值。
- 表 - 由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以_info结尾。表用于描述有层次关系的复合结构数据,整个Class文件本质上就是一张表。每一个 Class 文件对应于一个如下所示的结构体:
每个数据项对应具体信息如下:
TestClass.java -> TestClass.class
源文件如下,TestClass.java:
package com.simonwoo.clazz;
public class TestClass{
private int m;
public int inc() {
return m + 1;
}
}它对应的字节码如下:
cafe babe 0000 0032 0018 0700 0201 001e 636f 6d2f 7465 7374 2f64 6f63 2f65 7870
2f54 6573 7443 6c61 7373 436f 6465 0700 0401 0010 6a61 7661 2f6c 616e 672f 4f62
6a65 6374 0100 0b61 7474 7269 6275 7465 5f31 0100 124c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b01 000b 6174 7472 6962 7574 655f 3201 0013 4c6a 6176 612f 6c61
6e67 2f49 6e74 6567 6572 3b01 0006 3c69 6e69 743e 0100 0328 2956 0100 0443 6f64
650a 0003 000d 0c00 0900 0a01 000f 4c69 6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65 0100 0474 6869 7301 0020 4c63 6f6d
2f74 6573 742f 646f 632f 6578 702f 5465 7374 436c 6173 7343 6f64 653b 0100 0f74
6573 7449 6e74 6572 6661 6365 5f31 0100 0f74 6573 7449 6e74 6572 6661 6365 5f32
0100 2628 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 294c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b01 0005 7061 7261 6d01 000a 536f 7572 6365 4669 6c65 0100
1254 6573 7443 6c61 7373 436f 6465 2e6a 6176 6100 2100 0100 0300 0000 0200 0200
0500 0600 0000 0400 0700 0800 0000 0300 0100 0900 0a00 0100 0b00 0000 2f00 0100
0100 0000 052a b700 0cb1 0000 0002 000e 0000 0006 0001 0000 0003 000f 0000 000c
0001 0000 0005 0010 0011 0000 0001 0012 000a 0001 000b 0000 002b 0000 0001 0000
0001 b100 0000 0200 0e00 0000 0600 0100 0000 0b00 0f00 0000 0c00 0100 0000 0100
1000 1100 0000 0100 1300 1400 0100 0b00 0000 3600 0100 0200 0000 022b b000 0000
0200 0e00 0000 0600 0100 0000 0e00 0f00 0000 1600 0200 0000 0200 1000 1100 0000
0000 0200 1500 0600 0100 0100 1600 0000 0200 17javap是一个专门用于分析Class文件字节码的工具,javap -verbose TestClass命令输出文件字节码内容:
Compiled from "TestClass.java"
public class com.simonwoo.clazz.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/simonwoo/clazz/TestClass.m:I
#3 = Class #17 // com/simonwoo/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/simonwoo/clazz/TestClass
#18 = Utf8 java/lang/Object
{
public com.simonwoo.clazz.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
}
SourceFile: "TestClass.java"每个Class的文件的前四个字节称为魔数,它的唯一作用是确定这个文件是否为一个被虚拟机接受的Class文件。 紧接着魔数的4个字节储存的是Class文件的版本号:5,6字节是次版本号(Minor Version),7,8字节是主版本号(Major Version)。
接着主次版本号之后是常量池的入口,由于常量池中常量是不固定的,所以在常量池的入口需要放入一个u2类型的数据,代表常量池容量计数值(constant_pool_count)。常量池中主要存在两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量为Java常量,如文本字符串,声明为final的常量值等。符号引用则属于编译原理方面的概念,包括以下三种:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
在Class文件中不会保存各个方法,字段的最终内存布局信息,因此这些字段,方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应符号的引用,再在类创建时或运行时解析,翻译到内存地址等。
在常量池结束后,紧接着的两个字节代表访问标志(access_flags),这个标志用于标示一些类或者接口层次访问信息,包括:这个Class是类还是接口;是否为public类型;是否定义为abstract类型;如果是类的话,是否声明为final等。
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据集合,Class文件是由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。类索引,父类索引和接口索引集合都按照顺序排列在访问标志之后。
字段表(field_info)用于描述接口或者类中声明的变量。字段包括类型变量以及实例变量,但不包括方法内部的局部变量。可包含的信息有:字段的作用域(public, private, protected),实例变量或类变量(static修饰符),可变性(final),并发可见性(volatile),可否被序列号(transient),字段数据类型(基本类型,对象,数组),字段名称。字段表的格式如下:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| u2 | attributes | attributes_count |
access_flags与类中的十分相似,都是一个u2数据类型。 name_index和descriptor_index都是对常量池的引用,分别代表着字段的简单名称,字段和方法的描述符。 简单名称是指没有类型和参数修饰的方法或者字段名称,如inc,m。 描述符的作用是用来描述字段的数据类型,方法的参数列表(包含数量,类型以及顺序)和返回值。根据规则,基本数据类型以及无返回值void类型都用一个大写字符来标示。
| 标示字符 | 含义 |
|---|---|
| B | byte |
| C | char |
| D | double |
| F | float |
| I | int |
| J | long |
| S | short |
| Z | boolean |
| V | void |
| L | 对象类型,如Ljava/lang/Object |
用描述符描述方法时,按照县参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号之内。 字段表集合不会列出从超类或者父接口中继承而来的字段,但是有可能列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
Class文件存储格式中对方法的描述与对字段的描述几乎完全一致,方法表的结构也与字段表一致。与字段表相对应,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的信息。但同样的,有可能会出现有编译器自动添加的方法,最典型的便是类构造器clinit方法和实例构造构造器init方法。
属性表(attribute_info)在前面已经出现过多系,在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。Java虚拟机规范中预定义了9项虚拟机应当能识别的属性(JDK1.5 后又增加了一些新的特性,因此不止下面 9 项,但下面 9 项是最基本也是必要,出现频率最高的),如下表所示:
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量表来表示,而属性值的结构则是完全自定义的,只要说明属性值所占用的位数长度即可。一个符合规则的属性表应该满足如下表定义的结构:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | attribute_name_index | 1 |
| u2 | attribute_length | 1 |
| u1 | info | attribute_length |
Java程序方法体里的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有方法都必须存在这个属性表,譬如接口或抽象类中的抽象方法就不存在Code属性,如果方法有Code属性表存在,那么它的结构如下表:
attribute_name_index是一项指向CONSTANT_Utf8_info常量表的索引,常量值固定为“Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节。- max_stack代表操作数栈(Operand Stacks)的最大深度。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Frame)中的操作数栈深度。
- max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量表分配内存所使用的最小单位。对于byte,char,float,int,shot,boolean,reference和returnAddress等长度不超过32位的数据类型,每个局部变量占1个Slot,而double与long这两种64位的数据类型而需要2个Slot来存放。方法参数(包括实例方法中的隐藏参数“this”),显示异常处理器的参数(Exception Handler Parameter,即try-catch语句中catch块所定义的异常),方法体中定义的局部变量都需要使用局部表来存放。另外,并不是在方法中使用了多个局部变量,就把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所在的Slot就可以被其他局部变量所使用,编译器会根据变量的作用域来分类Slot并分配给各个变量使用,然后计算出max_locals的大小。
- code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然名为字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可相应地找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应该如何理解。关于code_length还有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到2的32次方减1,但虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过这个限制,Javac编译器就会拒绝编译。一般来讲,只要我们写Java代码时不是刻意地编写超长的方法,就不会超过这个最大值限制。但是,在编译复杂的JSP文件中,可以会因为这个原因导致编译失败。
Code属性是Class文件中最重要的一个属性,如果表一个Java程序中的信息分为代码(Code,方法体里的Java代码)和元数据(Metadata,包括类、字段、方法定义及其它信息)两部分,那么在整个Class文件里,Code属性用于描述代码,其它的所有数据项目就都用于描述元数据。 在字节码指令之后的是这个方法的显示异常处理表,异常表对于Code属性表来说不是必须存在的。异常表的格式如下表:
异常表它包含4个字段,这些字段的含义为:如果字节码从第start_pc到end_pc行之间(不包含第end_pc)行出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任何的异常情况都需要转向到handler_pc行行进行处理。异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。注:字节码的“行”是一种形象的描述,指的是字节码相对于方法体开始的偏移量,而不是Java源代码的行号。
.png)


.jpeg)
.png)