Java类加载过程详解

类的生命周期

首先我们应明确在Java中类的生命周期是什么,引用一张图像进行说明

Java类加载过程详解_第1张图片

包括以下 7 个阶段:

  • 加载(Loading)

  • 验证(Verification)

  • 准备(Preparation)

  • 解析(Resolution)

  • 初始化(Initialization)

  • 使用(Using)

  • 卸载(Unloading)


类加载时机

对于什么时候加载,Java虚拟机规范中并没有约束,各个虚拟机都可以按自身需要来自由实现。但绝大多数情况下,都遵循“什么时候初始化”来进行加载。

什么时候初始化?Java虚拟机规范有明确规定,当符合以下条件时(包括但不限于),虚拟机内存中没有找到对应类型信息,则必须对类进行“初始化”操作:

  • 使用new实例化对象时、读取或者设置一个类的静态字段或方法时
  • 反射调用时,例如 Class.forName("com.xxx.MyTest")
  • 初始化一个类的子类,会首先初始化子类的父类
  • Java虚拟机启动时标明的启动类
  • 当使用ClassLoader类的loadClass()方法来加载类时,该类只进行加载阶段,而不会经历初始化阶段,使用Class类的静态方法forName(),根据initialize来决定会不会初始化该类,不传该参数默认强制初始化
  • 运行main方法,main方法所在类会被加载
  • JDK8 之后,接口中存在default方法,这个接口的实现类初始化时,接口会其之前进行初始化
初始化阶段开始之前,自然还是要先经历 加载、验证、准备 、解析的。

类加载过程

Java类加载过程包括加载、验证、准备、解析和初始化五个过程。其中加载、验证、初始化是类加载的三个基本阶段,准备和解析是可选的。

首先类加载过程离不开类加载器的帮助

Java类加载过程详解_第2张图片

类的加载器负责类的加载职责,任何一个对象的class 在JVM 中都只存在唯一的一份。

JVM 有三大类加载器,不同的加载器负责将不同的类加载到JVM 内存之中,并且它们之中严格遵守这父委托机制。

Bootstrap

根加载器,为最顶层的加载器,主要负责虚拟机核心类库的加载如Java.lang库的加载,可以通过-Xbootclasspath 来指定根加载器的路径,也可以通过类属性来得知当前Jvm 的根加载器都加载类哪些资源。

System.out.println("Bootstrap" + String.class.getClassLoader());
   null
System.out.println(System.getProperty("sun.boot.class.path"));
   /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar:
   /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar

第一个获取的classloader 会是null,是因为根类加载器是获取不到引用的。

第二个输出更加在的加载路径

ExtClassLoader

扩展类加载器,这个类加载器是根加载器,它主要是加载JAVA_HOME 下的jre/lb/ext 目录里面的类库

 System.out.println(System.getProperty("java.ext.dirs"));

获取扩展类加载资源的路径

ApplicationClassLoader

系统类加载器是一种常见的类加载器,其负责加载 classpath 下的类库资源,

System.out.println(System.getProperty("java.class.path"));
System.out.println(DeadLock.class.getClassLoader());

第一个打印出当前系统类加载器资源的路径,

第二个打印出当前类的类加载器,会输出

sun.misc.Launcher$AppClassLoader@18b4aac2

具体来说,类的加载过程包括以下步骤:

加载:Java虚拟机通过类加载器查找并加载类的二进制数据,将类的二进制数据读入内存,并在堆区创建一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。

其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。

  • 从网络中获取,最典型的应用是 Applet。

  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。

  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

验证:验证二进制字节流是否符合Java虚拟机规范,保证被加载的类的正确性和安全性。

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

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

Java类加载过程详解_第3张图片

准备:为类的静态变量分配内存并设置默认初值,使用的是方法区的内存 。

解析将类中的符号引用转化为直接引用,解析常量池中的符号引用,并将符号引用指向类中相应的直接引用。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

初始化:为类的静态变量赋予正确的初始值,并执行类构造器方法。

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 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);  // 2
}

可以在Java接口中使用static进行修饰。但不能修饰静态语句块,因为接口中只能包含常量和方法,而静态语句块并不属于常量或方法的范畴。接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。

但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。

虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

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

类的主动使用和被动使用

并不是代码中出现了这个类,就会将其进行加载,要区分类出现与使用的场景。

类的主动使用

此时将会触发类加载,并初始化

  • new 关键字创建一个类
  • 访问类的静态变量包括读取和更新都会使得类初始化
  • 访问类的静态方法包括读取和更新都会使得类初始化
  • 对类进行反射操作的时候
  • 初始化子类导致父类的初始化
  • 当一个类中包括main 函数的时候,当这个main 函数得到启动的时候,这个类会初始化

类的被动使用

关于Java类的被动使用,当我们通过类名引用静态字段时,只会触发该字段所在类的初始化过程,而不会触发该类的类加载过程。其中,静态字段必须声明为final类型和已在编译期把结果放入常量池的常量才不会触发类的初始化过程。如此,就实现了在不触发完整的类加载过程的情况下使用类的常量。

此时不会触发类加载与初始化

  • 访问类的静态常量(final 修饰的变量)不会使得类初始化
  • 构建数组的时候并不会使得类初始化

双亲委派模型 (Parents Delegation Model)

先看英语介绍

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.

在类加载的过程中,Java虚拟机采用了双亲委派模型,即类加载请求会先被委派给父类加载器进行加载,只有当父类加载器无法加载时,才由子类加载器进行加载。这样可以避免同一个类被重复加载,保证类的唯一性和正确性。

JVM 判定两个 Java 类是否相同的具体规则 :JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。


著作权归所有 原文链接:https://javaguide.cn/java/jvm/classloader.html

类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。

public abstract class ClassLoader {
  ...
  // 组合
  private final ClassLoader parent;
  protected ClassLoader(ClassLoader parent) {
       this(checkCreateClassLoader(), parent);
  }
  ...
}

为什么类加载器之间的父子关系通过组合而实现?

Java类加载器之间的父子关系通过组合而不是继承实现,主要是因为继承实现会导致类加载器之间的依赖关系变得非常复杂,容易出现循环依赖的情况,进而导致类加载器的死锁和其他问题,在这种情况下,应用程序可能会抛出ClassCircularityError。

另外,通过组合实现类加载器的父子关系,可以使得类加载器之间的关系更为灵活,这是因为组合可以在运行时动态地将类加载器之间的关系构造出来,而不需要在编译时就确定好。

如果类加载器的父子关系通过继承实现而不是组合实现,可能会导致以下问题:

  1. 类加载器之间出现循环依赖,进而导致死锁等问题。

  2. 类加载器不容易进行扩展和修改,因为继承关系是一种静态的关系,很难在运行时动态地修改。

  3. 继承关系容易导致类加载器之间的耦合度变得非常高,使得代码的可维护性变得很低。

总的来说,通过组合实现类加载器的父子关系,可以使得类加载器之间的关系更为灵活,避免出现循环依赖的问题,并且方便进行扩展和修改,提高代码的可维护性和可扩展性。

双亲委派模型的好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。


用程序配合解释类加载的过程

以下是一个简单的Java类,通过打印出不同阶段的输出,展示了类的加载过程:

public class ClassLoadingDemo {
    static {
        System.out.println("ClassLoadingDemo init!");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class clazz = classLoader.loadClass("ClassLoadingDemo");
        System.out.println(clazz.getName());
    }
}

输出结果:

ClassLoadingDemo init!
ClassLoadingDemo

可以看到,在类加载器加载ClassLoadingDemo类时,打印了初始化阶段的输出。

每个类的唯一性都是由其自己,以及谁加载了它决定的。我们可以通过实验验证每个类都有自己的类加载器

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLoaderDemo's ClassLoader is "+ClassLoaderDemo.class.getClassLoader());
        System.out.println("The parent of ClassLoaderDemo's ClassLoader is "+ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The grandparent of ClassLoaderDemo's ClassLoader is "+ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}

输出结果是:

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoadernull的话,那么该类是通过 BootstrapClassLoader 加载的。

为什么 获取到 ClassLoadernull就是 BootstrapClassLoader 加载的呢? 这是因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。


著作权归所有 原文链接:https://javaguide.cn/java/jvm/classloader.html

双亲委派模型的执行过程

整个过程集中在java.lang.ClassLoader库的loadClass()中,相关代码与注释来自参考资料

protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}
------
著作权归所有
原文链接:https://javaguide.cn/java/jvm/classloader.html

 每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

Java类加载过程详解_第4张图片

小节双亲委派模型的执行流程:

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。

参考资料:

类加载器详解(重点) | JavaGuide(Java面试+学习指南) 

Java 类加载过程 - 知乎 (zhihu.com)

(2条消息) 请你说说Java类的加载过程_java类加载_LuckyWangxs的博客-CSDN博客

Java类加载机制 - 知乎 (zhihu.com)

你可能感兴趣的:(Java,java,jvm,开发语言,面试)