虚拟机类加载机制

类加载机制定义: 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成被虚拟机直接使用的java类型。
类型的加载、连接和初始化过程都是在程序运行期间完成,java动态扩展的特性就是依赖运行期间动态加载和动态连接这个特点实现的。如:编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类。

类加载时机

类的生命周期
在这里插入图片描述
加载、验证、准备、初始化和卸载这五个阶段顺序是确定的,但是解析阶段可能会在初始化之后开始(这是为了支持运行时动态绑定)。
类的初始化时机(只有在一下六种情况下进行类的初始化)
(1)遇到new(new 关键字实例化对象)、getstatic(读取static静态字段,同时被final修饰的除外,因为final修饰的字段在常量池中,即在解析阶段就完成了赋值)、putstatic(设置static静态字段)、或者invokestatic(调用静态方法)这四条字节码指令。
(2)使用java.lang.reflect包中方法对类型反射调用。
(3)初始化类时,先初始化父类
(4)虚拟机启动时,初始化一个指定主类(main方法在的类)
(5)如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF、newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有被初始化,就需要先初始化
(6)当接口中存在默认方法(default修饰的接口方法),如果接口实现类发生初始化,那么接口要在该类初始化之前初始化。
以上六种称为主动引用,以下几种在引用一个类型时不进行初始化,称为被动引用
(1)通过子类引用父类的静态字段,不会导致子类初始化。
对于静态字段,只有直接定义这个字段的类才会被初始化。
(2)通过数组定义来引用类,不会触发此类初始化。
注:在定义一个数组时,虚拟机会自动生成一个直接继承于Object的子类,由字节码指令newarray触发,数组的属性和方法(用户只能使用length属性和clone()方法)都在这个类中实现。
(3)常量在编译阶段会存入调用类的常量池中,本质上没有直接引用定义常量的类,因此不会触发定义常量的类的初始化。
注:接口也有加载过程,编译器也会为接口生成()类构造器,用于初始化接口中定义的成员变量。但是接口在初始化时,不要求父接口进行初始化,只有真正用到父接口的时候(如:引用接口中的常量)才会初始化。

类加载过程

(1)加载

加载过程

  • (1)获取二进制字节流:类加载器通过类的全限定名获取该定义该类的二进制字节流。
  • (2)在元空间中生成instanceKlass对象:instanceKlass对象就是对象头中类型指针(Klass)指向的对象,该对象存储了类的元数据,包括类的方法列表、常量池、类的静态字段(jdk6以及以前)、实现的接口列表、父类、子类等等与该类有关的所有信息。
  • (3)在堆中生成类的java镜像即Class对象:Class对象不存储类的元数据,只是对instanceKlass对象的包装(即只是instanceKlass对象的一个镜像)供java的反射访问用,instanceKlass对象与Class对象相互引用。

(2)验证

(1)文件格式验证,即验证二进制字节流是否符合Class文件格式规范。如是否以魔术oxCAFEBABE开头、主次版本是否符合在当前虚拟机接受范围、常量池中是否有不被支持的常量等。该阶段的验证,是基于二进制字节流的,之后的三个步骤的验证是基于方法区的存储结构进行的。
(2)元数据验证,对字节码描述的信息进行语义分析,保证描述的信息符合规范。如该类是否有父类、该类继承的类是否合法(不能继承被final修饰的类)、该类如果不是抽象类必须实现其父类或者接口要求的所有方法、类中的字段,方法是否与父类矛盾(覆盖父类的final字段、出现不符合规则的重载即出现只有返回类型不同的两个方法(这两个方法的特征签名相同,无法同时存在))
(3)字节码验证,即方法体(即方法表的Code属性)进行校验。如保证操作码与操作数的数据类型一致、跳转指令不跳转到方法体之外、类型转换必须合法(即子类对象赋值给父类数据类型是合法的,但是父类对象赋值给子类类型或者赋值给不相干的类型是不合法的,同时在基本类型上,int数据赋值给long、double、float是可行的,但是赋值给short、char是不可行的)
该阶段的优化手段:用StackMapTable(Code属性表的属性)记录方法体中基本块(按照控制流拆分)开始时变量表和操作数栈的状态,字节码验证期间就不用进行数据流分析,而是只要检查StackMapTable中记录是否合法。
(4)符号引用验证:该验证是在解析阶段进行验证(即将符号引用转化为直接引用),需要验证的有:通过类的全限定名能不能找到该类,符号引用中类、字段、方法的可访问性能不能被当前类访问等。

(3)准备

该阶段的内容:为类变量分配内存空间并设置初始值(注意是初始值)。
注意:
(1)静态变量放置的位置:在jdk6以及之前,静态变量是放在instanceKlass对象的末尾(在方法区,方法区用永久代实现),而在jdk7以及之后,方法区是用元空间来实现,此时静态变量放在了Class对象(在堆中)的末尾。
(2)只是赋初值:这里所说的赋初值,通常来说就是零值(但是boolean的初始的false、reference的初值是null),而不是实际的赋值操作。因为这里还没有执行任何的java方法,而真正的赋值指令putstatic指令是放在类构造器()方法中,故真正的赋值操作必须到类初始化阶段执行类的构造函数时才进行真正的赋值。
(3)上面之所以说是通常情况,是因为当static变量同时别final修饰时,字段表中会多一个ConstantValue属性,记录该变量的值,那么此时在准备阶段就会初始化为实际的值。
如 public static final int value = 123。此时在准备阶段value就被赋为123。如果没有final就是赋值0。

(4)解析

(解析阶段做的工作:将那些一旦编译完成就不会改变(不会改变理解为不会被覆盖)的方法(invokestatic调用的方法、invokespecial调用的方法、以及final修饰的方法)的符号引用转化为直接引用)
解析:虚拟机将常量池中的符号引用替换为直接引用的过程。class文件中常量池中一般结构为:#2 符号引用(即表示常量池中偏移量为2的位置是某个符号引用),当解析过后,那个就变成了如下结构:#2 直接引用(直接引用可内存布局直接相关,可以是指向目标的指针、某个对象中的相对偏移量、或者是一个句柄地址)

方法的符号引用解析为直接引用,分为静态解析(即解析)和动态连接。静态解析即为在类加载阶段或者第一次使用的时候方法被转化为直接引用,可以被静态解析的方法必须是在程序真正运行之前就有一个确定的调用版本,并且该方法的调用版本在运行期间不会改变。这样的方法包括invokestatic调用的方法(静态方法)、invokespecial(实例的构造方法、私有方法、父类方法)以及final修饰的方法。
动态连接:
类或者接口解析
假设当前代码所在的类为D,N是类或者接口C的符号引用,并且没有被解析过
那么解析N的过程:
(1)若C非数组,D的类加载器通过N代表的额全限定名加载类C。
(2)如果C是数组,且数组元素类型为对象,那么就按照(1)加载数据元素类型,接着虚拟机生成一个代表该数组维度和元素的数组对象。(该对象的类型是虚拟机自动生成的,比如如果数组定义为String[] a = new Strring[10],那么虚拟机会自动生成一个名为“[Ljava/lang/String”类型,该类型直接继承于Object对象,并且可以使用该类的成员变量length和clone()方法)
(3)经过(1)(2)C已经被加载到了虚拟机中,并且生成了InstanceKlass和Class对象,但是还需要进行符号引用验证,确认D是否具备对C的访问权限。
(4)然后N被替换为类C的实际地址(即InstanceKlass的地址)
字段解析
首先对字段表中class_index项进行解析,即先对字段所属的类或者接口进行解析。如果解析成功则把该字段所属的类或者接口用C表示,然后按照如下过程搜素字段的直接引用:
(即先在搜索自己,在从下到上搜索接口,最后从下到上搜素父类)
(1)如果C中包含的简单名称和描述符与目标匹配,则返回这个字段的直接引用。
(2)否则,C中实现了接口,则会按照继承关系从下到上递归搜索,看是否有匹配的字段,有就返回直接引用。
(3)否则,C不是Object类的话,按照继承关系从下到上递归搜素父类看是否有匹配,有匹配则返回直接引用。
(4)否则查找失败,抛出java.lang.NoSuchFieldError异常。
方法解析
首先对方法所属的类解析(如果方法表中的class_index为一个接口,则直接抛出异常),先在当前类中查找是否有匹配的简单名称和描述符,再从下到上在继承的类中查找,再从下到上在实现的接口中查找(如果找到则说明C是一个抽象类,抛出AbstractMethodError异常)如果都没找到,则抛出NoSuchMethodError异常。
接口方法解析
先解析接口方法对应的接口,之后按照顺序查找接口方法被接口中、从下到上搜索父接口(直到Object类(包括)),如果没找到则抛出NoSuchMethodError异常。

(5)初始化

在初始化阶段,虚拟机才真正执行java代码。
初始化阶段工作:执行类构造器“<clinit”()方法。类构造方法是由javac编译器直接生成的。
关于<clinit()方法,需要注意的细节:
(1)<clinit()方法如何产生:编译器自动收集类中的所有类变量的赋值语句(不包括方法中的)+静态语句块中的语句合并产生。收集的顺序按照源文件中语句出现的顺序。

 //静态语句块只能访问定义在他之前的静态变量,对于定义在其之后的静态变量,只能对其赋值,不能访问
public class Test{
	static{
		i = 0;  //可以对该变量赋值
		system.out.println(i); //这句编译器会提示“非法向前引用”
	}
	static int i = 1;
产生上面情况的原因是
}

产生上述情况的原因是:因为在之前的准备阶段,已经在方法区(逻辑上是方法区,但是实际上:在jdk1.7之后静态边防放在堆中Class对象的末尾)为静态变量分配了内存,并且赋了初始值(零值),故该变量已经存在了,所以是可以赋值的。之所以不能访问,因为static语句块出现在静态变量签名,故static块中的语句在<clinit()方法中要先执行,这时候如果访问定义在其之后的静态变量的话,由于静态变量的赋值操作还没有执行,这时只能访问到初始值(零值)。
(2)<clinit()方法与类中的构造方法<init()不同,其不需要显示调用父类的<clinit()方法,jvm会保证子类的类构造器执行之前,父类的已经执行完毕。(父类的类构造器比子类的先执行)。但是执行接口或者接口的实现类中的<clinit()方法,不需要其父接口的<clinit()方法先执行。
(3)如果一个类中没有静态变量的赋值语句(可以有静态变量的定义)和静态语句块,则编译器不会生成类构造器。
(4)多个线程同时初始化一个类,那么只会有一个线程执行这个类的<clinit()方法,其他线程只能等待,直到活动线程执行完毕<clinit()方法(当活动线程执行完类构造方法,其他线程被唤醒,但是不会再执行类构造方法,因为一个类只能被实例化一次)。当这个类的<clinit()方法耗时很长,那么可能造成多个进程阻塞。

类加载器

类加载器通过类的全限定名来获取描述类的二进制字节流。(即加载阶段的第一个步骤)
类加载器与类本身共同确立类在虚拟机中的唯一性,两个类来源同一个class文件且被同一个类加载器加载,那么这两个类才相等。只要加载器不同则这两个类肯定不相等。(这里的相等指的是,代表这个类的Class对象的equals()方法,isAssignableFrom()方法,isInstance()方法的返回结果,以及instanceof关键字判定对象所属类的关系判定)

双亲委派模型

java虚拟机角度类加载器的划分
在这里插入图片描述

在这里插入图片描述
双亲委派模型
在这里插入图片描述
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应有自己的父加载器。这里的父子关系不是继承关系,而是组合关系来复用父加载器代码。

双亲委派模型工作过程:一个类加载器收到了类加载请求,把该请求委派给父加载器(所有的加载请求最终都应该传送到启动类加载器),只有当父加载器无法加载该类(即类加载器的加载范围中没有找到该类),子加载器才尝试自己完成加载。

双亲委派模型的好处:java类随着它的类加载器一起具备了优先级的层次关系。例如类java.lang.Object存在rt.jar中,无论哪个类加载器加载这个类,最终都会委派给启动类加载器,这样可以保证Object类在程序的各种类加载器环境中都能够保证是同一个类。
双亲委派具体参考: link.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值