类加载器的基本概念 java
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。通常来讲,Java 虚拟机使用 Java 类的方式以下:Java 源程序(.java 文件)在通过 Java 编译器编译以后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每一个这样的实例用来表示一个 Java 类。经过此实例的 newInstance()方法就能够建立出该类的一个对象。 web
基本上全部的类加载器都是 java.lang.ClassLoader类的一个实例。 数据库
下面详细介绍这个 Java 类。 apache
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,而后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此以外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如 表 1所示。关于这些方法的细节会在下面进行介绍。
表 1. ClassLoader 中与加载类相关的方法 api
方法 |
说明 |
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 类 |
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 能够根据用户的须要定制本身的类加载过程,在运行期进行指定类的动态实时加载。
tomcat
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类的实例
3.加载过程 app
在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,若是已经加载则直接返回该类的引用。
若是到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,若是还不能成功,就会抛出异常。
直接使用系统加载器加载类失败抛出的是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异常。
由于在此模型下用户自定义的类装载器不可能装载应该由父加载器装载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器装载的可靠代码。
例如全部 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"; } }在该代码中必需要说明的一点是, 该自定义类加载器的并无指定父加载器。
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, 那么当咱们在使用自定义类加载器加载的类中引用第三方的类, 例如引用了原本应该是由扩展类加载器或者系统加载器加载的类时, 就会出现不能加载的问题。
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)
在线程中运行的代码能够经过此类加载器来加载类和资源。
正常的双亲委派模型中,下层的类加载器可使用上层父加载器加载的对象,可是上层父类的加载器不可使用子类加载的对象。
而有些时候程序的确须要上层调用下层,这时候就须要线程上下文加载器来处理。
Thread.currentThread().getContextClassLoader()前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的所有问题。
在给出代码以前先说下Tomcat.6的类加载器, 结构层次以下:
+-----------------------------+ | Bootstrap | | | | | System | | | | | Common | | / \ | | WebApp1 WebApp2 | | | | | +-----------------------------+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.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容器