跨平台的实现
Java诞生之初提出一个口号"一次编写,到处运行"。与平台无关的思想最终实现在操作系统的应用层上:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现一次编写,到处运行。
实现语言无关性(java虚拟机上可以运行Scala等其他语言)仍然是虚拟机和字节码的存储格式.
java 虚拟机不和任何语言绑定,只是和Class文件这种特定的二进制文件格式关联。Class文件包含了java虚拟机指令集和符号表,等信息.也就是java虚拟机可以支持其他语言,只要能编译为class格式的文件就行
Class 文件的 结构
任何一个Class文件都对应着唯一一个类或者接口的信息,但是反过来说类或者接口的信息并不一定都得定义在文件中(类或者接口的信息也能够通过类加载器生成),任意一个有效的类或接口应当满足的格式称为"Class文件格式",并不一定以磁盘文件存在
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件之中,没有任何分隔符
,如果占用的空间不够,则会按照big-endian分隔为多个8位字节进行存储
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
表是有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯地以_info结尾。表用于描述有层次关系的复合结构的数据,整个class文件本质上就是一张表
下面看下class文件的一个结构图
最后,强调下, 由于Class的结构不像xml等描述语言,没有任何的分隔符,所以class文件结构中的数据项中,无论是顺序还是数量,甚至是存储的字节序(Class文件中存储的字节序是big-endian),都是严格规定的,下面按照这表来介绍下文件结构
魔数和class文件的版本号
从上面的表看出,class文件的头4个字节为magic,魔数,确定这个文件是否是java虚拟机可以接受的class文件,后面的4个字节,minor_version表示的是次版本号,major_version表示的是主版本号
常量池
从上面的表看出,后面存放的常量计数器和表(也就是常量池),常量池中主要存储两大类常量:
字面量(Literal)和符号引用(Symbolic References),字面量比较接近于java语言常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了三类常量:
1> 类和接口的全限定名
2> 字段的名称和描述符
3> 方法的名称和描述符
java代码在进行javac编译的时候,是在虚拟机加载Class文件的时候进行动态连接,也就是说,Class文件中不会保存各个方法、字段的最终内存布局信息,如果不经过运行期转换无法得到真正的内存入口地址。
也就是当虚拟机运行时候,需要从常量池中获取对应的符号引用,再在类创建时或运行时解析翻译找到具体的内存地址。
什么是符号引用,什么是直接引用?
符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能定位到目标即可,符号引用和虚拟机的内存布局无关,符号引用存在引用的目标不一定加载到了内存中
直接引用:直接引用是一个能定位到目标的指针、偏移量或者句柄。直接引用和虚拟机的内存布局相关,只要直接引用存在,目标一定加载到了内存中
常量池的每一个常量都是一个表,jdk1.7中新增了3个类型,常量池的常量类型如下:
在CONSTANT_Class_info中name_index是一个索引值,指向常量池中的一个CONSTANT_Utf8_info类型常量,这个常量表示类或者接口的全限定名,这里name_index值:0x0002,指向了常量池中第二个常量,就是CONSTANT_Utf8_info,标识位是0x01,由此可以确定name_index是指向常量池中的CONSTANT_Utf8_info类型常量
length值说明了这个UTF-8编码的字符串长度是多少个字节,后面紧跟的长度为length字节的连续数据是一个使用utf-8略码标识的字符串。
utf-8缩略编码和普通utf-8编码的区别是:从 \u001到\u007的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从\u0080到\u07ff之间的字符使用2个字节表示
由于Class文件中方法、字段等都需要引用CONSTATN_Utf8_info描述常量,这个常量的最大长度是length的长度,u2类型最大值为65535,java程序中如果定义了超过了64kb的变量或者方法名,将无法编译
我们使用javap来看下Test类编译字节码信息,class文件中常量池的信息如下:
d:\>javap -verbose Test.class
Classfile /d:/Test.class
Last modified 2018-4-12; size 275 bytes
MD5 checksum 79ebe8a03c66bed1910b536bcec097ee
Compiled from "Test.java"
public class com.test.Test
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/test/Test.m:I
#3 = Class #17 // com/test/Test
#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 getM
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/test/Test
#18 = Utf8 java/lang/Object
{
从这里看出有18个常量信息,#表示保存的是索引信息(utf8表示索引指向的是字符串信息),后面的表示指向的内容
访问标志
常量池后面的两个字节表示的是访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括这个class是类还是接口,是否定义为public类型;是否定义为abstract类型,如果是类的话,是否定义为final
如果有一个普通的java类,是public,但是不是final和abstract的,这样只有上面的两个标志位
acc_public ,acc_super ,结果就是这个类的标识位access_flags = 0x0021
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
字段表集合
字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但是不包括在方法内部声明的局部变量。
包括的信息有:字段的作用域,是实例变量还是类变量,可变性(final),并发可见性(volatile修饰符,是否强制从主内存中读写),是否可以被序列化(transient修饰符),字段数据类型、字段名称。
而字段名称、字段的类型需要引用常量池中的符号引用来描述
access_flags和类文件结构中的含义类似,表示访问标志,是否是public,abstract,final等
name_index:常量池中名称的引用,代表字段的简单名称
descriptor_index:常量池中描述的引用,代表字段和方法的描述符
通过name_index和descriptor_index就可以确定,字段名和描述符 比如 private int m 的信息
解释下一些名词,简单名称,方法和字段的描述符
例如:
全限定名:org/fengxiao/test/Mytest 就是这个类的全限定名
方法test(),int m =0;字段的简单名称就是 test,m
描述符的作用:描述字段的数据类型、方法的参数列表和返回值,基本类型、void和对象类型都使用一个字母表示
注意:
1. 对于数组类型,每一个维度将使用一个前置的[符号来描述,如定义为java.lang.String[][]类型的数组,将会被记录为[[Ljava/lang/String,一个整型数组int[] 将记录为[I
2. 用描述符来描述方法时候,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()内,
例如:void () 描述符表示为 ()V
java.lang.String toString() 描述符表示为()L java/lang/String
int get(int m,String flag, double n) 描述符为(ILD)I
注意
1. 字段表集合不会列出从父类中继承而来的字段,但是可能列出原来java代码中不存在的字段,比如内部类中为了保持
对外部类的访问性,会自动添加外部类的实例字段
2. 在java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码,如果两个字段的描述符不同,字段是允许重名的
方法表集合
方法表的结构如同字段表一样,依次包括了访问标志,名称索引,描述符索引、属性表集合
方法的定义可以通过访问标志、名称索引、描述符所以来表示,但是里面的代码去哪了?方法里的java代码,经过编译器编译恒字节码指令后,存放在方法属性表集合中一个名为"code"的属性里面
与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,但是可能出先有编译器自动添加的方法,最典型的就是类构造器<clinit>和实例构造器<init>方法
在java语言中,要重载Overload一个方法,除了要和原方法具有相同的简单名称之外,还要求必须拥有和原方法不同的方法签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不包含在特征签名,因而无法通过返回值不同完成重载
在class文件格式中,特征签名的范围更大些,只要描述符号不是一致的两个方法也可以共存,比如,如果两个方法具有相同的名称和特征签名,返回值不同,也是可以共存在一个class文件中,只不过不是称为重载
属性表 attribute_info
属性表在class文件中、字段表、方法表中用来描述一些特殊场景下的信息
属性表中不像class文件中其他结构一样,有严格的顺序要求,只要不和已有的属性名重名,虚拟机中识别的部分属性表如下:
属性名称 | 使用位置 | 含义 |
Code | 方法表 | java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exception | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅在一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | java源码行号和字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | jdk1.6中新增的属性,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
每个属性,名称也是需要从常量池中引用一个COSNTANT_Utf8_info类型的常量来表示,属性值的结构是完全自定义的,只需要一个u4长度属性说明属性值占的位数即可,一个符合规则的属性表应该满足下面结构
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
1. code 属性
java方法编译为字节码后,放在code属性中,code属性放在方法表的方法集合中,并非所有的方法表都存在code属性,接口和抽象类方法表不存在code属性,code属性的结构表示如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
*attribute_name_index
指向常量池中CONSTANT_Utf8_info类型的常量的引用,表示名称,固定是"Code"
*max_stack
表示最大的操作数栈的深度,java虚拟机运行时,根据这个来分配每个栈帧的操作数栈的深度
*max_local
表示局部变量表存储的空间,单位是slot
对于byte,short,int,float等不超过32位数据类型使用一个slot来存储,long,double超出了32位,使用两个slot存储
并不是方法区中用到多少局部变量,就将这些变量的总和,作为max_local,因为slot是可以重用的
*code_length、code:
用来存储java源代码编译后生成的字节码指令,code_length代表字节码长度,code是存储字节码指令的一些列字节流
字节码指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令
对于code_length虽然是U4类型的,但是实际上虚拟机规定了方法不能超过65534KB字节,一般情况下,只要不是人为将方法写的很长是不会超过65KB的限制的,但是翻译jsp文件的情况下, 将页面内容放入一个方法内,就可能超出这个限制
note: Code属性是Class文件中重要属性,如果将一个java程序中的信息分为代码(方法体里面的java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)。
上图是实例构造器<init>方法的Code属性,可以看出code字节码为 2A B7 00 0A B1,一个字节一个字节翻译过去就是:
1> 读取2A,查表的0x2A对应的指令为aload_0,这个指令含义是将第0个Slot中为reference类型的本地变量推送到栈顶
2> 读取B7,查表的0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型指定的对象作为方法的接受者,调用此对象的实例构造方法、private方法或者父类的方法
3> 读取 00 0A ,这是invokespecial的参数,查常量池的0x000A对应的常量为实例构造器方法的引用
4> 读取B1,查表0xB1对应的指令为return,含义是返回此方法,返回值是void
TestClass.java
package com.test;
public class TestClass {
private int m;
public int inc(){
return m + 1;
}
}
javap指令翻译的字节码:
public com.test.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<ini
":()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
}
Inc方法中方法参数和局部变量都没有,那么locals,和arg_size为什么是1?
在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象,实现原理是通过javac编译器编译的时候把对this关键字的访问转为对一个普通方法参数的访问,然后虚拟机调用实例方法时候自动传入此参数。
因而在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会存在一个slot位来存放实例引用,方法参数值从1开始计算,如果将Inc方法改为static,那么args_size就是0了
异常表Exception
类型 | 名称 | 数量 | 类型 | 名称 | 数量 |
u2 | start_pc | 1 | u2 | handler_pc | 1 |
u2 | end_pc | 1 | u2 | catch_pc | 1 |
当字节码在start_pc到end_pc之间,出现了类型为catch_type(是指向常量池中类型为CONSTANT_Utf8_info的引用)或子类的异常,就会转到handler_pc执行,当catch_pc值为0,表示任何情况都要转到handler_pc处理
java代码:
public int inc(){
int x;
try{
x=1;
return x;
}catch(Exception e){
x=2;
return x;
}finally{
x=3;
}
}
javap -verbose TestClass.clas 查看字节码执行:
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 //try中x=1
1: istore_1
2: iload_1 //保存x 到returnValue中,此时x=1
3: istore_2
4: iconst_3 //finally块中x=3
5: istore_1
6: iload_2 //将returnValue中的值放到栈顶,准备给ireturnn返回
7: ireturn
8: astore_2 //给catch中定义的Exception e赋值,存储在slot2中
9: iconst_2 //catch块中x=2
10: istore_1
11: iload_1 // 将x保存到returnValue中,此时x=2
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4
19: iconst_3
20: istore_1
21: aload 4
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 9: 0
line 10: 2
line 15: 4
line 10: 6
line 11: 8
line 12: 9
line 13: 11
line 15: 13
line 13: 15
line 15: 17
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
SourceFile: "TestClass.java"
Exception属性
这里的Exception属性是在方法表中和Code属性平级的一项属性,不和前面的异常表混淆,Exception属性的作用和列出可能抛出的受检查异常,也就是throws关键字后面列举的异常