乐趣区

彻底搞懂JVM类加载器基本概念

写在前面

在 Java 面试中,在考察完项目经验、基础技术后,我会根据候选人的特点进行知识深度的考察,如果候选人简历上有写 JVM(Java 虚拟机)相关的东西,那么我常常会问一些 JVM 的问题。JVM 的类加载机制是一个很经典的知识点,围绕这个知识点可以有下面这些难度不同的问题。

  1. 简单讲下 JVM 中的类加载过程
  2. JVM 中的类加载和卸载的时机?
  3. 如何理解 JVM 中不同类加载器的概念和作用?
  4. 简单讲下 JVM 中的双亲委派模型?
  5. 什么情况下会破坏双亲委派模型?为什么?可否举个例子?
  6. Tomcat 中的类加载机制有了解吗?为什么这么设计?
  7. 实际开发中有遇到哪些类加载器相关的问题?你又是如何解决的?
  8. JVM 之上的弱类型语言例如 Groovy 是如何实现?简单讲下动态类加载机制?

在接下来的几篇文章,我将跟读者一起重新梳理一遍类加载器的相关知识,争取能够妥善解答上面列出的这些问题。

基本概念篇

类的加载和卸载

JVM 是虚拟机的一种,它的指令集语言是字节码,字节码构成的文件是 class 文件。平常我们写的 Java 文件,需要编译为 class 文件才能交给 JVM 运行。可以这么说:C 语言代码——> 二进制文件——> 计算机硬件,就相当于 Java 代码——> 字节码文件——>JVM。JVM 将指定的 class 文件读取到内存里,并运行该 class 文件里的 Java 程序的过程,就称之为 类的加载 ;反之,将某个 class 文件的运行时数据从 JVM 中移除的过程,就称之为 类的卸载

class 文件的运行时数据就是 C ++ 对象,也称为 kclass 对象,这些运行时数据在 JDK7 之前是放在永久代(PermGen),JDK8 之后则放在元空间(Metaspace)。

类的生命周期

Java 类从被虚拟机加载开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段;其中验证、准备和解析又统称为连接(Linking)阶段。

类的加载的时机

虚拟机规范并未严格规定类加载的时机,跟具体的 JVM 虚拟机有关。类加载的最佳时机是解析 Java 字节码类文件中常量池符号的时候,Class.forName()、ClassLoader.loadClass()、反射 API 和 JNI_FindClass 都可以触发类加载,Hot JVM 自身启动的时候也会触发类加载。

通过 JVM 参数中加-verbose:class,可以在应用启动的时候打印类加载的过程,如下图所示:

初始化这个阶段,JVM 虚拟机给出了 5 种必须对类进行“初始化”的情况

  1. 使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段的时候、调用一个类的静态方法的时候;
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要先触发其初始化;
  3. 当初始化一个类的时候,如果发现其父类还没有被初始化,则要先初始化其父类;
  4. 当虚拟机启动时,用户需要指定一个执行的主类(包含 main 方法的那个类),则虚拟机会优先初始化这个主类;
  5. 在 JDK1.7 以后,动态语言支持的时候,如果一个 java.lang.invoke.MethodHandle 实例最后的结果是要执行第 1 种情况的操作,则也要进行初始化。

类的卸载时机

类的卸载跟采用的垃圾收集算法有关,在 CMS 中有两种方法卸载不必要的类,一种是等到元空间(Metaspace)满了的时候触发 FGC,另一种是使用跟 CMS 并发收集算法类似的方式,不过对于元空间的阈值和触发 CMS 并发收集的阈值是独立的。更具体的可以参考之前的文章:CMS 学习笔记。在这里,我们只需要记住,JVM 中一个类的卸载要满足下面这 3 个条件:

  1. 该类所有的实例对象都已被回收;
  2. 该类的类加载器对象已经被回收;
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

类加载器的作用

类的加载是需要类加载器完成的,但是类加载器在 JVM 中的作用可不止这些。在 JVM 中,一个类的唯一性是需要这个类本身和类加载一起才能确定的,每个类加载器都有一个独立的命名空间。

不同的类加载器,即使是同一个类字节码文件,最后再 JVM 里的类对象也不是同一个,下面的代码展示了这个结论:

package jvm;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
        InstantiationException {ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if (inputStream == null) {return super.loadClass(name);
                }
                try {byte[] b = new byte[inputStream.available()];
                    inputStream.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {throw new ClassNotFoundException();
                }
            }
        };

        Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof jvm.ClassLoaderTest);

        ClassLoaderTest classLoaderTest = new ClassLoaderTest();
        System.out.println(classLoaderTest.getClass());
        System.out.println(classLoaderTest instanceof jvm.ClassLoaderTest);
    }
}

上述代码的运行结果是:

可以看出,代码中使用自定义类加载器(myLoader)加载的 jvm.ClassLoaderTest 类和通过应用程序类加载器加载的类不是同一个类。综上,类加载器在 JVM 中的作用有:

  1. 将类的字节码文件从 JVM 外部加载到内存中
  2. 确定一个类的唯一性
  3. 提供隔离特性,为中间件开发者提供便利,例如 Tomcat

总结

今天的文章,应该可以回答文章开始提出的前两个问题,下篇再会。

参考资料

  1. https://jrebel.com/rebellabs/do-you-really-get-java-classloaders/
  2. https://stackoverflow.com/questions/2424604/what-is-a-java-classloader
  3. https://docs.oracle.com/javase/9/docs/api/java/lang/ClassLoader.html
  4. https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
  5. https://blogs.oracle.com/sundararajan/understanding-java-class-loading
  6. 《深入理解 Java 虚拟机》
  7. 《揭秘 Java 虚拟机》
  8. 《Java 性能权威指南》

本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。

退出移动版