一、前言

平时小伙伴们在做业务开发的时候可能比较少去接触类加载器,但是如果你想要深入学习Tomcat这种开源项目的话,熟悉类加载的原理就是必须的了。

二、类加载器

类加载器,顾名思义,就是一个可以将java字节码加载为java.lang.Class实例的工具。

类加载器有以下两个特点:

1.动态加载:不需要在程序一开始运行的时候就去加载,而是在程序运行的过程中,动态地按需加载,.class字节码可以来源于各个地方,比如jar包、war包、网络中等等。

2.全盘负责:当一个类加载器去加载一个类时,这个类所依赖的、引用的其它所有的类都将由这个类加载器去加载,除非在程序中显示地指定另外一个类加载器加载。

一个类的唯一性是由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实力ID作为唯一标识,也就是PackageName+ClassName+ClassLoader Id),因此在一个运行程序中有可能存在两个包名和类名完全一致的类,但是如果这两个类不是由同一个ClassLoader加载的,就会被视为两个不同的类。比较两个类是否相等(包括Class对象的equals()、isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。

从实现方式上,类加载器可以分为两种:一种是启动类加载器,是由C++语言实现的,是虚拟机自身的一部分;另一种是继承于java.lang.ClassLoader的类加载器,包括扩展类加载器、应用程序类加载器以及自定义类加载器。

启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>lib目录中的jar包的,或者被-Xbootclasspath参数所指定的路径,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果想设置Bootstrap ClassLoader为其parent,可直接设置null。

image-20250922111323396.png

我们写个代码来测试一下启动类加载器的打印:

image-20250922112213670.png

为什么打印的不是“Bootstrap ClassLoader”而是 null 呢? 这是因为启动类加载器(Bootstrap ClassLoader)是由 C++ 实现的,而这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>libext目录中的jar包的,或者被java.ext.dirs系统变量所指定路径中的所有类库。该类加载器由sun.misc.Launcher$ExtClassLoader实现。扩展类加载器由启动类加载器加载,其父类加载器为启动类加载器,即parent=null。

image-20250922111349805.png

我们写个代码来测试一下扩展类加载器的打印:

image-20250922112514442.png

应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,由sun.misc.Launcher$App-ClassLoader实现。开发者可直接通过java.lang.ClassLoader中的getSystemClassLoader()方法获取应用程序类加载器,所以也可称它为系统类加载器。应用程序类加载器也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。在一个应用程序中,系统类加载器一般是默认类加载器。

应用程序类加载器是用来加载 classpath 也就是用户写的所有类的,接下来我们写代码测试一下应用程序类加载器的打印,代码如下:

image-20250922112711112.png

三、双亲委派机制

双亲委派机制,可以用一句话来说表达:任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载

双亲委派机制是java类加载器的一种工作模式,通过这种工作模式,java虚拟机将类文件加载到内存中,这样就保证了java程序能够正常的运行起来。JVM 并不是在启动时就把所有的.class文件都加载进来,而是程序在运行过程中用到了这个类才去加载。除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。

public abstract class ClassLoader {

    //每个类加载器都有个父加载器
    private final ClassLoader parent;
    
    public Class<?> loadClass(String name) {
  
        //查找一下这个类是不是已经加载过了
        Class<?> c = findLoadedClass(name);
        
        //如果没有加载过
        if( c == null ){
          //先委派给父加载器去加载,注意这是个递归调用
          if (parent != null) {
              c = parent.loadClass(name);
          }else {
              // 如果父加载器为空,查找Bootstrap加载器是不是加载过了
              c = findBootstrapClassOrNull(name);
          }
        }
        // 如果父加载器没加载成功,调用自己的findClass去加载
        if (c == null) {
            c = findClass(name);
        }
        
        return c;
    }
    
    protected Class<?> findClass(String name){
       //1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
          ...
          
       //2. 调用defineClass将字节数组转成Class对象
       return defineClass(buf, off, len);
    }
    
    // 将字节码数组解析成一个Class对象,用native方法实现
    protected final Class<?> defineClass(byte[] b, int off, int len){
       ...
    }
}

从上面的代码可以得到几个关键信息:

  • JVM 的类加载器是分层次的,它们有父子关系,而这个关系不是继承维护(不是java中的extend继承),而是组合,每个类加载器都持有一个 parent 字段(private final ClassLoader parent;),指向父加载器。(AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是BootstrapClassLoader,但是ExtClassLoader的parent=null。)
  • defineClass 方法的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象。
  • findClass 方法的主要职责就是找到.class文件,并且把.class文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。子类必须实现findClass 。
  • loadClass 方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。

双亲委派机制原理:

  • 如果一个类加载器收到了要加载某个类的请求,它要做的首要事情不是直接去加载,而是将这个请求委托给父类的加载器去执行
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

image-20250922105054697.png

四、为什么要双亲委派

双亲委派机制保证类加载器自下而上的委派,又自上而下的去加载类,这样可以保证每一个类在各个类加载器中都是同一个类。

一个非常明显的目的就是保证java官方的类库<JAVA_HOME>lib和扩展类库<JAVA_HOME>libext的加载安全性,不会被我们开发者覆盖,加载过了,就不用再加载一遍。

例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。

如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。

五、总结

  1. java 的类加载,就是获取.class文件的二进制字节码数组并加载到 JVM 的方法区,并在 JVM 的堆区建立一个用来封装 java 类相关的数据和方法的java.lang.Class对象实例。
  2. java默认有的类加载器有三个,启动类加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用程序类加载器(也叫系统类加载器)(AppClassLoader)。类加载器之间存在父子关系,这种关系不是继承关系,是组合关系。如果parent=null,则它的父级就是启动类加载器。启动类加载器无法被java程序直接引用。
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]