类加载器简介

一、ClassLoader简介

               Java的类加载器的作用就是在运行时加载类到虚拟机中,首先不管对于什么样的java应用肯定是由很多class组成实现的,不同的功能所在的class是不一样的(你说我把所有的功能放在一个class里面,那你很棒棒哦),比如当我们的入口函数被调用的时候,入口函数使用了其他class(静态代码块、实例)的功能,这时类加载器就会按需加载将需要的类加载进内存中,类加载器不是一次性全部class加载到内存中,而是按需加载。类加载器加载class的原则是:委托、可见性和单一性。

 单一性:单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。

 可见性:可见性是指子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类 。     

 委托:这里重点说下双亲委托机制,说这个之前我们先看下jvm定义的类加载器:

Bootstrap ClassLoader: 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等等。另:我们是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。
Extention ClassLoader: 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
AppclassLoader:        也称为SystemAppClass 加载当前应用的classpath的所有类。

           除了以上3个jvm定义的类加载器,我们也可以自定义类加载器(通过继承ClassLoader这里就不说了)。这里的父类加载器不是继承关系,而是持有一个应用!类的加载流程用一句话简单说就是向上委托,向下查找加载。流程如下:

 

二、类加载器的委托加载

          看了这3个类加载器加载路径和执行顺序后,为了更好的理解,我们从代码方面看下具体的加载路径和加载顺序。我们先看下sun.misc.Launcher,它是java程序的入口,这个类的代码结构如下:
 

public Launcher() {
    // Create the extension class loader
    ClassLoader extcl;
    try {
        extcl = ExtClassLoader.getExtClassLoader();
    } catch (IOException e) {
        throw new InternalError(
            "Could not create extension class loader", e);
    }
    // Now create the class loader to use to launch the application
    try {
        loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
            "Could not create application class loader", e);
    }
    //设置AppClassLoader为线程上下文类加载器
    Thread.currentThread().setContextClassLoader(loader);
}
static class AppClassLoader extends URLClassLoader {	}

static class ExtClassLoader extends URLClassLoader{}

public ClassLoader getClassLoader() {
   return this.loader;
}

从代码看出,Launcher在构造器中初始化了ExtClassLoader和AppClassLoader,但是没有Bootstrap类加载器的信息。但是有个路径:sun.boot.class.path,这就是BootstrapClassLoader加载jar的路径,我们看下BootstrapClassLoader加载哪些jar或classes:

public static void main(String[] args) {
    System.out.println(System.getProperty("sun.boot.class.path"));
}

/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes

BootstrapClassLoader因为不是Java写的类加载器,所以在这里是看不到的,但是它也是要去加载类的,通过BootClassPathHolder类可以发现其加载范围是由System.getProperty("sun.boot.class.path")决定的。上面我们也看到了其加载的路径了。

ExtClassLoader类加载器,我们首先看下ExtClassLoader的代码:

static class ExtClassLoader extends URLClassLoader {
    //The class loader used for loading installed extensions.
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
        final File[] var0 = getExtDirs();
        try {
            return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                public Launcher.ExtClassLoader run() throws IOException {
                    int var1 = var0.length;
                    for(int var2 = 0; var2 < var1; ++var2) {
                        MetaIndex.registerDirectory(var0[var2]);
                    }
                    return new Launcher.ExtClassLoader(var0);
                }
            });
        } catch (PrivilegedActionException var2) {
            throw (IOException)var2.getException();
        }
    }
    //获取加载的路径
    private static File[] getExtDirs() {
        String var0 = System.getProperty("java.ext.dirs");
        File[] var1;
        if (var0 != null) {
            StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
            int var3 = var2.countTokens();
            var1 = new File[var3];
            for(int var4 = 0; var4 < var3; ++var4) {
                var1[var4] = new File(var2.nextToken());
            }
        } else {
            var1 = new File[0];
        }
        return var1;
    }
    static {
        ClassLoader.registerAsParallelCapable();
    }
    .....省略
}

这里我们又看到了一个路径 java.ext.dirs,这个是ExtClassLoader的加载路径,我们看下它是加载了哪些路径:

public static void main(String[] args) {
    System.out.println(System.getProperty("java.ext.dirs"));
}
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext
/Network/Library/Java/Extensions
/System/Library/Java/Extensions

再看看AppClassLoader部分,接着看它的代码部分:

static class AppClassLoader extends URLClassLoader {

    final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }

    public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
        int var3 = var1.lastIndexOf(46);
        if (var3 != -1) {
            SecurityManager var4 = System.getSecurityManager();
            if (var4 != null) {
                var4.checkPackageAccess(var1.substring(0, var3));
            }
        }
        if (this.ucp.knownToNotExist(var1)) {
            Class var5 = this.findLoadedClass(var1);
            if (var5 != null) {
                if (var2) {
                    this.resolveClass(var5);
                }
                return var5;
            } else {
                throw new ClassNotFoundException(var1);
            }
        } else {
            return super.loadClass(var1, var2);
        }
    }

    static {
        ClassLoader.registerAsParallelCapable();
    }
}

可以看到AppClassLoader加载的就是java.class.path下的路径。我们同样打印它的值。

public static void main(String[] args) {
    System.out.println(System.getProperty("java.class.path"));
}
//这里打印出来很多的,我这里只是粘贴出来我们当前运行的这个工程
/Users/pengjian/work/workspace/pepsi/pepsi-master/tomcat-demo/target/classes

现在3个类加载器已知晓其加载的路径了,下面我们用事例来看下它的加载顺序是什么。先看一个简单的代码片段:

public class SimpleClass {
    
}

public class ClassLoaderTest {
    public static void main(String[] args) {
        //当前类加载器
        ClassLoader cl = SimpleClass.class.getClassLoader();
        System.out.println(cl);
        //父类加载器
        ClassLoader parentCLoader= cl.getParent();
        System.out.println(parentCLoader);
        //父类的父类
        ClassLoader grandParentCl= parentCLoader.getParent();
        System.out.println(grandParentCl);
    }
}
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@23fc625e
null

这里不难看出,当前SimpleClass是我的应用中的类,是应该有AppClassLoader加载,通过输出也可以看出,其AppClassLoader的父类加载器是ExtClassLoader。但是为什么ExtClassLoader的父类是null呢?我们看下Launcher中的代码:

ClassLoader extcl;
//没有设置父类
extcl = ExtClassLoader.getExtClassLoader();
//设置父类加载器是extcl
loader = AppClassLoader.getAppClassLoader(extcl);

          这里先说下AppClassLoader和ExtClassLoader,有个父类URLClassLoader,ExtClassLoader在调用 父类构造方法的时候传入的父类加载器为null,所以我们这里输出出来的ExtClassLoader父类加载器为null。可能有人要问了,既然是null为什么可以当作ExtClassLoader的父类加载器去加载java_home/lib下面的jar和classes?
           因为Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用。
          我们在从代码层面去看当需要Bootstrap ClassLoader加载的时候是怎么loadClass的,先看下loadClass方法代码:

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;
    }
}

这段代码基本上解释了上面所有的问题。
1、先是执行findLoadedClass(String)去检测这个class是不是已经加载过了。 
2、执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器,这里最终加载方法是一个本地方法。 
3、如果向上委托父加载器没有加载成功,则通过findClass(String)查找。
总结一下类加载器:
1、java 定义了3个ClassLoader,用于加载不同路径的class类。
2、ClassLoader通过双亲委托来按需加载指定路径下的class和资源。 
3、另外,JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。           
下面一篇我们将说明一下自定义类加载器和tomcat的类加载器。 

### Java 线程上下文类加载器 (Thread Context ClassLoader) 的概念与用法 #### 一、线程上下文类加载器简介 线程上下文类加载器(Thread Context ClassLoader)是一个特殊的类加载器,它可以通过 `java.lang.Thread` 类中的方法 `setContextClassLoader(ClassLoader cl)` 进行设置,并通过 `getContextClassLoader()` 方法获取[^1]。默认情况下,线程的上下文类加载器继承自父线程的上下文类加载器。 这种机制允许某些框架或库动态指定用于加载资源或类的类加载器,而不需要依赖于系统的默认类加载器链。这在线程池场景下尤为重要,因为线程可能被重用多次,其初始类加载器环境可能会发生变化。 --- #### 二、为何使用线程上下文类加载器? 通常,在多模块或多层架构的应用程序中,不同层次的组件由不同的类加载器负责加载。如果某个高层组件需要访问低层组件所管理的类,则直接调用系统默认的类加载器可能导致无法找到目标类的情况。此时,可以利用线程上下文类加载器来解决这一问题[^2]。 SPI(Service Provider Interface)机制就是一个典型例子。服务提供者接口定义了一组抽象方法,具体实现则交由第三方开发者完成并打包到独立 JAR 文件中。当应用程序运行时,JDK 或其他框架会尝试从 SPI 配置文件中读取这些实现类的名字并通过反射实例化它们。为了能够正确加载来自外部 JAR 包内的类,就需要借助线程上下文类加载器。 --- #### 三、如何设置和获取线程上下文类加载器? 以下是关于如何操作线程上下文类加载器的具体方式: - **设置线程上下文类加载器** 可以通过以下代码片段为当前线程设定一个新的类加载器作为它的上下文类加载器: ```java Thread.currentThread().setContextClassLoader(customClassLoader); ``` - **获取线程上下文类加载器** 获取当前线程正在使用的上下文类加载器可通过此语句实现: ```java ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); ``` 上述两步展示了基本的操作流程[^3]。 --- #### 四、实际应用案例分析 考虑这样一个需求:在一个复杂的 Web 应用环境中,存在多个 WAR 文件部署在同一容器内共享同一个 JVM 实例。假设其中一个 Servlet 请求处理过程中需要用到另一个 WAR 中定义的服务对象。由于这两个 WAR 各自有自己专属的类加载器范围,因此单纯依靠标准双亲委派模型下的类查找路径不足以满足跨 WAR 调用的需求。这时就可以调整执行该逻辑所在线程的上下文类加载器指向后者所属的那个特定类加载器实例,从而成功定位所需的目标类型及其关联资源[^4]。 下面是基于前面提到背景的一个简化版演示代码: ```java package com.example; public class CustomClassLoaderExample { public static void main(String[] args) throws Exception { // 创建子类加载器 MyCustomClassLoader customClassLoader = new MyCustomClassLoader(); // 设置当前线程的上下文类加载器 Thread.currentThread().setContextClassLoader(customClassLoader); // 加载并创建目标类的对象 Object obj = customClassLoader.loadClass("com.target.MyTarget").newInstance(); System.out.println(obj.getClass().getClassLoader()); } } class MyCustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] b = loadClassData(name); // 假设这是某种形式的数据加载过程 return defineClass(name, b, 0, b.length); } private byte[] loadClassData(String className){ // 模拟数据加载... return null; } } ``` 在此示例中,我们手动设置了主线程的上下文类加载器为我们的定制版本 (`MyCustomClassLoader`) ,之后再依据此类加载器去检索原本不可见的远程包里的内容。 --- ### 总结 综上所述,理解并合理运用线程上下文类加载器对于构建灵活可扩展的企业级解决方案至关重要。无论是面对复杂分层结构还是异构插件体系设计挑战,掌握好这项技术都能带来显著帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值