标签:中间件 避免 ict 存储结构 methods split 1.7 exe 生成
计算机只能识别0和1,所以大家编写的程序都需要经过编译器,转换为由0和1组成的二进制本地机器码(Native Code)。随着虚拟机的不断发展,很多程序语言开始选择与操作系统和机器指令集无关的格式作为编译后的存储格式(Class文件),从而实现”Write Once, Run Anywhere”。 Java设计之初,考虑后期能让Java虚拟机运行其他语言,目前有越来越多的其他语言都可以直接需要在Java虚拟机,虚拟机只能识别Class文件,至于是由何种语言编译而来的,虚拟机并不关心,如下图:
可以看出不管是由Java语言,还是JRuby等其他语言,只能能生成.class字节码文件,就都可以运行在Java虚拟机上。故发布规范文档时,Java规范拆分为Java语言规范和Java虚拟机规范。
Java语法中定义各种变量、关键字、运算符的语义最终由多个字节码命令组合而成。因此字节码命令所能提供的语义描述能力必然要比Java语言本身更加强大。
Class文件是一组以8位字节为单位的二进制流,中间没有任何分隔符,非常紧凑。 当需要占用8位以上的数据时,会按照Big-endian顺序,高位在前,低位在后的方式来分割成多个8位字节来存储。
Java虚拟机规范规定:Class文件格式采用伪结构来存储数据,伪结构中只有无符号数和表这两种数据类型。
下面介绍几个概述:
是指把类全名中的“.”号,用“/”号替换,并且在最后加入一个“;”分号后生成的名称。比如java.lang.Object
对应的全限定名为java/lang/Object;
。
这个比较好理解,就是直接的方法名或者字段。比如toString()
方法,不需要包名作为前缀了。
用于描述字段的数据类型。
规则如下:
基本类型字符 | 对应类型 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
S | short |
J | long |
Z | boolean |
V | void |
L+classname +; | 对象类型 |
[ | 数组类型 |
例如:
用来描述方法的参数列表(数量、类型以及顺序)和返回值。
格式:(参数描述符列表)返回值描述符。 例如:Object m(int i, double d, Thread t) {..}
==>IDLjava/lang/Thread;)Ljava/lang/Object;
一个Class类文件是由一个ClassFile结构组成:
ClassFile {
u4 magic; //魔数,固定值0xCAFEBABE
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]; //具体的属性内容
}
一个Class文件的大小:26 + cp_info[] + u2[] + field_info[] + method_info[] + attribute_info[]
接下来,将具体来介绍ClassFile文件的各个组成部分。
每个Class文件头4个字节称为魔数(Magic Number),作用是用于确定这个Class文件是否能被虚拟机所接受,魔数固定值0xCAFEBABE。这是身份识别,比如jpeg等图片文件头也会有魔数。
紧跟魔数,也占用4个字节。从第5字节到第8字节存储的分别是 次版本号,主版本号。
常量池是Class文件空间最大的数据项之一,长度不固定。
a. 常量池长度 用u2类型代表常量池容量计数值,u2紧跟版本号。u2的大小等于常量池的常量个数+1。对于u2=0的特殊情况,代表没有使用常量池。
b. 常量池内容,格式如下:
cp_info {
u1 tag;
u1 info[];
}
包括两个类常量,字面量和符号引用:
常量池中每一项常量都是一个表结构,每个表的开始第一位是u1类型的标志位tag, 代表当前这个常量的类型。在JDK 1.7.中共有14种不同的表结构的类型,如下:
Class文件都是二进制格式,可通过Jdk/bin/javap.exe
工具,分析Class文件字节码。关于javap用法,可通过javap --help
来查看。
2个字节代表,标示用于识别一些类或者接口层次的访问信息.
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为public;可以从包外部访问 |
ACC_FINAL | 0x0010 | 被声明为final;不允许子类修改 |
ACC_SUPER | 0x0020 | 当被invokespecial指令调用时,将特殊对待父类的方法 |
ACC_INTERFACE | 0x0200 | 接口标识符 |
ACC_ABSTRACT | 0x0400 | 声明为abstract;不能被实例化 |
ACC_SYNTHETIC | 0x1000 | 声明为synthetic;不存在于源代码,由编译器生成 |
ACC_ANNOTATION | 0x2000 | 声明为注释类型 |
ACC_ENUM | 0x4000 | 声明为枚举类型 |
当前类索引和父类索引占用大小都为u2类型,由于一个类智能继承一个父类,故父类索引只有一个。除了java.lang.Object对象的父类索引为0,其他所有类都有父类。
一个类可以实现多个接口,故利用interfaces_count来记录该类所实现的接口个数,interfaces[interfaces_count]来记录所有实现的接口内容。
字段表用于描述类或接口中声明的变量,格式如下:
field_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
字段访问标识如下:(表中加粗项是字段独有的)
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 public; 可以从包外部访问 |
ACC_PRIVATE | 0x0002 | 声明为 private; 只有定义的类可以访问 |
ACC_PROTECTED | 0x0004 | 声明为 protected;只有子类和相同package的类可访问 |
ACC_STATIC | 0x0008 | 声明为 static;属于类变量 |
ACC_FINAL | 0x0010 | 声明为 final; 对象构造后无法直接修改值 |
ACC_VOLATILE | 0x0040 | 声明为 volatile; 不会被缓存,直接刷新到主屏幕 |
ACC_TRANSIENT | 0x0080 | 声明为 transient; 不能被序列化 |
ACC_SYNTHETIC | 0x1000 | 声明为 synthetic; 不存在于源代码,由编译器生成 |
ACC_ENUM | 0x4000 | 声明为enum |
Java语法中,接口中的字段默认包含ACC_PUBLIC, ACC_STATIC, ACC_FINAL标识。ACC_FINAL,ACC_VOLATILE不能同时选择等规则。
紧跟其后的name_index和descriptor_index是对常量池的引用,分别代表着字段的简单名和方法的描述符。
方法表用于描述类或接口中声明的方法,格式如下:
method_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
方法访问标识如下:(表中加粗项是方法独有的)
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 public; 可以从包外部访问 |
ACC_PRIVATE | 0x0002 | 声明为 private; 只有定义的类可以访问 |
ACC_PROTECTED | 0x0004 | 声明为 protected;只有子类和相同package的类可访问 |
ACC_STATIC | 0x0008 | 声明为 static;属于类变量 |
ACC_FINAL | 0x0010 | 声明为 final; 不能被覆写 |
ACC_SYNCHRONIZED | 0x0020 | 声明为 synchronized; 同步锁包裹 |
ACC_BRIDGE | 0x0040 | 桥接方法, 由编译器生成 |
ACC_VARARGS | 0x0080 | 声明为 接收不定长参数 |
ACC_NATIVE | 0x0100 | 声明为 native; 由非Java语言来实现 |
ACC_ABSTRACT | 0x0400 | 声明为 abstract; 没有提供实现 |
ACC_STRICT | 0x0800 | 声明为 strictfp; 浮点模式是FP-strict |
ACC_SYNTHETIC | 0x1000 | 声明为 synthetic; 不存在于源代码,由编译器生成 |
属性表格式:
attribute_info {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u1 info[attribute_length]; //属性的具体内容
}
属性表的限制相对宽松,不需要各个属性表有严格的顺序,只有不与已有的属性名重复,任何自定义的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机运行时会忽略掉无法识别的属性。 关于虚拟机规范中预定义的属性,这里不展开讲了,列举几个常用的。
属性名 | 使用位置 | 解释 |
---|---|---|
Code | 方法表 | 方法体的内容 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 声明为deprecated |
InnerClasses | 类文件 | 内部类的列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
Signature | 类、方法表、字段表 | 用于支持泛型的方法签名,由于Java的泛型采用擦除法,避免类型信息被擦除后导致签名混乱,Signature记录相关信息 |
Code属性 java程序方法体中的代码,经编译后得到的字节码指令存储在Code属性内,Code属性位于方法表的属性集合中。但与native或者abstract的方法则不会存在Code属性中。
Code属性的格式如下:
Code_attribute {
u2 attribute_name_index; //常量池中的uft8类型的索引,值固定为”Code“
u4 attribute_length; //属性值长度,为整个属性表长度-6
u2 max_stack; //操作数栈的最大深度值,jvm运行时根据该值佩服栈帧
u2 max_locals; //局部变量表最大存储空间,单位是slot
u4 code_length; // 字节码指令的个数
u1 code[code_length]; // 具体的字节码指令
u2 exception_table_length; //异常的个数
{ u2 start_pc;
u2 end_pc;
u2 handler_pc; //当字节码在[start_pc, end_pc)区间出现catch_type或子类,则转到handler_pc行继续处理。
u2 catch_type; //当catch_type=0,则任意异常都需转到handler_pc处理
} exception_table[exception_table_length]; //具体的异常内容
u2 attributes_count; //属性的个数
attribute_info attributes[attributes_count]; //具体的属性内容
}
Code属性是Class文件中最重要的属性,Java程序的幸福课分为代码(方法体中的Java代码)和元数据(包含类、接口、字段、方法定义以及其他信息)两部分。
ConstantValue属性 ConstantValue属性是指被static关键字修饰的变量(也称为类变量)。
Java虚拟机采用基于栈的架构,其指令由操作码和操作数组成。
(byte1 << 8) | byte2
。放弃操作数对齐操作数对齐方案:
大多数指令包含了其操作所对应的数据类型信息,比如iload,表示从局部变量表中加载int型的数据到操作数栈;而fload表示加载float型数据到操作数栈。由于操作码长度只有1Byte,因此Java虚拟机的指令集对于特定操作只提供有限的类型相关指令,并非为每一种数据类型都有相应的操作指令。必要时,有些指令可用于将不支持的类型转换为可被支持的类型。
对于byte,short,char,boolean类型,往往没有单独的操作码,通过编译器在编译期或者运行期将其扩展。对于byte,short采用带符号扩展,chart,boolean采用零位扩展。相应的数组也是采用类似的扩展方式转换为int类型的字节码来处理。 下面分门别类来介绍Java虚拟机指令,都以int类型的数据操作为例。
栈是指操作数栈
变量进栈 | 含义 | 变量保存 | 含义 |
---|---|---|---|
iload | 第1个int型变量进栈 | istore | 栈顶nt数值存入第1局部变量 |
iload_0 | 第1个int型变量进栈 | istore_0 | 栈顶int数值存入第1局部变量 |
iload_1 | 第2个int型变量进栈 | istore_1 | 栈顶int数值存入第2局部变量 |
iload_2 | 第3个int型变量进栈 | istore_2 | 栈顶int数值存入第3局部变量 |
iload_3 | 第4个int型变量进栈 | istore_3 | 栈顶int数值存入第4局部变量 |
lload | 第1个long型变量进栈 | lstore | 栈顶long数值存入第1局部变量 |
fload | 第1个float型变量进栈 | fstore | 栈顶float数值存入第1局部变量 |
dload | 第1个double型变量进栈 | dstore | 栈顶double数值存入第1局部变量 |
aload | 第1个ref型变量进栈 | astore | 栈顶ref对象存入第1局部变量 |
常量进栈 | 含义 |
---|---|
aconst_null | null进栈 |
iconst_m1 | int型常量-1进栈 |
iconst_0 | int型常量0进栈 |
iconst_1 | int型常量1进栈 |
iconst_2 | int型常量2进栈 |
iconst_3 | int型常量3进栈 |
iconst_4 | int型常量4进栈 |
iconst_5 | int型常量5进栈 |
lconst_0 | long型常量0进栈 |
fconst_0 | float型常量0进栈 |
dconst_0 | double型常量0进栈 |
bipush | byte型常量进栈 |
sipush | short型常量进栈 |
常量池操作 | 含义 |
---|---|
ldc | int、float或String型常量从常量池推送至栈顶 |
ldc_w | int、float或String型常量从常量池推送至栈顶(宽索引) |
ldc2_w | long或double型常量从常量池推送至栈顶(宽索引) |
栈顶操作 | 含义 |
---|---|
pop | 栈顶数值出栈(不能是long/double) |
pop2 | 栈顶数值出栈(long/double型1个,其他2个) |
dup | 复制栈顶数值,并压入栈顶 |
dup_x1 | 复制栈顶数值,并压入栈顶2次 |
dup_x2 | 复制栈顶数值,并压入栈顶3次 |
dup2 | 复制栈顶2个数值,并压入栈顶 |
dup2_x1 | 复制栈顶2个数值,并压入栈顶2次 |
dup2_x2 | 复制栈顶2个数值,并压入栈顶3次 |
swap | 栈顶的两个数值互换,且不能是long/double |
注意:dup2对于long、double类型的数据就是一个,对于其他类型的数据,才是真正的两个,这个的2代表的是2个slot的数据。
字段调用 | 含义 |
---|---|
getstatic | 获取类的静态字段,将其值压入栈顶 |
putstatic | 给类的静态字段赋值 |
getfield | 获取对象的字段,将其值压入栈顶 |
putfield | 给对象的字段赋值 |
方法调用 | 作用 | 解释 |
---|---|---|
invokevirtual | 调用实例方法 | 虚方法分派 |
invokestatic | 调用类方法 | static方法 |
invokeinterface | 调用接口方法 | 运行时搜索合适方法调用 |
invokespecial | 调用特殊实例方法 | 包括实例初始化方法、父类方法 |
invokedynamic | 由用户引导方法决定 | 运行时动态解析出调用点限定符所引用方法 |
方法返回 | 含义 |
---|---|
ireturn | 当前方法返回int |
lreturn | 当前方法返回long |
freturn | 当前方法返回float |
dreturn | 当前方法返回double |
areturn | 当前方法返回ref |
运算指令是用于对操作数栈上的两个数值进行某种运算,并把结果重新存入到操作栈顶。Java虚拟机只支持整型和浮点型两类数据的运算指令,所有指令如下:
运算 | int | long | float | double |
---|---|---|---|---|
加法 | iadd | ladd | fadd | dadd |
减法 | isub | lsub | fsub | dsub |
乘法 | imul | lmul | fmul | dmul |
除法 | idiv | ldiv | fdiv | ddiv |
求余 | irem | lrem | frem | drem |
取反 | ineg | lneg | fneg | dneg |
其他运算:
类型转换用于将两种不同类型的数值进行转换。
(1) 对于宽化类型转换(小范围向大范围转换),无需显式的转换指令,并且是安全的操作。各种范围从小到大依次排序: int, long, float, double。
(2)对于窄化类型转换,必须显式地调用类型转换指令,并且该过程很可能导致精度丢失。转换规则中需要特别注意的是当浮点值为NaN, 则转换结果为int或long的0。虽然窄化运算可能会发生上/下限溢出和精度丢失等情况,但虚拟机规范明确规定窄化转换U不可能导致虚拟机抛出异常。
类型转换指令:i2b, i2c,f2i
等等。
控制指令是指有条件或无条件地修改PC寄存器的值,从而达到控制流程的目标
异常:
Java程序显式抛出异常: athrow指令。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现,而是采用异常表来完成。
同步:
方法级的同步和方法内部分代码的同步,都是依靠管程(Monitor)来实现的。
Java语言使用synchronized语句块,那么Java虚拟机的指令集中通过monitorenter和monitorexit两条指令来完成synchronized的功能。为了保证monitorenter和monitorexit指令一定能成对的调用(不管方法正常结束还是异常结束),编译器会自动生成一个异常处理器,该异常处理器的主要目的是用于执行monitorexit指令。
在基于堆栈的的虚拟机中,指令的主战场便是操作数栈,除了load是从局部变量表加载数据到操作数栈以及store储存数据到局部变量表,其余指令基本都是用于操作数栈的。
本文主要讲述虚拟机如何把 Class文件加载到内存的过程。校验、转换解析和初始化,最终形成可被虚拟机使用的Java类型,这就是虚拟机的类加载机制。类型的加载、连接和初始化都是在程序运行期间完成,这样做的优劣势,如下:
类的生命周期是指把Class字节码从文件中加载到内存,直到卸载内存整个过程,分为7个步骤。
图中用红色圈起来的3个过程分别为验证、准备、解析,它们合称为链接(Linking)过程。另外图中紫色的5项是严格按照执行。而蓝色的解析阶段不一定要在初始化之前, 也可以在初始化之后再解析,这种情况称为动态绑定或晚期绑定。
虚拟机在加载阶段,主要工作如下:
类的全限定名
获取该类的二进制字节流;对于上述字节流,可能来源:
注:对于数组类,不通过类加载器创建,而是由虚拟机直接创建的。另外加载阶段尚未完成,连接阶段可能已经开始。
验证是连接阶段(Linking)的第一步,目的是为了确保Class文件的字节流符合虚拟机规范,不会危害虚拟机自身安全。比如:访问数组越界问题,将对象转型为未实现的类型,跳转到不存在的代码区等情绪编译器都会拒绝编译,也就是无法生成Class文件,既然如此,为什么还要验证呢?原因是Class文件不一定都是由java源码编译而成,可以是任何途径,所以验证还是很有必要的,尽可能保证系统能承受住恶意代码攻击。
验证主要工作分4阶段:
验证点有比如是否魔数0xCAFEBABE开头;主、次版本号是否范围之内;常量池中常量tag标示是否正确等等,只有通过全部的验证,才能把字节流存储到内存的方法区。
经过文件格式验证,字节流已加载到方法区,这个阶段工作是对方法区的字节码进行语义分析,保证符合Java语言规范。 验证点比如:
比如操作数栈的数据类型和指令代码序列配合,跳转指令不会跳到方法体之外等。HotSpot虚拟机提供 -XX:-UseSplitVerifier选项来关闭这项优化。
校验点:
对于虚拟机的类加载机制来说,验证阶段非常重要的,但不是一定必要的。如果所运行的全部代码(包含自己编写以及第三方包的代码)都已经被反复使用和验证过,那么可以考虑使用 -Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
主要工作:static变量
分配内存,并设置类变量的初始值的阶段。
(1). 类变量:赋予零值
数据类型的零值表,如下:
类型 | int | long | float | double | short | byte | char | boolean | reference |
零值 | 0 | 0L | 0.0f | 0.0d | (short)0 | (byte)0 | ‘\u0000’ | false | null |
例如:
public static int value = 10;
在准备阶段,会为变量value
在方法区分配内存并初始化零值,即value=0
,而非10。 因为对于value的赋值10,是由putstatic
指令完成。该指令是在java程序被编译后,存放在类构造器<clinit>
方法之中。所以 value
=10的操作是在类初始化的时候才发生,故类变量在准备期value=0
。
(2). 常量:赋予真实值
例如:
public static final int value = 10;
对于常量,准备阶段会把类字段的字段属性表中的ConstantValue属性所指定的值(此处是10),赋给常量(value
),故常量在准备期间value =10
;
(3). 实例变量:不赋任何值
该阶段仅对类变量进行内存分配,而对于实例变量(或者称呼为成员变量)并不会分配内存,也就更不用提赋值的事。实例变量的初始化,是随着对象实例化时在Java堆上分配内存而进行的。
主要工作:虚拟机将常量池内的符号引用替换为直接引用的阶段。
先解析下符号引用和直接引用的概念
同一个符号引用 在不同的虚拟机中解析出来的直接引用地址一般都是不相同的;同一个符号引用,在同一个虚拟机下,多次解析时,会对第一次解析结果进行缓存(常量池记录直接引用,并标记已解析状态),从而避免多次解析。
特殊情形,对于invokedynamic指令,不会进行缓存过程,每次使用前都会进行解析。
主工作:主要是执行类构造器方法clinit。(class init的简称)
类初始化阶段是类加载的最后一个阶段。在初始化之前的过程中,用户可控的地方只有通过自定义类加载器参与,其余都是虚拟机主导和控制。
到了初始化,才开始真正的执行类中定义的Java程序代码。
(1). 类的构造方法
类构造方法是由编译器自动收集源文件中的类变量赋值操作
和静态语句块
合并而成的。收集顺序是由语句在源文件的顺序所决定。故静态语句块只能访问定义之前的静态变量;对于定义之后的变量可以赋值,但不能访问。
clinit方法不需要显式调用父类构造器
,虚拟机会保证子类的clinit方法执行之前,父类的clinit方法已经执行完毕。故第一个被执行的clinit方法的类肯定是java.lang.Object;clinit方法不是必需的
,对于没有静态块和类变量赋值操作,编译器不会生成clinit方法。父类静态语句和静态变量赋值优先于子类
.interface中不能有静态语句块
,但仍可以有变量初始化的赋值操作,也可以生成clinit方法。但接口和类的不同是,执行接口的clinit方法不需要先执行父接口的clinit方法。只有当父接口中定义的变量使用时,父接口才会初始化。阻塞等待
,当类构造方法有耗时操作,会造成多进程的阻塞,往往比较隐蔽。(2). 类初始化时机
虚拟机规范中严格规定有且只有5种情况下,当类没有初始化时必须立即对类进行初始化:
new
、getstatic
、putstatic
或invokeStatic
这4条字节码指令时。常见场景:new
;getstatic
;(final常量除外)putstatic
;invokeStatic
;main()
的类),虚拟机会先初始化该类;java.lang.reflect
包中的方法对类进行反射调用时;java.lang.invoke.MethodHandle
实例最后的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且该句柄所对应的类没有进行过初始化;上面讲到final常量不能触发类初始化,是由于在编译时已把数据放入常量池的静态字段,当读取类的static final
字段时,并不需要初始化类,而是从常量池中去获取相应的数据。
上述的5种场景的行为都是对类的一个主动引用过程。除此之外,还有被动引用并不会除非类的初始化过程。 另外
执行引擎是Java虚拟机非常最核心的部分,对于物理即的执行引擎是直接建立在处理器、硬件、指令集合操作系统层面,而虚拟机执行引擎则是由自行定制的指令集与执行引擎的结构体系。执行引擎在执行Java会有解释执行(通过解释器)和编译执行(通过JIT生成的本地代码)两种选择,对于Android ART又多了一种提前编译器(AOT)。
接下来,主要讲解虚拟机的方法执行过程,对于Java虚拟机的解释器的执行模型(不考虑异常处理):
do {
atomically calculate pc and fetch opcode at pc;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);
对象创建,不包括数组和Class对象,例如 Person person = new Person()
,
当虚拟机遇到new指令时:
从虚拟机角度,到此一个新的对象已经创建完成。但从Java视角,对象才刚刚开始,init构造方法还没有执行,所有字段还是零。执行完init方法,按java程序的构造方法进行初始化后,对象便是彻底创建完成。
转载:http://gityuan.com/2015/10/17/jvm-class-instruction/
标签:中间件 避免 ict 存储结构 methods split 1.7 exe 生成
原文地址:http://www.cnblogs.com/ONDragon/p/7645075.html