通俗易懂的Java虚拟机类加载

Java程序编译后生成Class文件,包含字节码和辅助信息。类加载过程包括加载、验证、准备、解析和初始化,其中初始化涉及静态字段赋值和静态方法调用。类加载器按双亲委派模型工作,保证类的唯一性。字节码指令用于操作数栈和局部变量表的数据处理。
摘要由CSDN通过智能技术生成

1.概述

我们知道Java程序的代码文件,在编译后会生成.class字节码文件,也就是Class类文件。Class文件中包含了Java虚拟机指令集、符号表以及其他的辅助信息,Java虚拟机规范要求Class文件必须满足许多强制性的语法和结构约束,使得其是被Java虚拟机所接受的有效的Class文件。

那么Java代码是如何变成这些能够被Java虚拟机所接受的Class文件的呢?这其中需要经过Java编译器的编译。

2.Class类文件结构

任何一个Class文件都对应着一个唯一的类或接口的定义信息;但是,类或接口并不完全是定义在文件里的,类或接口也可以动态生成,直接加入类加载器中。

Class文件中,只有两种数据类型:无符号数

Class文件结构

Class文件本质也是一张表,由以下数据项按严格顺序排列而成

类型名称数量
u4magic(Class文件的前4个字节)1
u2minor_version(次版本号)1
u2major_version(主版本号)1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_count

3.类加载

Java虚拟机将定义类的信息从Class文件加载到内存,并对信息进行校验、解析和初始化,最终形成可以被Java虚拟机直接使用的Java类型,这就是Java虚拟机的类加载过程。

Java类型的加载、连接和初始化过程都是在程序运行期间进行的,这为Java程序提供了极高的可扩展性和灵活性。

3.1 类加载时机

Java虚拟机规范中规定,以下场景必须对类进行初始化:

  • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类型没有被初始化,则需要先进行类型的初始化。能够生成这四条指令的场景有:
    • 使用new关键字创建实例对象的时候
    • 读取或设置一个类型的静态字段(被final修饰、在编译期已把结果放入常量池的静态字段除外)的时候
    • 调用一个类型的静态方法的时候
  • 对类型进行反射调用的时候,如果类型没有被初始化,则需要先进行类型的初始化。
  • 在初始化类的时候,如果发现其父类还没有被初始化,则需要先对其父类进行类型的初始化。
  • 在虚拟机启动的时候,需要指定一个含有main()方法的主类,需要先对该类进行初始化。
  • 当一个接口中定义了JDK8新接入的被default关键字修饰的默认方法,如果这个接口的实现类发生了初始化,则该接口需要进行初始化。
  • 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

类加载时机

以上场景都会触发类型的初始化,这些行为称为对类型的主动引用。除此之外,对类型的引用都不会触发其初始化,称为对类型的被动引用

对类型的被动引用常见的场景有:

  • 通过子类来引用父类中定义的静态字段,只会触发父类的初始化,不会触发子类的初始化。
  • final修饰的静态常量会在编译阶段放入类的常量池中,没有直接引用到定义常量的类,不会触发类的初始化。

3.2 类加载过程

类加载过程

类加载的全过程包括:加载、验证、准备、解析、初始化。

3.2.1 加载

类加载过程的第一个阶段,主要完成以下3件事情:

  1. 通过类的全限定名获取此类的二级制字节流。
  2. 将该字节流所代表的的静态存储结构转化为运行时数据区域中方法区的数据结构。
  3. 在堆内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

3.2.2 验证

类加载过程的第二个阶段,其目的是为了确保Class文件的字节流中包含的信息符合Java虚拟机规范的约束要求。包括文件格式验证元数据验证字节码验证符号引用验证

类加载过程之验证阶段

3.2.3 准备

类加载过程的第三个阶段,该阶段为类中定义的类变量(静态变量)分配内存并根据类型设置初始值。

这里只包括类变量是因为实例变量将会在对象实例化时随着对象一起在Java堆中分配,这里为类型设置初始值,通常情况下为数据类型的零值,例如public static int num = 1;,那么变量num在经过准备阶段后的初始值为0,而不是1。因为这时尚未开始执行任何Java方法,而把变量num赋值为1的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法中,所以赋值的动作需等到类的初始化阶段才会被执行。

如果变量num被final修饰,那么在准备阶段就会被赋值为1。

3.2.4 解析

类加载过程的第四个阶段,该阶段将常量池内的符号引用替换为直接引用。

  • 符号引用:以一组符号老描述所引用的目标,符号可以是任何形式的字面量,只用使用时能无歧义地定位到引用目标即可。
  • 直接引用:可以直接指向引用目标的指针、相对偏移量或者是一个能间接定位到引用目标的句柄,如果有了直接引用,那引用的目标必定已经在内存中存在。

3.2.5 初始化

类加载过程的最后一个阶段,该阶段Java虚拟机才真正开始执行类中编写的代码。

在准备阶段已经为类变量赋值了数据类型的初始零值,初始化阶段会执行类构造器<clinit>()方法,它是Java编译器将类中的所有类变量的赋值动作和static静态代码块中的语句合并后进行编译生成,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态代码块中只能访问到定义其之前的类变量,定义在静态代码块之后的类变量,可以在静态代码块中赋值,但是不能访问。

类构造器<clinit>()方法与类的构造方法(实例构造器<init>()方法)不同,Java虚拟机会保证在子类的<clinit>()方法执行前,其父类的<clinit>()已经执行完毕;由于父类的<clinit>()先执行,所以父类中定义的静态代码块要优先于子类类变量的赋值操作。

<clinit>()方法对于类或接口来说并不是必需的

  • 如果一个类中没有类变量赋值操作,也没有静态代码块,那么编译器就不会为该类生成<clinit>()方法
  • 如果一个接口没有类变量赋值操作,那么编译器就不会为该类生成<clinit>()方法。执行接口<clinit>()方法时并不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被引用时,才会触发父接口的初始化

3.3 类加载器

类加载器作用于类加载过程的第一阶段加载阶段,通过类加载器,可以根据类的全限定名获取描述该类的二进制字节流。

对于任意一个类,其在Java虚拟机中的唯一性由加载它的类加载器和类本身确定。

3.3.1 类加载器分类

类加载器可以分为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)上所有的类库。

3.3.2 双亲委派机制

使用一个类加载器进行类加载时,它首先不会自己去尝试加载这个类,而是把这次加载请求委派给父类加载器去完成,每一个层次的类加载都是如此,因此,所有的类加载请求都将派送到最顶层的启动类加载器,只有当父类加载器无法完成这个加载请求时,即在它的加载范围内无法找到要加载的类,子类加载器才会尝试自己去完成加载请求。

使用双亲委派机制可以保证类的唯一性。

4.字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。

4.1 字节码与数据类型

对于大部分与数据类型相关的字节码指令,它的操作码助记符中都有特殊的字符来表明是为哪种数据类型服务的。

操作码助记符数据类型
bbyte
cchar
sshort
iint
ffloat
ddouble
llong
areference

4.2 加载和存储指令

加载和存储指令用于栈帧中局部变量表和操作数栈的数据之间的来回传输。

指令功能指令
将局部变量表的一个局部变量加载到操作数栈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

4.3 运算指令

运算指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。

指令功能指令
加法指令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

4.4 类型转换指令

类型转换指令可以将两种不同类型的数值想换转换。

宽泛类型转换是指小范围数据类型到大范围数据类型的转换,在转换时不需要显式的转换指令

窄化类型转换是大范围数据类型到小范围数据类型的转换,在转换时需要显式的转换指令,转换过程中可能会出现数值的精度丢失或者溢出。包括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;

4.5 对象创建与访问指令

类实例和数组都是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

4.6 操作数栈管理指令

指令功能指令
将操作数栈的栈顶一个或两个元素出栈:pop、pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
将栈最顶端的两个数值互换swap

4.7 控制转移指令

控制转移指令可以让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

4.8 方法调用和方法返回指令

指令功能指令
用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)invokevirtual
用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用invokeinterface
用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法invokespecial
用于调用类静态方法invokestatic
用于在运行时动态解析出调用点限定符所引用的方法。invokedynamic
返回值类型为boolean、byte、char、short、intireturn
返回值类型为longlreturn
返回值类型为floatfreturn
返回值类型为doubledreturn
返回值类型为引用类型areturn
void的方法、实例初始化方法、类和接口的类初始化方法使return

4.9 异常处理指令

Java虚拟机中异常处理由athrow指令来实现。

4.10 同步指令

在Java中,同步分为同步方法和同步代码块。

  • 同步方法是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。

    虚拟机可以从Class常量池中方法表结构中的的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。如果设置了,执行线程需要先持有同步锁才能执行该方法,当方法结束时释放同步锁。

  • 同步代码块则需要monitorentermonitorexit两条指令来支持synchronized关键字的语义

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值