Java中的类加载器,有启动类加载器(Bootstrap Classloader)、扩展类加载器(Launcher$ExtClassLoader)、应用程序类加载器(Launcher$AppClassLoader),用户还可以实现自定义的类加载器,见下图:
类加载的这种关系称为双亲委派模式,需要注意的是他们之间不是继承关系,而是组合关系,在执行类加载的动作时,首先都是交给父类去加载,如果父类无法加载再交给子类去完成,直到调用用户自定义的类加载器去加载,如果全部都无法加载,就会抛出ClassNotFoundException。
Launcher$ExtClassLoader和Launcher$AppClassLoader都是URLClassLoader的子类,但是他们的实现又是有一点不同的,Launcher$ExtClassLoader的实现是遵循的双亲委派模型,它重写的是findClass方法,如下:
protected Class findClass(String paramString) throws ClassNotFoundException { DownloadManager.getBootClassPathEntryForClass(paramString); return super.findClass(paramString); }而 Launcher$AppClassLoader 的实现是没有遵循双亲委派模型的,它重的是 loadClass 方法,以下是 AppClassLoader 中的 loadClass 的源码:
public synchronized Class loadClass(String paramString, boolean paramBoolean) throws ClassNotFoundException { DownloadManager.getBootClassPathEntryForClass(paramString); int i = paramString.lastIndexOf(46); if (i != -1) { SecurityManager localSecurityManager = System.getSecurityManager(); if (localSecurityManager != null) { localSecurityManager. checkPackageAccess(paramString.substring(0, i)); } } return super.loadClass(paramString, paramBoolean); }注:以上的源码是通过 JD 反编译过来的,可能会和实际的源码会有一点点不一样,JDK的版本是HotSpot 6U24。
他们的实现方式为什么会不同呢?因为Launcher$ExtClassLoader加载的类是属于$JRE_HOME/lib/ext下面(也可能通过系统变量java.ext.dir指定路径)的扩展类,Sun公司肯定不会写两个或者多个具有相同全限定名的类、但是功能却不相同的类的,如果真有这样的类存在,那JVM的执行肯定就会有问题了,这在它的控制范围之内的事情。而Launcher$AppClassLoader是用于加载各个不同应用下面的类,同一个JVM中可以同时存在多个应用,如同一个Tomcat中可以同时存在多个Web应用,而这些应用可能是来自不同的开发方,他们之间彼此可能都不知道是谁,但是他们写的类却可能具有相同的全限定名,所以为了确保这些应用之间互不干扰,就需要由各应用的类加载器去加载所属应用的类,这样就不会发生类冲突了。
上面说到了Launcher$ExtClassLoader和应用类加载器(Launcher$AppClassLoader)分别会用于加载哪些类,为了对类加载器有一个完整的认识,下面再介绍启动类加载器(Bootstrap Classloader)会从哪里加载类。
启动类加载器(Bootstrap Classloader),是最先启动的类加载器,默认是负责加载$JRE_HOME/lib目录下面的类,也可以通过JVM参数-Xbootclasspath来指定需要加载类的路径,不过虚拟机为了安全性以及功能的完整性,并不是任何存在于启动类加载器路径下的jar都会被加载,它是通过jar的名字来区分需要加载的类的,如rt.jar等,其它的类即使放在启动类加载器的加载目录下,也是不会被加载的,有兴趣的话可以自己编译一个jar放到$SRE_HOME/lib目录下,然后通过加上JVM参数-XX:+TraceClassLoading任意运行一个java程序,看其中是否有你jar,结论是不会存在的。
前面介绍了启动类加载器、扩展类加载器以及应用类加载器,现在回到正题,实现一个自定义的类加载器,我们可以参考扩展类加载器及应用类加载器,可分别通过重写ClassLoader中的loadClass方法或者findClass方法,但是重写不同的方法,就像扩展类加载器以及应用类加载器一样,要达到的目的是不一样的。
重写loaderClass方法
如果要想在JVM的不同类加载器中保留具有相同全限定名的类,那就要通过重写loadClass来实现,此时首先是通过用户自定义的类加载器来判断该类是否可加载,如果可以加载就由自定义的类加载器进行加载,如果不能够加载才交给父类加载器去加载。
这种情况下,就有可能有大量相同的类,被不同的自定义类加载器加载到JVM中,并且这种实现方式是不符合双亲委派模型。但是不能够说这种实现方式就一定是错误的,有可能当前的场景就需要这样的方式,如容器插件应用场景就适合。
一个插件容器,如下图所示:
要允许不同的插件增加到容器中,就需要采用这种方式,因为我们没有办法保证不同的插件中不能够有相同全限定名的类存在,如A插件中存在了test.Test这么一个类,B插件中也可能会有这么一个相同的类,不能说A插件的test.Test类被加载了,B插件的test.Test就不可再加载了,这就会导致B或/和A插件工作不正常,因为这两个不同的类实现的功能可能完全不同,或者可以说他们之间是一点关系都没有。
但是如果实现自定义类的场景不是类似上面的插件容器场景,最好还是实现findClass,这个也是Sun推荐的实现方式,并且它是符合双亲委派模型的。下面是一个自定义类加载器的实现源码:
ClassLoader myloader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { // 这个getClassInputStream根据情况实现 InputStream is = getClassInputStream(name); if (is == null) { return super.loadClass(name); } byte[] bt = new byte[is.available()]; is.read(bt); return defineClass(name, bt, 0, bt.length); } catch (IOException e) { throw new ClassNotFoundException("Class " + name + " not found."); } } }
注:上面源码中的getClassInputStream(name)方法根据实际情况去实现了,如果要加载的类是在类路径下,这个方法的实现可能是这样的:
String filename = name.replace('.', '/')+".class"; InputStream is = getClass().getResourceAsStream(filename);
InputStream is = new FileInputStream(new File(name));也有可能是通过网络获取的,只要能够获取得到就行。
重写findClass方法
重写findClass方法的自定义类,首先会通过父类加载器进行加载,如果所有父类加载器都无法加载,再通过用户自定义的findClass方法进行加载。如果父类加载器可以加载这个类或者当前类已经存在于某个父类的容器中了,这个类是不会再次被加载的,此时用户自定义的findClass方法就不会被执行了。
重写findClass方法是符合双亲委派模式的,它保证了相同全限定名的类是不会被重复加载到JVM中,下面是JDK 6u33中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 = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
这个是有一个历史原因的,因为双亲委派模型是JDK1.2以后才引用进来的,在1.1及以前用户实现自己的类加载器都是通过重写loadClass方法实现,为了兼容原来的实现方式,就选择了增加findClass这么一种妥协的方式。
实现自定义类,源码可以复用上面重写loadClass的实现,只需要将loadClass方法为findClass方法即可。