虚拟机类加载机制
一、概述
类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成
可以被虚拟机直接使用的java类型。
java语言中,类型的加载、连接、初始化过程都是在程序运行期间完成的。
二、类加载的时机
类的生命周期:
加载、验证、准备、解析、初始化、使用、卸载
其中:验证、准备、解析3个部分统称为连接
解析阶段的顺序不是一定的,某些情况下它可以在初始化阶段之后开始(为了支持java的动态绑定)
有且只有5种情况必须对类进行初始化(主动引用):
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过
初始化,则需要先触发其初始化。对应的常见java代码场景:
使用new关键字实例化对象
读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
调用一个类的静态方法
2、对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法),虚拟机会先初始化这个类
5、Methodhandler....
接口的初始化与类初始化的区别:上述第三点
接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(
如引用父接口中定义的常量)才会初始化
三、类加载的过程
1、加载
加载阶段,虚拟机需要完成三件事情:
(1)通过全限定名来获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表这个类的Class对象(实例化),作为方法区这个类的各种数据访问入口
一个非数组类的加载阶段是可控性最强的
但对于数组类则不同,数组类本身不通过类加载器创建,是由虚拟机直接创建的
2、验证
确保Class文件中的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
四个阶段的检验工作:
(1)文件格式验证
保证输入的字节流能正确地解析并存储于方法区之内,只有通过了这个阶段的验证之后擦,字节流才会进
入内存的方法区进行存储。后面的三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流
(2)元数据验证
对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息
如:是否有父类、父类是否继承了不允许被继承的类、类中字段、方法是否与父类产生矛盾等
(3)字节码验证(最复杂的阶段)
通过数据流和控制流分析,确认程序语义是合法的、符合逻辑的。
这个阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
(4)符号引用验证
发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段:解析阶段中进行
符号引用验证的目的:确保解析动作的正常执行
3、准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区
中进行分配
注:这时候进行内存分配的仅包括类变量,不包括实例变量,实例变量会在对象实例化时随着对象一起分配在
java堆中。并且,此处说的初始值通常指各种数据类型的零值(final常量例外)
4、解析
虚拟机将常量池中的符号引用替换为直接引用的过程
符号引用:以一组符号来描述所引用的目标。引用的目标不一定已经加载到内存中
直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接
引用,那引用的目标必定已经在内存中存在
并没有规定解析阶段发生的具体时间,可以根据需要来判断到底是在类被加载器加载时就对常量池中的
符号进行解析,还是等到一个符号引用将要被使用前才去解析它
(1)类或接口的解析:三个步骤(当前代码所处类D,符号引用N,直接引用C)
1、如果C不是数组,虚拟机将代表N的全限定名传递给D的类加载器去加载C
2、如果C是数组类型,按第一点的规则加载元素类型
3、如果上面都没异常,那么C在虚拟机中实际已经成为一个有效类或接口了,但在解析完成之前
还要进行符号引用验证,确认D是否具备对C的访问权限
(2)字段解析(字段所属的类或接口用C表示)
1、如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用
2、否则,如果C实现了接口,按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中
包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用
3、否则,按照继承关系从下往上递归搜索其父类,如果。。。
4、否则,查找失败,抛出NoSuchFieldError异常
如果查找过程成功返回了引用,将对这个字段进行权限验证,如果发现不具备访问权限,报异常
(3)类方法解析(所在类为C)
1、类方法和接口方法符号引用的常量定义是分开的,如果在类方法表中发现Class_index
中索引的C是个接口,抛异常。
2、如果第一步通过,在C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则
直接返回该方法的直接引用
3、否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有
则返回其直接引用
4、否则,在类C实现的接口列表中递归查找。。。。。。。。
5、否则,查找失败,抛异常
(4)接口方法解析
与类方法解析类似
5、初始化
到了初始化阶段,才真正执行了类中定义的Java代码(字节码)
初始化过程是执行类构造器()方法的过程
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,
编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在其之前
的变量,定义在它后面的变量,在前面的静态语句块中可以赋值,但是不能访问
<clinit>()方法与类的构造函数(<init>()方法)不同,不需要显式调用父类构造器,虚拟机会
保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕
<clinit>()对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值
操作,那么编译器可以不为这个类生成<clinit>()方法
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去
初始化一个类,那么只会有一个线程执行该方法,其他线程阻塞等待。
四、类加载器
1、类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器
,都拥有一个独立的类名称空间
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,
否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的
类加载器不同,那这两个类就必定不相等(equals,isAssignableFrom,isInstance等方法)
2、双亲委派模型
三种类加载器: 启动类加载器、扩展类加载器、应用程序加载器
双亲委派模型工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求
委派给父类加载器去完成,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才
会尝试自己去加载
保障了类的一致性(各个类加载器的基础类的统一问题)
被破坏:JNDI、OSGI