0%

Java的类加载机制

类加载过程

类从被加载到虚拟机内存开始,直到被卸载出内存为止,它的整个生命周期过程是:

加载 —> 验证 —> 准备 —> 解析 —> 初始化 —> 使用 —> 卸载

加载

加载是类加载过程的第一个阶段,在加载阶段虚拟机需要完成三件事。

  1. 通过一个类的全限定名来获取其定义的二进制字节流
  2. 将这个字节流所表示的静态存储结构转化为方法区运行时数据结构
  3. 在内存中生成一个代表这个类的class对象,作为方法区的各种数据的访问入口

类加载器

类的加载的全都是交给类加载器去实现的。

在Java虚拟机规范中,加载器是分为两大类:

  1. 引导类加载器

    引导类加载器是使用本地代码实现的类加载器,负责将<JAVA_HOME>/lib下的核心类库或-Xbootclasspath选项指定的jar包给加载到内存中。

    这个加载器是属于虚拟机部分的类,Java中是无法获取到的

  2. 自定义加载器

    自定义加载器可以分为三类:

    • 扩展类加载器

      扩展类加载器是负责将<JAVA_HOME>/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中

    • 应用程序类加载器

      应用程序类加载器是将classPath中所指定的类,一般来说Java类都是由这个类加载器来加载的

    • 自定义类加载器

      除了系统提供的类之外,也可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器

类的加载顺序 —– 双亲委派模型

他们之间的关系如下图所示:

JVM加载类的时候默认采用的是双亲委派机制,
也是是说在某个类加载器在接收到加载类的请求的时候,
首先是将加载任务委托给父类加载器,依次递归,如果父类加载器能够完成加载就成功返回,
只有当父类加载器无法完成加载任务时候,才会去调用自己去加载类。

在JDK中有默认的扩展类加载器和应用程序类加载器

sun.misc.Launcher$ExtClassLoader 和 sun.misc.Launcher$AppClassLoader

可以用代码看一下他们的加载关系:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
}

运行结果:

1
2
3
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@60e53b93
null

这个运行结果能够验证我们之前双亲委派模型的图,应用程序类加载器调用扩展类加载器最后调用一个访问不到的引导类加载器

他们的依赖关系:

图片说明 图片说明

他们最后都是调用ClassLoader这个里面的loadClass实现类加载

双亲委派模型的类加载逻辑:

  1. 首先检查c有没有被加载
  2. 如果没有没加载,查看父类加载器是否为空,为空调用父类加载器加载,否则调用引导类加载器加载
  3. 如果c在父类加载器加载下没有加载成功,会调用当前加载器的findClass进行加载
  4. 若class最终被加载,且resolve为true,则对该class进行链接操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {
    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if (c == null) {
    long t0 = System.nanoTime();
    try {
    if (parent != null) {
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }

    if (c == null) {
    // If still not found, then invoke findClass in order
    // to find the class.
    long t1 = System.nanoTime();
    c = findClass(name);

    // this is the defining class loader; record the stats
    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    sun.misc.PerfCounter.getFindClasses().increment();
    }
    }
    if (resolve) {
    resolveClass(c);
    }
    return c;
    }
    }

验证

验证阶段用于确保类或接口的二进制结构上是正确的。需要满足Java虚拟机限制中描述的静态或者结构上的约束

准备

准备阶段的任务是为类或接口的静态字段分配空间,并用初始值初始化这些字段,这个阶段不会执行任何的虚拟机字节码指令。
也是就说不会初始化任何实例变量。

初始值是指Java虚拟机规范所支持的数据类型和对应的初始值。
比如:

1
public static int value = 2

它最准备阶段被赋予的初始值是0而不是2。

在类的对象创建之后的任何时间,都可以进行准备阶段,但它得确保一定要在初始化阶段开始前完成。

解析

解析是根据运行时常量池的符号引用来动态决定具体的值的过程。
总共要解析六种类型:

  • 类与接口解析

  • 字段解析

  • 普通方法解析

  • 接口方法解析

  • 方法类型与方法句柄解析

  • 调用点限定符解析

初始化

初始化过程才是虚拟机真正执行Java代码的过程。

会触发初始化过程的有这几种行为:

  1. 在被选为Java启动的初始类
  2. 在对于某个类的子类初始化时
  3. 在调用JDK类库的反射方法时
  4. 在调用java.lang.invoke.MethodHandle时,如果检测出来有static方法时
  5. 在执行new方法、初始代码块或静态代码块时

在多线程环境下,虚拟机会保证一个类的初始化方法会被正确的加锁和同步。

使用

在初始化完成之后才能够对类进行调用

卸载

卸载时Java虚拟机将类移出内存的过程。