(1) JVM中ClassLoader的类型

JVM预定义了三种类型类加载器,当一个 JVM 启动的时候,Java 缺省开始使用如下三种类型类装入器:


Bootstrap类加载器:引导类加载器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。


Extension类加载器:扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。


System类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

java 加载配置文件 java加载项_classloader




ExtClassLoader和AppClassLoader都是继承自URLClassLoader。

java 加载配置文件 java加载项_classloader_02

 

(2) 类加载的规则

类加载规则有两个:

1 双亲委托

2 类A引用到类B,则由类A的加载器去加载类B,保证引用到的类由同一个加载器加载


JVM在加载类时默认采用的是双亲委托机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

java 加载配置文件 java加载项_classloader_03


ClassLoader在运行期会以父/子的层次结构存在,每个classLoader 实例都有其父ClassLoader的引用,而父ClassLoader并没有持有子ClassLoader的引用,从而形成一条单向链,当一个类装载请求提交到某个ClassLoader时,默认的类装载过程如下:

1. 检查这个类有没有被装载过,如果已经装载过,返回

2. 调用父ClassLoader去装载类,如果装载成功返回.

3. 调用自身的装载类方法,如果装载成功则返回

4. 如查都没有成功,抛出ClassNotFoundException.

简单说,当ClassLoader链上的某一ClassLoader收到类装载请求时,会按顺序向上询问其所有父节点,直到boot classLoader.任何一个节点成功受理了此请求,则返回,如果所有父节点都不能受理,这个时候才由请求的ClassLoader自身来装载这个类,如果仍不能装载,则抛出异常.


可以使用JVM参数-verbose:class来打印类的加载log.

(3) 类加载的方法

java 加载配置文件 java加载项_java 加载配置文件_04


(4) 代码实例

在eclipse工程的当前目录下建立一个文件./cp/test/MyClass.java

package test;

public class MyClass {
}

进入cp目录下,编译此文件,生成MyClass.class

javac test/MyClass.java

运行下面的代码

package learning.classloader;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        try {
            Class.forName("test.MyClass");
        } catch (ClassNotFoundException e) {
            System.out.println("Failed to load MyClass");
        }
        
        File myClassDir = new File("./cp");
        URLClassLoader sysCl = (URLClassLoader) ClassLoader.getSystemClassLoader();
        Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
        addUrlMethod.setAccessible(true);
        addUrlMethod.invoke(sysCl, myClassDir.toURI().toURL());
        
        Class c = Class.forName("test.MyClass");
        System.out.println("Loaded MyClass successfully");
                
        URLClassLoader clNew = new URLClassLoader(sysCl.getURLs(), sysCl.getParent());
        Class c1 = clNew.loadClass("test.MyClass");
        
        System.out.println(c.equals(c1));
        System.out.println(c.getClassLoader());
        System.out.println(c1.getClassLoader());
        
        System.out.println();
        InputStream is1 = c.getResourceAsStream("MyClass.java");
        BufferedReader reader = new BufferedReader(new InputStreamReader(is1));
        String s;
        while ((s = reader.readLine()) != null) {
            System.out.println(s);
        }
        reader.close();
        
        System.out.println();
        InputStream is2 = c.getResourceAsStream("/test/MyClass.java");
        reader = new BufferedReader(new InputStreamReader(is2));
        while ((s = reader.readLine()) != null) {
            System.out.println(s);
        }
        reader.close();
        
        System.out.println();
        InputStream is3 = sysCl.getResourceAsStream("test/MyClass.java");
        reader = new BufferedReader(new InputStreamReader(is3));
        while ((s = reader.readLine()) != null) {
            System.out.println(s);
        }
        reader.close();
    }
}

分析:运行上面的例子,不要把cp目录加到classpath中,可以看到第一次调用Class.forName("test.MyClass")会失败。
之后把cp目录加到系统类加载器(sun.misc.Launcher$AppClassLoader)中,或者重新new一个类加载器,设置其classpath中包含cp目录,都能够正常加载MyClass类。

1)不同的类加载器加载指定类型得到的class对象不同
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.
上面的例子中,c是用系统类加载器(sun.misc.Launcher$AppClassLoader)加载,c1是用new出来的URLClassLoader加载,可以看到c.equals(c1))的结果是false。

2)创建类加载器必须正确设置父加载器
URLClassLoader clNew = new URLClassLoader(sysCl.getURLs(), sysCl.getParent());
上面创建新的类加载器的时候指定父类加载器为sysCl.getParent(), 即系统类加载器的parent,亦即extension类加载器(sun.misc.Launcher$ExtClassLoader)。
如果改成
URLClassLoader clNew = new URLClassLoader(sysCl.getURLs());
不指定其parent, 那么默认parent为系统类加载器。这样c.equals(c1))的结果将是true。这是因为类加载规则1-双亲委托造成的。

3)getResource/getResourceAsStream的用法
 ClassLoader和Class上都可以调用getResource/getResourceAsStream。区别在于
* Class.getResourceAsStream(path) 指定的path如果以"/"开头,表示查找路径是classpath,如果不以"/"开头,查找路径是Class所在的位置。
 c.getResourceAsStream("MyClass.java");                   --〉在test.MyClass所在位置查找 MyClass.java
 c.getResourceAsStream("/test/MyClass.java");          --〉在classpath指定的路径中寻找 ./test/MyClass.java
* ClassLoader.getResourceAsStream(path)  查找路径是classpath.

(5) ContextClassLoader

ContextClassLoader是Thread的一个属性,可以这样获得Thread.currentThread().getContextClassLoader(),也可以调用setContextClassLoader()设置。
ContextClassLoader只是一个逻辑上的概念,并没有一个叫ContextClassLoader的类。
如果创建一个新的线程,会从创建线程继承ContextClassLoader。
在线程里面new一个对象时,使用的是系统类加载器,并不会使用线程的ContextClassLoader。

那么为什么还需要ContextClassLoader呢?这其实是因为加载Class的默认规则在某些情况下不能满足要求。
比如jdk中的jdbc API 和具体数据库厂商的实现类SPI的类加载问题。在jdbc API的类是由BootStrap加载的,那么如果在jdbc API需要用到spi的实现类时,根据默认规则2,则实现类也会由BootStrap加载,但是spi实现类却没法由BootStrap加载,只能由Ext或者App加载。
当DriverManager需要加载SPI中的实现类时,可以获取ContextClassLoader,然后用它来加载spi中的类。
当然也可以不使用ContextClassLoader,自己保存一个classLoader,需要用到的地方使用此classLoader也可以。