JAVA类加载器
JAVA类加载器可以动态加载JAVA类到jvm中,它是JRE的一部分,每个java类都必须通过一个类加载器加载。有了类加载器,JAVA运行时系统就可以通过类加载器加载类文件,这样就不需要直接访问文件或者文件系统了。一般来说开发者是不需要直接使用到类加载器的,但是理解类加载器的规则和用法,有助于了解web容器的实现原理以及在处理ClassNotFoundException和NoClassDefFoundError能提供更多的解决问题的思路等。
类加载器的层次架构
每个类加载器都有一个父类加载器(引导类加载器除外),类加载器一共分四种分别是
引导类加载器(bootstrap class loader):引导类加载器加载核心的java类库(<JAVA_HOME>/jre/lib 目录下) ,这个类加载器是最顶层的类加载器没有父类加载器,它是由native方法实现的。
扩展类加载器(extension class loader):扩展类加载器加载的是扩展类(<JAVA_HOME>/jre/lib/ext 目录下,还可以是系统属性java.ext.dirs 指定的目录),它的实现是sun.misc.Launcher$ExtClassLoader 类
系统类加载器(system class loader) :系统类加载器加载 CLASSPATH目录下的文件,也就是说一般的应用创建的类都是由系统类加载器来加载的,它的实现是sun.misc.Launcher$AppClassLoader 类。
自定义加载器:可以通过继承java.lang.ClassLoader来实现自定义类加载器。
这四个加载的关系是,引导类加载器是扩展类加载器的父类加载器,扩展类加载器是系统类加载的父类加载器。而自定义的加载器的父加载器可能是另外一个自定义加载器也可能是系统类加载器,另外,自定义的类加载器默认的父类加载器是系统类加载器。他们的关系如图:
类加载器的委托机制
当一个类加载器加载一个类的时候,会首先通过它的父类加载器加载,如果父类加载器不能加载才会自己加载。也就是说,假如类加载器A是一个自定义加载器,并且A的父类加载器是系统类加载器。当A加载一个类B的时候,会首先委托系统类加载器加载类B,系统类加载器又会去委托扩展类加载器,同理扩展类加载器会去委托引导类加载器。这样就形成了一个类加载器的委托机制。
那这个委托机制的好处在哪里呢?首先JVM中是根据类加载器和类名来识别一个类的。也就是说不同的类加载器加载同样的类名,在JVM眼里,他们不是同一个类。这样就存在一个问题,如果不存在委托机制,并且这个时候有一个自定义加载器A,A加载了java.lang.String。同时也有一个自定义加载器B,B也加载了java.lang.String。这时候,因为就有两个不同类型的java.lang.String了。如果把前者new出来的对象赋到后者的类引用中,就会出现类型转化的异常了。委托机制就能解决这个问题,类似String这些核心类只会被引导类加载器加载。
如何自定义类加载器
一般来说自定义的类加载器首先继承java.lang.ClassLoader类,然后覆盖其findClass方法。一般来说,我们使用ClassLoader的时候,会调用他的loadClass方法,如下:
protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
由代码可以清晰的看到,loadClass方法会首先调用findLoadedClass方法,这个方法会首先去寻找当前的类加载器是否已经加载了该类,如果已经加载了直接返回该类。如果没有加载则调用父类加载器的loadClass方法,如果不能成功加载类,就会调用findClass方法,类加载器的委托机制就是通过这个方法来实现的。
下面是一个简单的自定义类加载器。根据指定路径读取.class文件,然后转化成二进制数组,然后使用defineClass来加载这个类文件。可以看到最终加载类是defineClass方法,
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"; } }
线程上下文类加载器
线程上下文类加载器一般用在框架中。每个线程都有上下文类加载器(除非这个线程是通过本地方法创建的)。通过Thread.setContextClassLoader()方法可以设置线程的上下文线程,如果你没有通过构造方法调用到这个方法,那么线程会继承他的父线程的上下文类加载器。线程的默认的类加载器是系统类加载器。有这样一种情况,父类加载器需要使用到子类加载器,这个时候委托机制就不生效了。委托机制只能委托父类加载器而不能委托子类加载器。这个时候就可以通过上下文类加载器来加载了。
javax.xml.parsers.SAXParserFactory 的 newInstance 中就是这样一个例子,
public static SAXParserFactory newInstance() { try { return (SAXParserFactory) FactoryFinder.find( /* The default property name according to the JAXP spec */ "javax.xml.parsers.SAXParserFactory", /* The fallback implementation class name */ "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl"); } catch (FactoryFinder.ConfigurationError e) { throw new FactoryConfigurationError(e.getException(),e.getMessage()); } }
newinstance()方法会创建一个SAXParserFactory类,SAXParserFactory当前的类加载器的实现是引导类加载器,但是SAXParserFactory的实现类com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl引导类加载器并不能加载,这时候就可以用到线程上下文类加载器了。在上面源码中,FactoryFinder.find()中的newInstance()方法中的getProviderClass()方法中就能看到使用了线程类上下文加载器。