赞
踩
我们知道Java程序的代码文件,在编译后会生成.class
字节码文件,也就是Class类文件。Class文件中包含了Java虚拟机指令集、符号表以及其他的辅助信息,Java虚拟机规范要求Class文件必须满足许多强制性的语法和结构约束,使得其是被Java虚拟机所接受的有效的Class文件。
那么Java代码是如何变成这些能够被Java虚拟机所接受的Class文件的呢?这其中需要经过Java编译器的编译。
任何一个Class文件都对应着一个唯一的类或接口的定义信息;但是,类或接口并不完全是定义在文件里的,类或接口也可以动态生成,直接加入类加载器中。
Class文件中,只有两种数据类型:无符号数
和表
。
Class文件本质也是一张表,由以下数据项按严格顺序排列而成
类型 | 名称 | 数量 |
---|---|---|
u4 | magic(Class文件的前4个字节) | 1 |
u2 | minor_version(次版本号) | 1 |
u2 | major_version(主版本号) | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
Java虚拟机将定义类的信息从Class文件加载到内存,并对信息进行校验、解析和初始化,最终形成可以被Java虚拟机直接使用的Java类型,这就是Java虚拟机的类加载过程。
Java类型的加载、连接和初始化过程都是在程序运行期间进行的,这为Java程序提供了极高的可扩展性和灵活性。
Java虚拟机规范中规定,以下场景必须对类进行初始化:
以上场景都会触发类型的初始化,这些行为称为对类型的主动引用。除此之外,对类型的引用都不会触发其初始化,称为对类型的被动引用。
对类型的被动引用常见的场景有:
类加载的全过程包括:加载、验证、准备、解析、初始化。
类加载过程的第一个阶段,主要完成以下3件事情:
类加载过程的第二个阶段,其目的是为了确保Class文件的字节流中包含的信息符合Java虚拟机规范的约束要求。包括
文件格式验证
、元数据验证
、字节码验证
和符号引用验证
。
类加载过程的第三个阶段,该阶段为类中定义的类变量(静态变量)分配内存并根据类型设置初始值。
这里只包括类变量是因为实例变量将会在对象实例化时随着对象一起在Java堆中分配,这里为类型设置初始值,通常情况下为数据类型的零值,例如public static int num = 1;
,那么变量num在经过准备阶段后的初始值为0,而不是1。因为这时尚未开始执行任何Java方法,而把变量num赋值为1的putstatic指令是程序被编译后,存放于类构造器<clinit>()
方法中,所以赋值的动作需等到类的初始化阶段才会被执行。
如果变量num被final修饰,那么在准备阶段就会被赋值为1。
类加载过程的第四个阶段,该阶段将常量池内的符号引用替换为直接引用。
类加载过程的最后一个阶段,该阶段Java虚拟机才真正开始执行类中编写的代码。
在准备阶段已经为类变量赋值了数据类型的初始零值,初始化阶段会执行类构造器<clinit>()
方法,它是Java编译器将类中的所有类变量的赋值动作和static静态代码块中的语句合并后进行编译生成,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态代码块中只能访问到定义其之前的类变量,定义在静态代码块之后的类变量,可以在静态代码块中赋值,但是不能访问。
类构造器<clinit>()
方法与类的构造方法(实例构造器<init>()
方法)不同,Java虚拟机会保证在子类的<clinit>()
方法执行前,其父类的<clinit>()
已经执行完毕;由于父类的<clinit>()
先执行,所以父类中定义的静态代码块要优先于子类类变量的赋值操作。
<clinit>()
方法对于类或接口来说并不是必需的
<clinit>()
方法<clinit>()
方法。执行接口<clinit>()
方法时并不需要先执行父接口的<clinit>()
方法,只有当父接口中定义的变量被引用时,才会触发父接口的初始化类加载器作用于类加载过程的第一阶段加载阶段,通过类加载器,可以根据类的全限定名获取描述该类的二进制字节流。
对于任意一个类,其在Java虚拟机中的唯一性由加载它的类加载器和类本身确定。
类加载器可以分为3类:
启动类加载器(Bootstrap Class Loader)
、扩展类加载器(Extension Class Loader)
和应用程序类加载器(Application Class Loader)
1.启动类加载器(Bootstrap Class Loader)
负责将在<JAVA_HOME>\lib
目录,或者被-Xbootclasspath
参数所指定的路径中存放的,能够被Java虚拟机识别(按文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录下也不会被加载)的类库加载到虚拟机的内存中。
2.扩展类加载器(Extension Class Loader)
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext
目录中,或者被java.ext.dirs
系统变量所指定的路径中所有的类库。
3.应用程序类加载器(Application Class Loader)
它负责加载用户类路径(ClassPath)上所有的类库。
使用一个类加载器进行类加载时,它首先不会自己去尝试加载这个类,而是把这次加载请求委派给父类加载器去完成,每一个层次的类加载都是如此,因此,所有的类加载请求都将派送到最顶层的启动类加载器,只有当父类加载器无法完成这个加载请求时,即在它的加载范围内无法找到要加载的类,子类加载器才会尝试自己去完成加载请求。
使用双亲委派机制可以保证类的唯一性。
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。
对于大部分与数据类型相关的字节码指令,它的操作码助记符中都有特殊的字符来表明是为哪种数据类型服务的。
操作码助记符 | 数据类型 |
---|---|
b | byte |
c | char |
s | short |
i | int |
f | float |
d | double |
l | long |
a | reference |
加载和存储指令用于栈帧中局部变量表和操作数栈的数据之间的来回传输。
指令功能 | 指令 |
---|---|
将局部变量表的一个局部变量加载到操作数栈 | load、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n> |
将一个数值从操作数栈存储到局部变量表 | istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n> |
将一个常量加载到操作数栈 | bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d> |
扩充局部变量表的访问索引的指令 | wide |
运算指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。
指令功能 | 指令 |
---|---|
加法指令 | iadd、ladd、fadd、dadd |
减法指令 | isub、lsub、fsub、dsub |
乘法指令 | imul、lmul、fmul、dmul |
除法指令 | idiv、ldiv、fdiv、ddiv |
求余指令 | irem、lrem、frem、drem |
取反指令 | ineg、lneg、fneg、dneg |
位移指令 | ishl、ishr、iushr、lshl、lshr、lushr |
按位或指令 | ior、lor |
按位与指令 | iand、land |
按位异或指令 | ixor、lxor |
局部变量自增指令 | iinc |
比较指令 | dcmpg、dcmpl、fcmpg、fcmpl、lcmp |
类型转换指令可以将两种不同类型的数值想换转换。
宽泛类型转换是指小范围数据类型到大范围数据类型的转换,在转换时不需要显式的转换指令
窄化类型转换是大范围数据类型到小范围数据类型的转换,在转换时需要显式的转换指令,转换过程中可能会出现数值的精度丢失或者溢出。包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f
// 宽泛类型转换
int a = 1;
long b = a;
float c = a;
double d = a;
double e = b;
// 窄化类型转换
long a = 1L;
int b = (int) a;
double c = 1.0d;
float d = (float) c;
类实例和数组都是Java对象,但是它们的字节码指令却是不同的。
指令功能 | 指令 |
---|---|
创建类实例 | new |
创建数组实例 | newarray、anewarray、multianewarray |
访问类字段和实例字段 | getstatic、putstatic、getfield、putfield |
把一个数组元素加载到操作数栈的指令 | baload、caload、saload、iaload、laload、faload、daload、aaload |
将一个操作数栈的值储存到数组元素中的指令 | bastore、castore、sastore、iastore、fastore、dastore、aastore |
取数组长度的指令 | arraylength |
检查类实例类型的指令 | instanceof、checkcast |
指令功能 | 指令 |
---|---|
将操作数栈的栈顶一个或两个元素出栈 | :pop、pop2 |
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶 | dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 |
将栈最顶端的两个数值互换 | swap |
控制转移指令可以让Java虚拟机从指定位置指令的下一条指令继续执行。
指令功能 | 指令 |
---|---|
条件分支 | ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne |
复合条件分支 | tableswitch、lookupswitch |
无条件分支 | goto、goto_w、jsr、jsr_w、ret |
指令功能 | 指令 |
---|---|
用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派) | invokevirtual |
用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 | invokeinterface |
用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 | invokespecial |
用于调用类静态方法 | invokestatic |
用于在运行时动态解析出调用点限定符所引用的方法。 | invokedynamic |
返回值类型为boolean、byte、char、short、int | ireturn |
返回值类型为long | lreturn |
返回值类型为float | freturn |
返回值类型为double | dreturn |
返回值类型为引用类型 | areturn |
void的方法、实例初始化方法、类和接口的类初始化方法使 | return |
Java虚拟机中异常处理由
athrow
指令来实现。
在Java中,同步分为同步方法和同步代码块。
同步方法是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。
虚拟机可以从Class常量池中方法表结构中的的
ACC_SYNCHRONIZED
访问标志得知一个方法是否被声明为同步方法。如果设置了,执行线程需要先持有同步锁才能执行该方法,当方法结束时释放同步锁。
同步代码块则需要monitorenter
和monitorexit
两条指令来支持synchronized
关键字的语义
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。