java类加载过程

类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,他的整个生命周期总共有七个阶段:加载(loading)、验证(verification)、准备(preparation)、解析(resolution)、初始化(initialization)、使用(using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以被统称为连接(linking)。

如下图所示:

JAVA类加载机制的详细解析 - 编程语言 - 亿速云

类加载过程

class文件需要加载到虚拟机之后才能运行和使用,系统加载class类型的文件主要分为三步:加载—>连接—>初始化。连接过程又可分为:验证—>准备—>解析

Java类加载机制 - 知乎

1.加载

类加载过程的第一步,主要完成下面3件事情:

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

加载这一步主要是通过 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。

2.验证

该阶段主要是为了保证加载进来的字节流符合JVM的规范,不会对JVM有安全性问题。其中有对元数据的验证,例如检查类是否继承了被final修饰的类;还有对符号引用的验证,例如校验符号引用是否可以通过全限定名找到,或者是检查符号引用的权限(private、public)是否符合语法规定等。

验证阶段主要由四个检验阶段组成:

  1. 文件格式验证(Class 文件格式检查)
  2. 元数据验证(字节码语义检查)
  3. 字节码验证(程序语义检查)
  4. 符号引用验证(类的正确性检查)

验证阶段示意图

3.准备

准备阶段的主要任务是为类的类变量开辟空间并赋默认值

  1. 静态变量是基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0
  2. 静态变量的引用类型赋给默认值null
  3. 静态常量默认值为声明时设定的值

这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。

4.解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析的动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

《深入理解 Java 虚拟机》7.34 节第三版对符号引用和直接引用的解释如下:

符号引用和直接引用

在程序执行方法时,系统需要明确知道这个方法的所在位置,java虚拟机为每个类都准备了一张方法表来存放类中的所有方法。当需要调用某个类的某个方法的时候,只需要知道这个方法在方法表中的偏移量既可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

5.初始化

初始化阶段是执行初始化方法()方法的过程,是类加载的最后一步,这一步jvm才开始真正执行类中定义的java程序代码(字节码)在这个方法中所有的类变量都会被赋予正确的值,也就是编写的时候指定的值。这个方法中包含所有类变量的赋值动作和静态语句块的执行代码,

另外() 方法和类的构造函数有所不同,它不需要显示的调用父类的构造器,虚拟机会保证父类的 () 方法最先执行,因此父类的静态变量总是能够得到优先赋值。

当主动使用触发了类的初始化之后就会调用这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
static {
try {
System.out.println("static code will be inviked");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
IntStream.range(0,5).forEach(i -> new Thread(DeadLock::new));
}

执行上面的代码片段,在同一个世界,只能有一个线程执行到静态代码块中的内容,并且静态代码块中仅仅会被执行一次,JVM 保证了 () 方法在多线程的执行环境下的同步

类的加载时机

  1. 创建该类的实例(比如new)
  2. 调用该类的类方法
  3. 访问类或接口的类变量,或为类变量赋值
  4. 利用反射Class.forName(String name, boolean initialize,ClassLoader loader);
    当使用ClassLoader类的loadClass()方法来加载类时,该类只进行加载阶段,而不会经历初始化阶段,使用Class类的静态方法forName(),根据initialize来决定会不会初始化该类,不传该参数默认强制初始化
  5. 初始化该类的子类
  6. 运行main方法,main方法所在的类会被加载

类的加载顺序

  1. 先加载并连接当前类

  2. 父类没有被加载,则去加载,连接,初始化父类,依旧是先加载并连接,再去判断有无父类,如此循环。所以jvm始终先加载object类

  3. 如果类中有初始化语句,包括声明时赋值与静态初始化块,则按顺序进行初始化

    类的初始化顺序:先执行父类静态变量赋值、父类静态初始化块,再执行子类静态属性赋值、静态初始化块。

类的卸载

卸载类即该类的class对象被GC

卸载类需要满足三个要求:

  1. 该类的所有实例对象已经被GC,也就是说堆中不存咋该类的实例对象
  2. 该类没有在其他地方被引用
  3. 该类的类类加载器的实例已经被GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

只要想通一点就好了,JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

类的加载器

类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。

每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

简单来说,类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。 字节码可以是 Java 源程序(.java文件)经过 javac 编译得来,也可以是通过工具动态生成或者通过网络下载得来。

JVM 中内置了三个重要的 ClassLoader

  1. BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jarresources.jarcharsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。

  2. ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。

  3. AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类

    除了这三种类加载器之外,还有一种用户自定义加载器,用来拓展,满足自己的特殊需求。

Java的类加载器种类 - JAVA366