(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() 来获取它。
ExtClassLoader和AppClassLoader都是继承自URLClassLoader。
(2) 类加载的规则
类加载规则有两个:
1 双亲委托
2 类A引用到类B,则由类A的加载器去加载类B,保证引用到的类由同一个加载器加载
JVM在加载类时默认采用的是双亲委托机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
ClassLoader在运行期会以父/子的层次结构存在,每个classLoader 实例都有其父ClassLoader的引用,而父ClassLoader并没有持有子ClassLoader的引用,从而形成一条单向链,当一个类装载请求提交到某个ClassLoader时,默认的类装载过程如下:
1. 检查这个类有没有被装载过,如果已经装载过,返回
2. 调用父ClassLoader去装载类,如果装载成功返回.
3. 调用自身的装载类方法,如果装载成功则返回
4. 如查都没有成功,抛出ClassNotFoundException.
简单说,当ClassLoader链上的某一ClassLoader收到类装载请求时,会按顺序向上询问其所有父节点,直到boot classLoader.任何一个节点成功受理了此请求,则返回,如果所有父节点都不能受理,这个时候才由请求的ClassLoader自身来装载这个类,如果仍不能装载,则抛出异常.
可以使用JVM参数-verbose:class来打印类的加载log.
(3) 类加载的方法
(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也可以。