JVM 整理(二) 类加载器

JVM 整理(二) 类加载器 在Java虚拟机的世界里类加载器ClassLoader扮演着“搬运工”的角色它负责将编译好的.class字节码文件加载到内存中为后续的执行引擎提供“原料”。理解类加载器的工作原理是掌握JVM底层机制的关键一环。本文将带你全面了解类加载器的概念、类的加载过程、类加载器的分类以及著名的双亲委派模型。1. 类加载器的基本概念类加载器是JVM的一个子系统它的核心任务是根据一个类的全限定名来读取对应的.class文件可以是文件系统、网络、数据库等来源将其转换为JVM内部的数据结构并在方法区中生成一个代表该类的java.lang.Class对象作为访问该类信息的入口。值得注意的是类加载器只负责加载至于这个类能否被正确执行则由执行引擎Execution Engine来决定。类加载器遵循“懒加载”原则即只有在程序运行中真正需要使用某个类时才会触发该类的加载过程。2. 类的加载过程生命周期一个类从被加载到虚拟机内存开始直到卸载出内存为止它的整个生命周期包括七个阶段加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析合称为链接。下面重点介绍前五个阶段。2.1 加载Loading加载是类加载的第一个阶段虚拟机需要完成三件事通过一个类的全限定名获取定义此类的二进制字节流可以从ZIP包、网络、运行时计算生成、JSP文件等获取。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口。这个Class对象相当于是类在方法区中的“镜像”通过它可以获取类的所有元数据信息。2.2 链接Linking链接阶段的主要任务是将加载阶段得到的二进制字节流整合到JVM的运行时状态中包括三个子步骤验证Verification确保Class文件的字节流中包含的信息符合当前虚拟机的要求并且不会危害虚拟机自身的安全。例如检查文件格式、元数据验证、字节码验证等。准备Preparation为类的静态变量分配内存并将其初始化为默认值零值。注意这里的分配内存仅包括静态变量static修饰不包括实例变量。实例变量将在对象实例化时随对象一起分配到Java堆中。对于final修饰的静态常量在准备阶段就会直接完成赋值即显式初始值而不是默认值。解析Resolution将常量池内的符号引用替换为直接引用。符号引用以一组符号描述所引用的目标可以是任何形式的字面量只要能够无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用的目标必须已经在内存中存在。2.3 初始化Initialization初始化阶段是类加载的最后一个步骤它真正开始执行类中定义的Java程序代码。在初始化阶段JVM会执行类的clinit()方法该方法由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而成。初始化是程序员对类变量进行显式赋值的过程例如public class MyClass { static int a 10; // 在初始化阶段执行赋值 static { // 静态代码块在初始化阶段执行 a 20; } }初始化阶段触发的时机包括遇到new、getstatic、putstatic、invokestatic这四条字节码指令时如果类未初始化则触发。使用java.lang.reflect包的方法对类进行反射调用时。当初始化一个类时如果其父类尚未初始化则先触发父类初始化。虚拟机启动时包含main()方法的主类会首先被初始化。当使用JDK 7的动态语言支持时如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄且该类未初始化则触发。3. 类加载器的作用与层次结构类加载器的主要作用就是实现上述的“加载”阶段。加载完成后类的信息被存放到方法区并在堆中生成对应的Class对象。我们可以通过Class对象的getClassLoader()方法获取加载它的类加载器。类加载器之间存在着逻辑上的父子关系但这种父子关系并不是通过继承实现的而是通过组合关系来维护。3.1 查看类加载器的层级下面这段代码可以打印出当前类的类加载器及其父加载器public class ClassLoaderDemo { public static void main(String[] args) { ClassLoaderDemo demo new ClassLoaderDemo(); // 应用程序类加载器 System.out.println(demo.getClass().getClassLoader()); System.out.println(ClassLoader.getSystemClassLoader()); // 平台类加载器扩展类加载器 System.out.println(demo.getClass().getClassLoader().getParent()); // 启动类加载器由于是C实现Java中返回null System.out.println(demo.getClass().getClassLoader().getParent().getParent()); // String类由启动类加载器加载因此getClassLoader()返回null String s new String(); System.out.println(s.getClass().getClassLoader()); } }运行结果类似jdk.internal.loader.ClassLoaders$AppClassLoader... jdk.internal.loader.ClassLoaders$AppClassLoader... jdk.internal.loader.ClassLoaders$PlatformClassLoader... null null4. 类加载器的分类从Java虚拟机的角度讲只存在两种不同的类加载器启动类加载器Bootstrap ClassLoader和其他所有类加载器。但为了方便描述我们通常将类加载器分为以下四种4.1 启动类加载器Bootstrap ClassLoader实现由C语言实现是虚拟机自身的一部分。职责负责加载存放在JAVA_HOME\lib目录中的核心类库或被-Xbootclasspath参数指定的路径中的类库。例如rt.jarJava 9之前、java.base模块Java 9及之后等。特点由于是C实现在Java代码中获取启动类加载器的引用时会得到null。4.2 扩展类加载器Extension ClassLoader/ 平台类加载器Platform ClassLoader在JDK 9之前称为扩展类加载器Extension ClassLoader由sun.misc.Launcher$ExtClassLoader实现负责加载JAVA_HOME\lib\ext目录下的类库或由-Djava.ext.dirs指定路径的类库。从JDK 9开始Java引入了模块化系统Project Jigsaw原来的扩展类加载器被重新设计为平台类加载器Platform ClassLoader负责加载一些平台相关的模块例如java.scripting、java.compiler等。4.3 应用程序类加载器Application ClassLoader也称为系统类加载器System ClassLoader由sun.misc.Launcher$AppClassLoaderJDK 8或jdk.internal.loader.ClassLoaders$AppClassLoaderJDK 9实现。它负责加载用户类路径ClassPath上指定的类库即我们开发中编写的绝大多数类都是由它加载的。可以通过ClassLoader.getSystemClassLoader()方法获取它。4.4 自定义类加载器如果上述三种加载器不能满足需求开发者还可以自定义类加载器通过继承java.lang.ClassLoader类并重写findClass()方法来实现。自定义类加载器可以用于加载非标准路径下的类、实现类版本控制、加密解密等高级功能。5. 双亲委派模型Parent Delegation Model双亲委派模型是Java类加载器工作的重要机制它保证了Java平台的稳定性和安全性。5.1 什么是双亲委派模型当一个类加载器收到类加载请求时它首先不会自己去尝试加载这个类而是将这个请求委派给父类加载器去完成每一层都是如此因此所有的加载请求最终都应该传送到最顶层的启动类加载器中。只有当父加载器反馈自己无法完成加载它的搜索范围内没有这个类时子加载器才会尝试自己去加载。这个过程可以概括为从下往上委托至上而下尝试加载。5.2 双亲委派模型的实现在java.lang.ClassLoader的loadClass()方法中已经实现了双亲委派模型的逻辑protected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先检查类是否已经被加载过 Class? c findLoadedClass(name); if (c null) { try { if (parent ! null) { c parent.loadClass(name, false); } else { c findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父类加载器抛出ClassNotFoundException说明父加载器无法完成加载 } if (c null) { // 如果父加载器加载失败则调用自己的findClass方法尝试加载 c findClass(name); } } if (resolve) { resolveClass(c); } return c; } }5.3 为什么要使用双亲委派模型避免类的重复加载父加载器已经加载过的类子加载器不会再重复加载保证了同一个类只被加载一次。保证核心类库的安全防止用户自定义的类覆盖核心API。例如如果用户编写了一个名为java.lang.String的类试图覆盖JDK核心类由于双亲委派模型该请求会一直向上委派给启动类加载器而启动类加载器会加载标准的String类从而保证核心类库的安全。这就是所谓的沙箱安全机制。5.4 双亲委派模型的破坏在某些场景下双亲委派模型可能被打破例如线程上下文类加载器JDBC、JNDI等服务提供者接口SPI的加载通常使用线程上下文类加载器来加载第三方实现类。热部署/热替换如Tomcat等Web容器每个Web应用使用独立的类加载器可以加载不同版本的类而不影响其他应用。OSGi模块化系统每个模块有自己的类加载器加载规则更加灵活。6. 总结类加载器是JVM的重要组成部分它负责将字节码文件加载到内存中为程序的运行提供基础。理解类的加载过程、类加载器的层次结构以及双亲委派模型不仅有助于我们诊断类冲突、NoClassDefFoundError等问题还能为编写自定义类加载器、实现热部署等高级功能打下基础。Java的类加载机制在设计上兼顾了性能、安全性和灵活性是Java平台稳定运行的基石之一。希望本文能帮助你更深入地理解这一机制在未来的开发中游刃有余。