类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析和使用是不一定保证顺序的。
第一阶段--加载
虚拟机规范中没有强制什么时候加载,但是有初始化阶段,这个阶段是在加载-验证-准备之后进行的,只有以下五种情况必须对类进行初始化:
- 遇到new、getstatic、putstatic(读取或设置一个静态字段)或invokestatic(调用类的静态方法)这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
2.反射方法调用
3.初始化时,如果父类还未初始化,先初始化父类。
4.虚拟机启动时指定执行一个main类
5.当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
过程:
虚拟机完成三件事:
- 通过一个类全限定类名获取定义此类的二进制流
2.将字节流中的静态存储结构转化为方法区的运行时数据 - 内存中生成一个该类Class对象,作为方法区这个类各种数据的访问入口
类加载与接口加载区别:
接口与类真正有所区别的是前面讲述的5种“有且仅有”需要开始初始化场景中的第3种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
第二阶段---验证
目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
4个阶段的检验动作:
-
文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
是否以魔数0xCAFEBABE开头。
主、次版本号是否在当前虚拟机处理范围之内。
常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。 -
元数据验证
对字节码描述的信息进行语义分析 -
字节码验证
确定程序语义是合法的、符合逻辑的。 -
符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。
第二阶段是准备阶段
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
第三阶段是解析阶段
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码).
字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
ClassLoader类加载器定义
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
作用
负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎:负责解释命令,提交操作系
统执行)决定
分类
虚拟机自带的加载器
• 启动类加载器(Bootstrap)C++实现
启动类加载器(Bootstrap ClassLoader):这个将负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的。仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
其他类加载器
• 扩展类加载器(Extension)Java
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
• 应用程序类加载器(AppClassLoader) Java也叫系统类加载器,加载当前应用的classpath的所有类。
这个类加载器由sun.misc.Launcher $App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
• 用户自定义加载器
Java.lang.ClassLoader的子类,用户可以定制类的加载方式。
具体实例如图所示:
以上展示了类加载器之间的层次关系,称为类加载器的双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
那么我们如何去验证类加载器的存在呢??
可以写一段测试代码:
public class ClassLoadTest {
public static void main(String[] args) {
//输出该类的类加载器的父类的父类,应该是bootstrap类加载器
System.out.println(new ClassLoadTest().getClass().getClassLoader().getParent().getParent());
//输出该类的类加载器的父类,ext 扩展类加载器
System.out.println(new ClassLoadTest().getClass().getClassLoader().getParent());
//输出该类的类加载器
System.out.println(new ClassLoadTest().getClass().getClassLoader());
System.out.println("===================");
System.out.println(new Object().getClass().getClassLoader());
System.out.println("null 代表是bootstrap的classloader,会输出为null");
}
}
输出:
看下这个例子:
public class String {
public static void main(String[] args) {
new String();
}
}
运行结果:
我们都知道,String属于默认加载类的,在双亲委派机制中,上一级类加载器先不加载,由最下面的开始加载,如果下面的加载不到,再委派给上一级类加载器去加载。
双亲委派机制是什么?
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这个机制就叫双亲委派机制。
好处:
java类随着类加载器一起具备了带有优先级的层次,比如Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。能够保证加载的唯一性和是同一个类。
相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
双亲委派机制的实现
1.首先,检查请求的类是否已经被加载过了
2.未加载,则请求父类加载器去加载对应路径下的类,
3.如果加载不到,才由下面的子类依次去加载。
待续
网友评论