虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
加载 --> 连接(验证 --> 准备 --> 解析)--> 初始化 --> 使用 --> 卸载
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定。
有且只有四种情况必须立即对类进行 “初始化”:
java.lang.reflect
包的方法对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化main()
方法的那个类),虚拟机会先初始化这个主类在加载阶段,虚拟机需要完成以下三件事情:
java.lang.Class
对象,作为方法区这些数据的访问入口会完成四个阶段的检验过程:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
几个注意点:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
概念说明:
类初始化阶段是类加载过程的最后一步,前面的类加载过程(准备阶段)中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。
初始化阶段是执行类构造器 <clinit>()
方法的过程:
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源(如类成员变量 -- 除了类变量以外的变量都属于类成员变量)。
几个应该记住的概念:
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}
块)中的语句合并产生的(会优先执行 static{})<clinit>()
方法执行之前,父类的 <clinit>()
方法已经执行完毕。因此在虚拟机中第一个被执行的 <clinit>()
方法的类肯定是 java.lang.Object
。<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
举个例子,下面的代码中运行结果 字段 B 的值将会是 2 而不是 1。
static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); }<clinit>()
方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>()
方法。<clinit>()
方法。只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类是来源于同一个 Class 文件,只要它们的类加载器不同,那这两个类就必定不相等。
java.lang.ClassLoader
<JAVA_HOME>\lib
目录中的,或者被 -Xbootclasspath
参数所指定的路径中,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用。
sun.misc.Launcher$ExtClassLoader
实现
负责加载 <JAVA_HOME>\lin\ext
目录中的,或者被 java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
sun.misc.Launcher$AppClassLoader
来实现
由于这个类加载器是 ClassLoader
中的 getSystemClassLoader()
方法的返回值,所以一般也称它为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应该程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
上图:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
如果一个类加载器收到了类加载的请求,它首先不会自己先尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNUll(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出 ClassNotFoundException
// 则说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候
// 再调用本身的 findClass 方法进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass()
方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,则在抛出 ClassNotFoundException
异常后,再调用自己的 findClass()
方法进行加载。
java.lang.Thread
类的 setContextClassLoaser()
方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。