类加载类加载流程类加载是指将.class字节码文件从磁盘、网络或其他来源加载到JVM内存中并生成对应的java.lang.Class对象的过程。这个过程不是一次性完成的而是遵循固定的顺序分为五个阶段1. 加载 (Loading)通过类的全限定名如java.lang.String获取其二进制字节流.class文件。将这个字节流存储在方法区元空间中并在堆中创建一个java.lang.Class对象作为程序访问方法区数据的入口。字节码不仅可来自本地文件还可来自网络、JAR 包、数据库甚至是运行时动态生成如动态代理。2. 验证 (Verification)确保加载的字节码是合法且安全的防止恶意代码破坏 JVM。校验内容文件格式检查魔数CAFEBABE、版本号是否兼容。元数据检查类的继承关系如不能继承final类。字节码检查指令是否合法避免非法操作内存。符号引用检查引用的类、方法是否存在。3. 准备 (Preparation)为类的静态变量分配内存并设置默认初始值。此阶段不执行赋值逻辑。例如static int a 10;在准备阶段a的值是0默认值而不是10。赋值10的操作会在初始化阶段执行。如果是static final修饰的常量准备阶段会直接将其赋值为代码中定义的值因为编译期已确定。4. 解析 (Resolution)将常量池中的符号引用Symbolic Reference替换为直接引用Direct Reference。符号引用就像是“某小区某栋楼某单元”直接引用就是具体的“内存地址”。解析就是把抽象的描述转换成具体的内存指针。5. 初始化 (Initialization)执行类构造器clinit()方法的过程即执行静态代码块 (static{}) 和静态变量的赋值语句。这是类加载过程中唯一执行开发者编写的 Java 代码的阶段。只有当类被首次主动使用时如创建实例、调用静态方法才会触发初始化。双亲委派机制双亲委派机制是 Java 类加载器ClassLoader在加载类时遵循的一种层级委托模型。简单来说当一个类加载器收到加载类的请求时它不会自己立即去加载而是把这个请求委派给它的父类加载器去完成。每一层都是如此因此所有的请求最终都会传送到顶层的启动类加载器。只有当父类加载器无法完成加载在它的搜索路径下找不到该类时子类加载器才会尝试自己去加载。类加载器的层级结构启动类加载器 (Bootstrap ClassLoader)最顶层由 C 实现在 Java 中表现为null。加载 Java 核心类库如java.lang.*,java.util.*通常位于$JAVA_HOME/lib目录下如rt.jar。扩展类加载器 (Extension ClassLoader / Platform ClassLoader)中间层由 Java 实现。加载 Java 的扩展库。JDK 8 及之前位于$JAVA_HOME/lib/extJDK 9 改为加载平台模块。应用程序类加载器 (Application ClassLoader)也称系统类加载器是默认的类加载器。加载用户类路径ClassPath上指定的类库。工作流程1. 向上委托应用程序类加载器收到请求 → 委派给扩展类加载器 → 委派给启动类加载器。2. 向下尝试启动类加载器检查rt.jar等核心库发现没有com.example.User加载失败。扩展类加载器检查扩展库目录没有找到加载失败。应用程序类加载器在 ClassPath 中找到了com.example.User.class读取字节码加载成功。优点安全性 (防止核心 API 被篡改)这是最主要的原因。假设你自定义了一个java.lang.String类试图注入恶意代码。根据双亲委派请求会先交给启动类加载器。启动类加载器会在核心库中找到官方的String类并加载而你的恶意类永远不会被加载。这保证了核心类库的沙箱安全。避免类的重复加载确保一个类在 JVM 全局中只有唯一的一份。父类已经加载过了子类就不再加载避免了同一个类被多次加载导致的内存浪费和类型转换异常。类的唯一性对于任意一个类都由“加载该类的类加载器”和“类本身”共同确定其在 JVM 中的唯一性。即使字节码相同由不同类加载器加载的类也被视为不相等。打破双亲委派虽然双亲委派是默认规则但在某些特殊场景下需要“打破”它SPI (服务提供商接口)如 JDBC。核心的数据库接口由启动类加载器加载但具体的数据库驱动如 MySQL 驱动由应用程序类加载器加载。此时需要通过线程上下文类加载器Thread Context ClassLoader让父类加载器去请求子类加载器加载实现类。热部署/热更新如 JRebel 或 Spring DevTools需要自定义类加载器来实现类的重新加载。模块化框架如 OSGi它使用了一种网状的类加载结构而非简单的树状结构。自定义类加载器示例Java 中自定义类加载器通常是为了实现一些特殊需求比如热部署不重启应用更新代码、插件化架构隔离不同插件的依赖或者加密类文件防止反编译步骤 1创建自定义类加载器importjava.io.ByteArrayOutputStream;importjava.io.File;importjava.io.FileInputStream;importjava.io.IOException;/** * 自定义类加载器从指定目录加载 .class 文件 */publicclassCustomClassLoaderextendsClassLoader{// 指定类文件所在的目录路径privateStringclassPath;publicCustomClassLoader(StringclassPath){this.classPathclassPath;}/** * 核心方法根据类名查找并加载类 * param name 类的全限定名 (如 com.example.MyClass) * return Class对象 * throws ClassNotFoundException */OverrideprotectedClass?findClass(Stringname)throwsClassNotFoundException{// 1. 将类名转换为文件路径// 例如: com.example.MyClass - /path/com/example/MyClass.classStringfileNameclassPathFile.separatorname.replace(.,File.separator).class;try{// 2. 读取 .class 文件的字节码byte[]classDataloadClassData(fileName);if(classDatanull){thrownewIOException(类文件不存在: fileName);}// 3. 调用父类的 defineClass 方法将字节码转换为 Class 对象// 这是自定义类加载器的关键一步returndefineClass(name,classData,0,classData.length);}catch(IOExceptione){thrownewClassNotFoundException(无法加载类: name,e);}}/** * 读取文件字节码的辅助方法 */privatebyte[]loadClassData(StringfileName)throwsIOException{try(FileInputStreamfisnewFileInputStream(fileName);ByteArrayOutputStreambaosnewByteArrayOutputStream()){intch;while((chfis.read())!-1){baos.write(ch);}returnbaos.toByteArray();}}}步骤 2编写测试类// 保存为 /tmp/src/com/example/MyTest.javapackagecom.example;publicclassMyTest{publicvoidsayHello(){System.out.println(Hello, 我是由自定义类加载器加载的);}}编译# 创建目录mkdir-p/tmp/classes/com/example# 编译并输出到指定目录javac-d/tmp/classes /tmp/src/com/example/MyTest.java步骤 3使用自定义加载器publicclassClassLoaderDemo{publicstaticvoidmain(String[]args)throwsException{// 1. 创建自定义类加载器指定路径CustomClassLoadercustomLoadernewCustomClassLoader(/tmp/classes);// 2. 加载类 (注意此时不会初始化类即 static 代码块不会执行)Class?clazzcustomLoader.loadClass(com.example.MyTest);// 3. 创建实例并调用方法Objectobjclazz.newInstance();// 或使用构造器反射clazz.getMethod(sayHello).invoke(obj);}}