Java类加载器(ClassLoader)机制详解


大部分人平时不会直接接触到ClassLoader,但ClassLoader作为Java的一个重要的核心特性却又和平时的编码工作息息相关,了解ClassLoader的机制有助于我们更好的了解Java的工作机制,同时对于学习OSGI,Web服务器等工作原理也有帮助

ClassLoader定义

无论是写一个简单的单文件程序,还是一个复杂的多模块程序,其大致都可分为下列几步:

  1. 代码人员将设计逻辑转换为Java语言逻辑并生成.java文件
  2. Java编译器将.java文件编译为Java字节代码(.class文件)
  3. ClassLoader加载.class文件并转换成java.lang.Class类的一个实例放入缓存,每个这样的实例用来表示一个 Java 类。后续通过此实例的 newInstance()方法就可以创建出该类的对象

所以ClassLoader的主要作用就是加载.class文件以供运行时使用

ClassLoader分类

在Java中,ClassLoader可大致分为两类,第一类为系统提供的,另外一类是由开发人员自行扩展的,其中系统提供的ClassLoader大致有三种,它们分别为:

  • 引导类加载器(Bootstrap ClassLoader);它用来加载 Java 的核心库,如:rt.jar、resources.jar等
  • 扩展类加载器(Extension ClassLoader);负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar
  • 应用类加载器(App ClassLoader);负责加载应用程序classpath目录下的所有jar和class文件

在这三种系统提供的ClassLoader中,引导类加载器较为特殊,这一点在后续会提到;而由开发人员自行扩展的ClassLoader则需继承java.lang.ClassLoader类并根据需要重写特定方法,一般重写findClass方法即可

ClassLoader工作机制

相信即便是不了解ClassLoader工作机制的人,也听说过双亲委派机制,双亲委派机制就是对ClassLoader的工作机制描述,除了引导类加载器之外,所有的类加载器都有一个父类加载器(可以通过 getParent()
方法可以查看,该父类加载器与当前类加载器不是继承关系,是关联关系),如应用类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器

ClassLoader loader = ClassLoaderStructure.class.getClassLoader();//获得加载当前类的类加载器
while(loader != null) {
    System.out.println(loader);
    loader = loader.getParent();//获得父类加载器的引用
}
System.out.println(loader);

//运行结果
sun.misc.Launcher$AppClassLoader@232204a1 //应用类加载器
sun.misc.Launcher$ExtClassLoader@14ae5a5 //扩展类加载器
null //引导类加载器,由于应到类加载器不继承与 java.lang.ClassLoader,由原生代码实现,所以这里显示是null

对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是应用类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器

C_S.png

当一个ClassLoader实例需要加载某个类时,它会首先检查这个类是否已经加载,这个过程是由下至上依次检查,若所有加载器均未加载,则先从顶层加载器开始试图加载,若加载失败,则把任务转交给扩展类加载器进行加载,如果也没加载到,则转交给应用类加载器进行加载,如果它依然没有加载到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类,这个过程是由上至下的。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象,这就是双亲委派的工作流程了

load.png

那为什么需要使用这种流程进行类的加载呢?首先来看下面实例:

//待加载类
public class Biz {
    private Biz instance;

    public void setInstance(Object instance) {
        this.instance = (Biz)instance; //类型转换
        System.out.println("instance inited");
    }
}

//自行实现的类加载器
public class FileSystemClassLoader extends ClassLoader{
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

//调用代码
public class Client {
    public static void main(String[] args) {
        String classDataRootPath = "D:\\temp"; //Biz.class放置于该目录下
        FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
        FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
        String className = "classloader.whydelegation.Biz";
        try {
            Class<?> class1 = fscl1.loadClass(className);
            System.out.println("class1 ClassLoader is " + class1.getClassLoader());
            Object obj1 = class1.newInstance();
            Class<?> class2 = fscl2.loadClass(className);
            System.out.println("class2 ClassLoader is " + class2.getClassLoader());
            Object obj2 = class2.newInstance();
            class1.getMethod("setInstance", java.lang.Object.class).invoke(obj1, obj2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//运行结果
class1 ClassLoader is classloader.whydelegation.FileSystemClassLoader@7f31245a
java.lang.reflect.InvocationTargetException
class2 ClassLoader is classloader.whydelegation.FileSystemClassLoader@135fbaa4
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at classloader.whydelegation.Client.main(Client.java:21)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.ClassCastException: classloader.whydelegation.Biz cannot be cast to classloader.whydelegation.Biz
    at classloader.whydelegation.Biz.setInstance(Biz.java:10)
    ... 10 more

这段代码示例通过两个不同的类加载器加载同一个.class文件,最后将生成的实例进行类型转换(Biz#setInstance中),但报ClassCastException,原因就在于即便是同一个.class文件被不同的类加载器加载,最终得到的也是两个不同的类的示例,因为JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的

再回到双亲委派机制, 它能保证公用的类特别是Java核心类库只会被加载一次,保证Java 应用所使用的都是同一个版本的 Java 核心库的类,如在加载一个类的时候,会首先去其父级加载器查找该类是否已经加载过,若加载过,则不会再次加载,同时保证该由父级加载器加载的类由父级加载,而不会出现自行实现的类加载器去加载核心类库的情况,试想如果没有双亲委派机制,那么对于java.lang.Object这种通用类,就会存在多个版本,且互不兼容

定义自己的ClassLoader

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。定义自已的类加载器分为两步:

  • 继承java.lang.ClassLoader
  • 重写父类的findClass方法

有人可能有疑问,ClassLoader类有那么多方法,为什么偏偏只重写findClass方法?因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法;具体代码示例见本文ClassLoader工作机制章节FileSystemClassLoader 类的实现

其他

对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是自己首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全

OSGi™是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse 就是基于 OSGi 技术来构建的。OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载

线程上下文类加载器(context ClassLoader)是从 JDK 1.2 开始引入的。类java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承 自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容