类加载器的基本概念
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。
下面详细介绍这个 Java 类。
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如 表 1所示。关于这些方法的细节会在下面进行介绍。
表 1. ClassLoader 中与加载类相关的方法
方法 |
说明 |
getParent() |
返回该类加载器的父类加载器。 |
loadClass(String name) |
加载名称为 name的类,返回的结果是 java.lang.Class类的实例 |
findClass(String name) |
查找名称为 name的类,返回的结果是 java.lang.Class类的实例 |
findLoadedClass(String name) |
查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例 |
defineClass(String name, byte[] b, int off, int len) |
把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的 |
resolveClass(Class<?> c) |
链接指定的 Java 类 |
本文将从以下几个方面来阐述classloader。
1.分类
1.1.Bootstrap ClassLoader(启动类加载器)
加载JAVA_HOME/lib目录下的核心api 或 -Xbootclasspath 选项指定的jar包装入工作,
是用原生代码来实现的, 并不继承自 java.lang.ClassLoader。
1.2.Extension ClassLoader(扩展类加载器)
加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录
加载JAVA_HOME/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包
1.3.System ClassLoader(系统类加载器)
加载java -classpath/-cp/-Djava.class.path所指的目录下的类与jar包
它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。
可以通过 ClassLoader.getSystemClassLoader()来获取它。
每个classpath以文件名或目录结尾,该文件名或目录取决于将类路径设置成什么:
对于包含.class文件的.zip或.jar文件,路径以.zip或.jar文件名结尾。
对于未命名包中的.class文件,路径以包含.class文件的目录结尾。
对于已命名包中的.class文件,路径以包含root包(完整包名中的第一个包)的目录结尾。
1.4.自定义类加载器
通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,
用户自定义 ClassLoader 可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。
2. 层次结构
这四种类加载器的层次关系如上图所示。
一般来说,这四种类加载器会形成一种父子关系,高层为低层的父加载器。
可以通过以下代码来获取类加载器, 同时该代码也演示了类的层次结构
public class ClassLoaderTree {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
代码运行结果如下:
sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11
第一个输出的是 ClassLoaderTree类的类加载器, 即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例
第二个输出的是扩展类加载器, 是 sun.misc.Launcher$ExtClassLoader类的实例。
这里并没有输出引导类加载器, 这是由于JDK 的实现对于父类加载器是引导类加载器的情况, getParent()方法返回 null。
3.加载过程
在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类的引用。
如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常。
直接使用系统加载器加载类失败抛出的是NoClassDefFoundException异常。
如果使用自定义的类加载器loadClass方法或者ClassLoader的findSystemClass方法加载类,抛出的是 ClassNotFoundException。
以下代码是除 BootstrapClassLoader 外的类加载器加载流程:
// 检查类是否已被装载过
Class c = findLoadedClass(name);
if (c == null ) {
// 指定类未被装载过
try {
if (parent != null ) {
// 如果父类加载器不为空, 则委派给父类加载
c = parent.loadClass(name, false );
} else {
// 如果父类加载器为空, 则委派给启动类加载加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 启动类加载器或父类加载器抛出异常后, 当前类加载器将其
// 捕获, 并通过findClass方法, 由自身加载
c = findClass(name);
}
}
4.加载类时的几个原则
4.1. 代理/双亲委托
类加载器在尝试自己去查找某个类的字节代码并定义它时, 会先代理给其父类加载器, 父加载器也会请求它的父加载器代理加载, 依次类推。
在介绍代理模式背后的动机之前, 首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。
Java 虚拟机不仅要看类的全名是否相同, 还要看加载此类的类加载器是否一样。
只有两者都相同的情况, 才认为两个类是相同的。
即便是同样的字节代码, 被不同的类加载器加载之后所得到的类,也是不同的。
下面通过实例代码来说明:
package com.example;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
测试Java类是否相同:
public class ClassIdentity {
public static void main(String[] args) {
new ClassIdentity().testClassIdentity();
}
public void testClassIdentity() {
String classDataRootPath = "C:\\Documents and Settings\\Administrator\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试Java类是否相同的代码运行结果:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。
虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。
不同的类加载器为相同名称的类创建了额外的名称空间。
相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。
不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。
因为在此模型下用户自定义的类装载器不可能装载应该由父加载器装载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器装载的可靠代码。
例如所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。
如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。
通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
但是实际上,类加载器的编写者可以自由选择不用把请求委托给parent加载器,也就是可以违背代理原则, 但正如上所说,会带来安全的问题。
4.2. 可见性/隔离性
被子加载器加载的类拥有被父加载器加载的类的可见性,但反之则不然。
自定义类加载器拥有三个其本类加载器加载的所有类的可见性,但是处于不同分支的自定义类加载器相互之间不具有可见性。
所谓不可见即不能直接互相访问, 也就是即使它们装载同一个类,也会拥有不同的命名空间, 会有不同的Class实例。
但如果持有类所对应的Class对象的引用, 还是可以访问另一命名空间的类。正如示例代码中我们通过反射的方式实现了不同加载器加载的类的访问。
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
同样我们也可以利用可见性原则实现不同加载器加载的类之间的互访, 只需要对Sample类稍加改造, 让其实现ISample接口。
public interface ISample {
public void setSample(Object instance)
}
public class Sample implements ISample {
private Sample instance;
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
String classDataRootPath = "C:\\Documents and Settings\\Administrator\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
ISample obj1 = (ISample)class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
ISample obj2 = (ISample)class2.newInstance();
obj1.setSample(obj2);
} catch (Exception e) {
e.printStackTrace();
}
上面示例的代码中我们使用自定义类加载器加载了Sample类, 而接口ISample是由系统类加载器加载的, 所以ISample对于Sample是具有可见性的, 因此转型成功。
4.3. 唯一性我们继续分析上面的示例, 使用下面的代码做转型
Class<?> class1 = fscl1.loadClass(className);
Sample obj1 = (Sample)class1.newInstance();
如果我们尝试直接使用如上的代码来访问, 会抛出 ClassCastException 异常。
因为在 Java 中, 即使是同一个类文件,如果是由不同的类加载器加载的, 那么它们的类型是不相同的。
在上面的例子中class1是由自定义类加载器加载的, 而Sample变量类型声名和转型里的Sample类却是由类加载器(默认为 AppClassLoader)加载的, 因此是完全不同的类型, 所以会抛出转型异常。
类加载器的代理/双亲委托原则, 决定了每一个类在一个加载器里最多加载一次, 当然多个加载器可以加载同一个类。
每个类对象在各自的namespace内,对类对象进行比较或者对实例进行类型转换时,会同时比较各自的名字空间。
5.自定义类加载器
自定义加载器给Java语言增加了很多灵活性,主要的用途有
- 可以从多个地方加载类,比如网络上,数据库中,甚至即时的编译源文件获得类文件;
- 类加载器可以在运行时原则性的加载某个版本的类文件;
- 类加载器可以动态卸载一些类;
- 类加载器可以对类进行解密解压缩后再载入类
下面的代码是上面示例中用到的自定义类加载器的实现类, 功能是从本地文件系统加载类
package classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
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";
}
}
在该代码中必须要说明的一点是, 该自定义类加载器的并没有指定父加载器。
JVM规范中规定在不指定父类加载器的情况下, 默认采用系统类加载器作为其父加载器, 所以在使用该自定义类加载器时, 需要加载的类不能在类路径中, 否则的话依据类加载器的代理/委托原则, 待加载类会由系统类加载器加载,
这样自定义类加载器想要实现的, 诸如类的热替换, 多版本共存, 将变的不可实现。
如果我们一定想要把自定义加载器需要加载的类放在类路径中, 应该怎么办呢, 答案是把自定义类加载器的父加载器设置为null。
JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器。
需要注意的是自定义类加载器不同的父加载器决定了加载类的不同的可见性。
下面的代码示例是一个把自定义类加载器的父加载器设置为null时, 如何处理加载类的不同可见性。
class CustomCL extends ClassLoader {
private String basedir; // 需要该类加载器直接加载的类文件的基目录
private HashSet dynaclazns; // 需要由该类加载器直接加载的类名
public CustomCL(String basedir, String[] clazns) {
super(null); // 指定父类加载器为 null
this.basedir = basedir;
dynaclazns = new HashSet();
loadClassByMe(clazns);
}
private void loadClassByMe(String[] clazns) {
for (int i = 0; i < clazns.length; i++) {
loadDirectly(clazns[i]);
dynaclazns.add(clazns[i]);
}
}
private Class loadDirectly(String name) {
Class cls = null;
StringBuffer sb = new StringBuffer(basedir);
String classname = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + classname);
File classF = new File(sb.toString());
cls = instantiateClass(name,new FileInputStream(classF),
classF.length());
return cls;
}
private Class instantiateClass(String name,InputStream fin,long len){
byte[] raw = new byte[(int) len];
fin.read(raw);
fin.close();
return defineClass(name,raw,0,raw.length);
}
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if(!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
}
在上面的自定义类加载器中, 我们设置了该自定义类加载器的父加载器为null, 那么当我们在使用自定义类加载器加载的类中引用第三方的类, 例如引用了本来应该是由扩展类加载器或者系统加载器加载的类时, 就会出现不能加载的问题。
所以我们在上面的自定义类加载器中, 重写了loadClass方法, 修改了或者说打破了代理/委托逻辑, 自定义类加载器先尝试自己加载, 当自定义类加载器不能加载的类, 交由系统加载器来加载。
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if(!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
6.
类加载方式
除了上面提到的通过自定义类加载器加载类, 我们通常会使用下面的两种方式来加载类
6.1. 隐式加载
A a = new A();
如果程序运行到这段代码时还没有A类,那么JVM会请求装载当前类的类装器来装载类。
6.2. 显示加载
//效果相同, 执行类的初始化
Class.forName("test.A");
Class.forName("test.A", true, this.getClass().getClassLoader());
//效果相同, 不执行类的初始化
getClass().getClassLoader().loadClass("test.A");
Class.forName("test.A", false, this.getClass().getClassLoader());
//效果相同, 不执行类的初始化
ClassLoader.getSystemClassLoader().loadClass("test.A");
Class.forName("test.A", false, Classloader.getSystemClassLoader());
//效果相同, 不执行类的初始化
Thread.currentThread().getContextClassLoader().loadClass("test.A")
Class.forName("test.A", false, Thread.currentThread().getContextClassLoader());
7.
上下文类加载器(
ContextClassLoader)类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。
如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。
Java 应用运行的初始线程的上下文类加载器是系统类加载器。
在线程中运行的代码可以通过此类加载器来加载类和资源。
正常的双亲委派模型中,下层的类加载器可以使用上层父加载器加载的对象,但是上层父类的加载器不可以使用子类加载的对象。
而有些时候程序的确需要上层调用下层,这时候就需要线程上下文加载器来处理。
Thread.currentThread().getContextClassLoader()
前面提到的类加载器的代理模式并不能解决 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 的实现中都会用到。
在5.自定义类加载器中我们的自定义类加载器CustomCL, 如果放到tomcat下的web应用中去使用会出现什么问题呢, 例如在自定义加载器 待加载的类中使用第三方类, 这个时候自定义加载器不能加载的类会交由系统加载器加载, 而该第三类不存在于类路径中, 只存在于该webApp下, 显然是加载不到的, 为了解决这个问题, 这个时候我们就需要上下文类加载器来解决这个问题了。
在给出代码之前先说下Tomcat.6的类加载器, 结构层次如下:
+-----------------------------+
| Bootstrap |
| | |
| System |
| | |
| Common |
| / \ |
| WebApp1 WebApp2 |
| |
| |
+-----------------------------+
Webapp 类装载器:
应用层的类装载器,每个应用程序都会创建一个单独的类装载器。该类装载器只能本应用程序中可见。
所有/WEB-INF/classes目录下未压缩的类文件,资源文件都由该类装载器加载。
所有/WEB-INF/lib目录下压缩后Jar/zip文件都由该类装载器加载
显然上面我们说到的问题应该用webapp类加载器来加载第三方类, 那我们在自定义类加载器中如何获得webapp类加载器呢, 在tomcat6中启动webapp线程的上下文类加载器被设置为webapp类加载器了, 所以我们可以通过如下代码来完成加载。
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if(!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
//自定义加载器和系统加载器均不能正常加载的类, 交由上下文加载器加载
cls = Thread.currentThread().getContextClassLoader().loadClass(name);
if(cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
其实ContextClassLoader就是Thread的一个属性而已, 我们当然可以不使用ContextClassLoader, 自己找个地方把classLoader保存起来, 在需要获取的时候能得到此classLoader就可以。8. 自定义类加载器的其他应用
8.1. 热加载
每次创建一个新的类加载器, 我们修改下上面示例中的ClassIdentity类, 让他可以实现热加载。
public class ClassIdentity extends Thread {
public static void main(String[] args) {
new ClassIdentity().start();
}
public void run() {
while(true) {
this.testClassIdentity();
try {
Thread.sleep(30 * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void testClassIdentity() {
String classDataRootPath = "C:\\Documents and Settings\\Administrator\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行该代码, 在运行过程中我们修改Sample类, 并覆盖原Sample类。
8.2. 类加密
指一般意义上的加密, 通过自定义加载器解密载入加密类
8.3. 应用隔离
非常典型的应用就是web容器
来源:oschina
链接:https://my.oschina.net/u/121271/blog/96791