vlambda博客
学习文章列表

Java类加载 - 双亲委派模型

类的加载阶段

类加载阶段分为加载、连接、初始化三个阶段,而加载阶段需要通过类的全限定名来获取定义了此类的二进制字节流。

Java特意把这一步抽出来用类加载器来实现。把这一步骤抽离出来使得应用程序可以按需自定义类加载器。在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。得益于类加载器,OSGI、热部署等领域才得以在Java中得到应用。

在Java中任意一个类都是由这个类本身和加载这个类的类加载器来确定这个类在JVM中的唯一性。不同类加载器加载的类将被置于不同的命名空间,也就是类加载器A加载的com.xx.U和类加载器B加载的com.xx.U是不同的,获取两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。所以即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类。

Java自带三种类加载器

  1. 启动类加载器(Bootstrap ClassLoader)
    属于虚拟机自身的一部分,用C++实现的,主要负责加载<JAVA_HOME>\lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件,它是所有类加载器的根。

  2. 扩展类加载器(Extension ClassLoader)
    Java实现的(Sun的sun.misc.Launcher$ExtClassLoader),独立于虚拟机,主要负责加载<JAVA_HOME>\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。

  3. 应用程序类加载器(Application ClassLoader)
    Java实现的(Sun的sun.misc.Launcher$AppClassLoader),独立于虚拟机,主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那它就是我们程序中的默认加载器,开发者可以直接使用系统类加载器。


类加载器的特性:

  • 每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。

  • 为了实现Java安全沙箱模型顶层的类加载器安全机制, Java默认采用了 “双亲委派的加载链 ” 结构。

双亲委派模型

类加载器层次关系

一个类加载器需要加载类,首先它会把这个类加载请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。

双亲委派模型不是一种强制性约束,是Java 1.2后引入的使用类加载器的方式,JVM在加载类时默认采用的是双亲委派机制。

类关系及代码

ExtClassLoader

AppClassLoader

通过分析ExtClassLoader、AppClassLoader、URLClassLoader、SecureClassLoader可知,都没有覆写java.lang.ClassLoader中默认的加载委派规则loadClass方法,所以虚拟机默认采用的双亲委派机制逻辑就在该方法,具体如下:

protected Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded //1. 首先判断该类型是否已经被加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); //2. 如果没有被加载,就委托给父类加载或者委派给启动类加载器加载 try { if (parent != null) { //3. 如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false); } else { //4. 如果不存在父类加载器,就检查是否是由启动类加载器加载的类 //通过调用本地方法native Class findBootstrapClass(String name) 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. long t1 = System.nanoTime(); //5. 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能 c = findClass(name);
// this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; }}
测试:
 
public static void main(String[] args) { System.out.println("1. "+ClassLoader.getSystemClassLoader()); System.out.println("2. "+ClassLoader.getSystemClassLoader().getParent()); System.out.println("3. "+ClassLoader.getSystemClassLoader().getParent().getParent());}

输出结果:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2

  2. sun.misc.Launcher$ExtClassLoader@46ee7fe8

  3. null

测试产生的问题:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?
答案:如果父加载器为null,则会调用本地方法进行启动类加载尝试

双亲委派模型的好处

  • Java类随着它的类加载器一起具备了一种带有优先级的层次关系
    类加载器除了能用来加载类,还能用来作为类的层次划分,拿java.lang.Object来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar里面的java.lang.Object加载到JVM中。

  • 避免类的重复加载
    当父ClassLoader已经加载了一个类时,子ClassLoader就不会再加载该类

  • 安全
    Java核心api中定义类型不会被随意替换,如自己造了个java.lang.Object,里面嵌入恶意代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护。

虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。

程序动态扩展方式

Java的连接模型允许用户运行时扩展应用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。

运行时动态扩展java应用程序有如下两个途径:

  • 调用java.lang.Class.forName(…),Class.forName使用的是被调用者的类加载器来加载类的

  • 用户自定义类加载器

线程上下文类加载器

Java默认的线程上下文类加载器是 应用程序类加载器(AppClassLoader)。使用线程上下文类加载器, 可以在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类。

典型的例子有, 通过线程上下文来加载第三方库jndi实现, 而不依赖于双亲委派。Tomcat就是采用contextClassLoader来处理web服务的。还有一些采用 hotswap 特性的框架, 也使用了线程上下文类加载器, 比如 seasar (full stack framework in japenese)。

线程上下文从根本解决了一般应用不能违背双亲委派模式的问题,使Java类加载体系显得更灵活。

随着多核时代的来临, 相信多线程开发将会越来越多地进入程序员的实际编码过程中,因此,在编写基础设施时, 通过使用线程上下文来加载类,应该是一个很好的选择。使用线程上下文加载类需要保证多个需要通信的线程间的类加载器应该是同一个,防止因为不同的类加载器, 导致类型转换异常(ClassCastException)。

JDBC是违反双亲委派模型的

你先得知道SPI(Service Provider Interface),这玩意和API不一样,它是面向拓展的,也就是我定义了这个SPI,具体如何实现由扩展者实现。我就是定了个规矩。

JDBC就是如此,在rt里面定义了这个SPI,那mysql有mysql的jdbc实现,oracle有oracle的jdbc实现,反正我java不管你内部如何实现的,反正你们都得统一按我这个来,这样我们java开发者才能容易的调用数据库操作。

所以因为这样那就不得不违反这个约束啊,Bootstrap ClassLoader就得委托子类来加载数据库厂商们提供的具体实现。因为它的手只能摸到<JAVA_HOME>\lib中,其他的它无能为力。这就违反了自下而上的委托机制了。

这个就使用线程上下文类加载器来实现,通过setContextClassLoader()设置应用程序类加载器,然后Thread.currentThread().getContextClassLoader()获得类加载器来加载。
sun.misc.Launcher的无参构造函数:

public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); }
try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); }
Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if (var2 != null) { SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { } catch (InstantiationException var6) { } catch (ClassNotFoundException var7) { } catch (ClassCastException var8) { } } else { var3 = new SecurityManager(); }
if (var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); }
System.setSecurityManager(var3); }
}