深入理解JVM类加载器与双亲委派模型
前言在Java技术栈中类加载机制是每一位开发者都应该深入理解的核心知识。无论是日常开发中遇到的ClassNotFoundException还是框架层面的热部署、模块隔离背后都离不开类加载器与双亲委派模型的支撑。本文将带你从零开始系统性地理解JVM类加载器的分类、双亲委派模型的工作原理以及这一设计背后的深意。一、什么是类加载器类加载器ClassLoader是JVM运行时系统的重要组成部分负责在运行时查找字节码文件将字节码文件加载到内存中并将其转化为运行时数据结构。简单来说类加载器的主要作用就是动态加载Java类的字节码.class文件到JVM中在内存中生成一个代表该类的Class对象。字节码可以是Java源程序经过javac编译得来也可以是通过工具动态生成或者通过网络下载得来。值得注意的是JVM启动时并不会一次性加载所有的类而是根据需要动态加载——大部分类在具体用到时才会被加载这种方式对内存更加友好。二、类加载器的分类从JVM的角度看类加载器主要分为两大类Bootstrap ClassLoader和其他类加载器。从Java开发者的角度我们需要了解以下四种2.1 启动类加载器Bootstrap ClassLoader这是最顶层的类加载器由C实现是虚拟机自身的一部分。它负责加载JAVA_HOME/lib目录下的核心类库如rt.jar、resources.jar、charsets.jar等以及被-Xbootclasspath参数指定路径下的所有类。在Java代码中我们无法直接获取到Bootstrap ClassLoader的引用——通过getClassLoader()获取时通常返回null。我们常用的java.lang包下的类如String、Object都是由Bootstrap ClassLoader加载的。2.2 扩展类加载器Extension ClassLoader扩展类加载器由sun.misc.Launcher$ExtClassLoader实现负责加载JAVA_HOME/lib/ext目录下的类库或者被java.ext.dirs系统变量指定路径中的所有类库。2.3 应用程序类加载器Application ClassLoader应用程序类加载器由sun.misc.Launcher$AppClassLoader实现负责加载用户CLASSPATH环境变量指定路径中的所有类库。如果没有自定义类加载器这就是Java程序中默认使用的类加载器。2.4 用户自定义类加载器Custom ClassLoader用户可以通过继承java.lang.ClassLoader类来实现自定义类加载器。常见的应用场景包括隔离加载类、修改类加载方式、扩展类加载源如从网络或数据库加载、防止源代码泄露对字节码加密后自定义加载等。注意JDK 9之后引入了模块化系统扩展类加载器Extension ClassLoader被改名为平台类加载器Platform ClassLoader。三、双亲委派模型3.1 什么是双亲委派模型双亲委派模型Parent Delegation Model是JVM类加载器的核心工作机制。它要求除了顶层的启动类加载器外其余的类加载器都应当有自己的父类加载器。这里的父子关系一般不是通过继承实现的而是通过组合关系来复用父加载器的代码。一个小插曲其实双亲这个翻译容易让人误解——每个类加载器只有一个父加载器翻译成单亲委派可能更准确但双亲委派模型这个说法已经在国内广为流传我们就按这个来理解就好。3.2 工作流程双亲委派模型的工作流程可以概括为以下几个步骤第一步自底向上检查。当一个类加载器收到类加载请求时它首先检查这个类是否已经被自己加载过了。如果已加载直接返回否则将请求委派给父类加载器。每一层都如此直到最顶层的启动类加载器。第二步自顶向下尝试加载。如果启动类加载器在自己的加载路径中找到了这个类就加载并返回如果找不到就由下一层扩展类加载器尝试加载以此类推直到最初收到请求的那个类加载器自己尝试加载。第三步加载失败则抛出异常。如果所有类加载器都无法加载这个类最终抛出ClassNotFoundException。可以用一张图来帮助理解向上检查向下加载Bootstrap ClassLoader ↑ (委托) | (加载) Extension ClassLoader ↑ (委托) | (加载) Application ClassLoader ↑ (委托) | (加载) Custom ClassLoader3.3 源码实现双亲委派模型的实现逻辑集中在java.lang.ClassLoader的loadClass()方法中核心代码如下protectedClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){// 首先检查这个类是否已经被加载过了Class?cfindLoadedClass(name);if(cnull){try{if(parent!null){// 存在父类加载器委托给父类加载cparent.loadClass(name,false);}else{// 父类加载器为null说明是启动类加载器cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// 父类加载器无法完成加载}if(cnull){// 父类加载器无法加载自己尝试加载cfindClass(name);}}returnc;}}3.4 为什么要采用双亲委派模型双亲委派模型的设计并非偶然它主要解决了以下几个关键问题1. 避免类的重复加载。通过自底向上的检查机制同一个类在全JVM范围内只会被加载一次保证了类的唯一性。2. 保证核心类库的安全性。如果没有双亲委派模型用户完全可以自己编写一个java.lang.String类并放在ClassPath中导致系统出现多个不同的String类Java类型体系中最基础的行为将无法保证。有了双亲委派模型所有对java.lang.String的加载请求最终都会到达Bootstrap ClassLoader加载rt.jar中的标准String类。3. 保证Java平台的稳定性。核心类库总是由最顶层的类加载器加载确保了不同类加载器环境中核心类的一致性。四、打破双亲委派模型双亲委派模型并不是一种强制性的约束只是JDK官方推荐的一种方式。在某些场景下我们需要打破这一模型。4.1 第一次破坏JDK 1.2之前的兼容性双亲委派模型在JDK 1.2之后才被引入但ClassLoader抽象类在Java的第一个版本中就已经存在。为了兼容已有的自定义类加载器代码Java设计者在ClassLoader中新增了findClass()方法引导用户重写findClass()而不是loadClass()这样既保留了自定义能力又能让新的类加载器符合双亲委派规则。4.2 第二次破坏基础类型回调用户代码线程上下文类加载器双亲委派模型的一个天然缺陷是子加载器可以看到父加载器的内容但反之不行。如果基础类型由Bootstrap ClassLoader加载需要回调用户代码在ClassPath中由Application ClassLoader加载就会遇到问题。一个典型的例子是JNDI服务。JNDI的代码在JDK 1.3时被加入rt.jar由启动类加载器加载。但JNDI需要调用由第三方厂商实现并部署在ClassPath下的SPI服务提供者接口代码。启动类加载器无法加载这些代码。为了解决这个问题Java引入了线程上下文类加载器Thread Context ClassLoader。通过Thread.setContextClassLoader()方法设置后基础类可以通过这个加载器来加载用户代码。这实质上打破了双亲委派模型的单向层次结构。4.3 第三次破坏对动态性的追求OSGi与热部署OSGiOpen Service Gateway Initiative是实现模块化热部署的典型代表。在OSGi环境中每个模块Bundle都有自己的类加载器。当需要更换一个Bundle时只需将Bundle连同其类加载器一起换掉即可实现代码的热替换。OSGi的类加载器不再遵循双亲委派模型的树状结构而是发展成了更加复杂的网状结构。收到类加载请求时OSGi会按照特定的顺序在多个Bundle中搜索类。除此之外Tomcat为每个Web应用创建独立的WebAppClassLoader打破了双亲委派模型优先自己加载而非委托父加载器实现了不同Web应用之间的类隔离。五、总结类加载器与双亲委派模型是JVM体系中的核心设计之一。理解这一机制不仅有助于我们排查日常开发中遇到的类加载相关异常理解Spring、Tomcat等框架的类隔离与热部署原理在需要时实现自定义类加载器满足特定业务需求双亲委派模型通过层次化的委托机制巧妙地平衡了安全性防止核心类被篡改、一致性保证类的唯一性和灵活性允许自定义类加载器三者之间的关系。尽管在某些场景下它需要被打破但作为Java平台二十多年来稳定运行的重要基石双亲委派模型的设计思想依然值得我们深入品味与学习。