Java ClassLoader技术剖析
要构建在线升级系统,一个重要的技术就是能够实现Java类的热替换——也就是在不停止正在运行的系统的情况下进行类(对象)的升级替换。而Java的ClassLoader正是实现这项技术的基础。
在Java中,类的实例化流程分为两个部分:类的加载和类的实例化。类的加载又分为显式加载 和隐式加载。大家使用new关键字创建类实例时,其实就隐式地包含了类的加载过程。对于类的显式加载来说,比较常用的是Class.forName。其 实,它们都是通过调用ClassLoader类的loadClass方法来完成类的实际加载工作的。直接调用ClassLoader的loadClass 方法是另外一种不常用的显式加载类的技术。
图1.Java类加载器层次结构图
ClassLoader在加载类时有一定的层次关系和规则。在Java中,有四种类型的类加 载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader以及用户自定义的 ClassLoader。这四种类加载器分别负责不同路径的类的加载,并形成了一个类加载的层次结构。
BootStrapClassLoader处于类加载器层次结构的最高层,负责 sun.boot.class.path路径下类的加载,默认为jre/lib目录下的核心API或-Xbootclasspath选项指定的jar包。 ExtClassLoader的加载路径为java.ext.dirs,默认为jre/lib/ext目录或者-Djava.ext.dirs指定目录下 的jar包加载。AppClassLoader的加载路径为java.class.path,默认为环境变量CLASSPATH中设定的值。也可以通过 -classpath选型进行指定。用户自定义ClassLoader可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。
这四种类加载器的层次关系图如图1所示。一般来说,这四种类加载器会形成一种父子关系,高层 为低层的父加载器。在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类, 那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常。Java类的加载过程如图2所示。
图2.Java类的加过程
每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
我们编写的应用类默认情况下都是通过AppClassLoader进行加载的。当我们使用 new关键字或者Class.forName来加载类时,所要加载的类都是由调用new或者Class.forName的类的类加载器(也是 AppClassLoader)进行加载的。要想实现Java类的热替换,首先必须要实现系统中同名类的不同版本实例的共存,通过上面的介绍我们知道,要 想实现同一个类的不同版本的共存,我们必须要通过不同的类加载器来加载该类的不同版本。另外,为了能够绕过Java类的既定加载过程,我们需要实现自己的 类加载器,并在其中对类的加载过程进行完全的控制和管理
编写自定义的ClassLoader
为了能够完全掌控类的加载过程,我们的定制类加载器需要直接从ClassLoader继承。首先我们来介绍一下ClassLoader类中和热替换有关的的一些重要方法。
◆findLoadedClass:每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无 论是直接的还是间接的,都保存在自己的名字空间中,该方法就是在该名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回null。这 里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。
◆getSystemClassLoader:Java2中新增的方法。该方法返回系统使用的ClassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。
◆defineClass:该方法是ClassLoader中非常重要的一个方法,它接收以字节数组表示的类字节码,并把它转换成Class实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。
◆loadClass:加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重新实现,我们可以完全控制和管理类的加载过程。
◆resolveClass:链接一个指定的类。这是一个在某些情况下确保类可用的必要方法,详见Java语言规范中“执行”一章对该方法的描述。
了解了上面的这些方法,下面我们来实现一个定制的类加载器来完成这样的加载流程:我们为该类加载器指定一些必须由该类加载器直接加载的类集合,在该 类加载器进行类的加载时,如果要加载的类属于必须由该类加载器加载的集合,那么就由它直接来完成类的加载,否则就把类加载的工作委托给系统的类加载器完 成。
在给出示例代码前,有两点内容需要说明一下:1、要想实现同一个类的不同版本的共存,那么这些不同版本必须由不同的类加载器进行加载,因此就不能把 这些类的加载工作委托给系统加载器来完成,因为它们只有一份。2、为了做到这一点,就不能采用系统默认的类加载器委托规则,也就是说我们定制的类加载器的 父加载器必须设置为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;
- }
- }
在该类加载器的实现中,所有指定必须由它直接加载的类都在该加载器实例化时进行了加载,当通过loadClass进行类的加载时,如果该类没有加载过,并且不属于必须由该类加载器加载之列都委托给系统加载器进行加载。
下面是通过将某个目录下的资源添加到classPath下面中:
public class ClassLoaderUtil { private static Field classes; private static Method addURL; static { try { classes = ClassLoader.class.getDeclaredField("classes"); addURL = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class }); } catch (Exception e) { throw new RuntimeException(e); } classes.setAccessible(true); addURL.setAccessible(true); } private static URLClassLoader system = (URLClassLoader) getSystemClassLoader(); public static ClassLoader getSystemClassLoader() { return ClassLoader.getSystemClassLoader(); } public static ClassLoader getExtClassLoader() { return getSystemClassLoader().getParent(); } public static Object getClassesLoadedBySystemClassLoader() { return getClassesLoadedByClassLoader(getSystemClassLoader()); } public static Object getClassesLoadedByExtClassLoader() { return getClassesLoadedByClassLoader(getExtClassLoader()); } public static Object getClassesLoadedByClassLoader(ClassLoader cl) { try { return classes.get(cl); } catch (Exception e) { throw new RuntimeException(e); } } public static URL[] getBootstrapURLs() { return Launcher.getBootstrapClassPath().getURLs(); } public static URL[] getSystemURLs() { return system.getURLs(); } public static void addURL2SystemClassLoader(URL url) { try { addURL.invoke(system, new Object[] { url }); } catch (Exception e) { throw new RuntimeException(e); } } public static void addClassPath(String path) { addClassPath(new File(path)); } @SuppressWarnings("deprecation") public static void addClassPath(File dirOrJar) { try { addURL2SystemClassLoader(dirOrJar.toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } } }
上面直接调用addClassPath方法则可以把资源加入到当前classPath中。
转自:http://my.oschina.net/skydog/blog/10493?catalog=43932