资讯动态

JVM类加载子系统全解析:从双亲委派到自定义类加载器

发布时间:2026/10/24 5:10:00 来源:尧图企业网站定制
很多Java开发者写了好几年业务代码第一次被JVM的类加载子系统“教育”通常是从某个诡异报错开始的ClassNotFoundException、NoClassDefFoundError或者“同一个类在两个ClassLoader里长得不一样”。我第一次真正理解ClassLoader是帮人排查一个明明classpath里什么都有、启动却找不到类的线上问题折腾到凌晨才发现是两个类加载器各加载了一份类。从那一刻起我意识到类加载子系统不是面试题里的纸面概念而是每个JVM应用都要走一遍的“字节码入职流水线”。这篇文章把JVM类加载子系统拆开讲透它接收什么、产出什么加载、链接、初始化三个阶段到底干了什么事三个内置类加载器和双亲委派机制为什么这么设计以及怎么手写一个自定义类加载器来做加密加载和模拟热部署。最后我会把ClassNotFoundException和NoClassDefFoundError的区别、Class.forName与loadClass的差异这些高频问题一起清一遍适合正在学JVM的开发者也适合准备面试或者排查运行时类加载问题的人。1. 类加载子系统在整台JVM里的“工位”输入、输出与全程数据流1.1 它到底接收什么、产出什么类加载子系统的职责简单说就是一句话“把class文件里的二进制字节流变成JVM运行时可以直接使用的Class对象。”它不是执行引擎并不知道方法内部怎么跑它也不管对象实例怎么创建。它只负责“把一个类从无到有地立起来”。它接收的输入是类的全限定名比如com.example.demo.UserService和对应的二进制字节流。这个字节流不一定来自磁盘上的.class文件也可以是jar包、网络传输、动态生成代理类、JSP编译产物、甚至加密后的class字节。JVM规范对“从哪里拿class”没有硬性要求只要你给它字节流。它输出的结果有两块一块是方法区HotSpot里对应元空间里的类元数据包括字段结构、方法签名、常量池、方法字节码这些另一块是堆内存里的java.lang.Class对象。注意每个被加载的类都会在堆里有一个对应的Class对象它是程序通过反射访问类元数据的入口也是getClass()返回的东西。这两块要分清楚类元数据可以理解为“档案”Class对象是“档案的窗口”。实例对象userService在初始化时创建它的类信息引用指向Class对象Class对象引用指向方法区里的元数据。很多人把Class对象和类元数据混在一起一旦后面讲到元空间OOM和类卸载就会绕晕。1.2 一个类的完整“入职”时间线把整个过程按时间线排开大概是这样的代码里第一次出现new UserService()或者访问了某个类的静态字段/静态方法。JVM检查类是否已经被加载。没加载就交给类加载子系统。类加载子系统通过全限定名获取二进制字节流完成“加载”。加载完成后进入“链接”验证字节码合法性、给静态变量分配默认值、把符号引用替换成直接引用。链接完成后进入“初始化”执行clinit方法给静态变量赋真实值、执行静态代码块。类加载完成JVM开始真正执行new字节码在堆上创建实例。这里有个重要细节类加载的三个阶段——“加载、链接、初始化”——是严格按顺序开始的但链接里的“解析”不是必须等到所有验证和准备都做完才开始。JVM规范允许解析被延迟甚至在某个类已经开始初始化之后才解析它的部分符号引用这是为了支持动态语言绑定等高级场景。所以面试里如果被问到“解析一定发生在初始化之前吗”答案是不一定。还有一个容易被忽略的点什么时候会触发加载JVM规范用的是“首次主动使用”而不是“首次出现”。写了一百处new UserService()只有第一次执行到它时才触发加载和初始化。这也是后面讲“被动引用”的底层逻辑。2. 加载与链接之间的细节验证、准备、解析为什么容易被忽略很多人把“加载”和“类加载”混为一谈其实“加载”只是整个流程里最前置的一步。加载阶段做的事情是通过全限定名获取二进制字节流、把字节流转换成方法区数据结构、在堆里生成Class对象。真正决定一个类“能不能被安全使用”的是接下来的链接阶段。链接包含三个子步骤验证、准备、解析。这三个步骤不像加载那样有炫酷的ClassLoader参与所以经常被跳过但线上很多诡异的类加载报错恰恰是这三个环节出的问题。2.1 验证阶段在拦什么验证的目标就一句话防止恶意或不完整的字节码破坏JVM运行时。字节码不是只能由javac生成任何人都可以手工构造。所以JVM必须验证字节码的每一个细节。规范要求的验证包括四类文件格式验证、元数据验证、字节码验证、符号引用验证。文件格式验证检查魔数是不是0xCAFEBABE、主次版本号是否在JVM支持的范围内元数据验证检查这个类是否有父类、是否继承了不该继承的final类、抽象方法是否有实现字节码验证对方法体的指令流做数据流分析确保操作数栈的类型不会出问题符号引用验证发生在解析阶段确认类确实能访问到引用的符号。给你一个直观的例子如果你用javap -c看编译后的class会发现方法里大量的invokevirtual、getstatic指令后面跟着常量池索引。如果手工篡改字节码把一个本该加载字符串的位置改成加载一个对象引用类型就对不上字节码验证阶段直接失败抛出VerifyError。这个错误我见过不少大多发生在字节码增强框架比如那些AOP代理工具版本不兼容时。排查建议看到javalang.VerifyError先检查依赖里做字节码增强的库和JDK版本是否匹配不要一上来就怀疑业务代码。2.2 准备阶段给静态变量发的“默认工资”准备阶段是为静态变量分配内存并设置默认值的阶段。重点这里设置的是默认值不是代码里写的初值。举个例子public class Person { static int age 30; static final String name Tom; }在准备阶段age的值是0不是30。name因为是编译期常量static final且值在编译期确定会直接被赋成“Tom”它不进clinit方法里赋值这是准备阶段唯一的例外。age 30这个动作会被javac收集到clinit方法里等初始化阶段执行。这个过程理解透了遇到“静态字段的值好端端变成0/null”的诡异问题就能快速定位大概率类还没走到初始化阶段或者初始化由于异常被中断了。之前我排查过一个案例一个工具类里有个静态Map运行时一直是null后来发现是另一个线程先触发了类初始化但静态代码块里抛了异常导致clinit失败之后所有线程拿到的都是“半初始化”的状态。2.3 解析符号引用到直接引用别小看这张“地图”解析阶段做的事是把常量池里的符号引用替换为直接引用。符号引用是什么可以理解为一串“字面地址”比如类名com/example/Service、字段描述符java/lang/String、方法名sayHelloJVM看到这些符号还不能直接干活必须找到它们对应的真实内存地址或运行时数据结构才能执行指令。直接引用在HotSpot里通常是指向方法区里相关数据的指针、字段偏移量或者句柄。之所以叫“直接”是因为JVM拿到它之后不需要再做任何查表换算直接用就行。解析最常发生在哪个指令上invokevirtual。JVM在执行invokevirtual时面对的是一个接口方法或虚方法需要根据实际对象类型做动态分派所以解析过程还会引入方法表查找。这也是为什么动态代理会产生额外的性能成本——每个代理方法调用都要走方法查找与解析。有一个实战细节如果一个类在链接阶段解析失败错误记录下来后续访问同类引用会直接抛NoClassDefFoundError或IllegalAccessError。所以要分辨“类文件找不到”和“类文件存在但解析不了”后者往往更隐蔽因为会拿着一个完整的类路径去排查结果发现那个类在包里好好的。3. 初始化阶段真正执行static逻辑的时刻加载、链接都完成之后类还处于“图纸阶段”——变量都是默认值静态字段没有真正赋初始值。初始化阶段执行clinit方法才把静态初始化块和静态变量赋初值这些代码真正跑一遍。clinit是类初始化方法不是构造函数。它由javac生成收集所有静态变量赋值动作和静态代码块按它们在源码里出现的顺序合并而成。注意实例构造器是init类初始化是clinit两者完全不一样。3.1 触发初始化的六种主动引用到底哪些情况会触发类初始化JVM规范列得很明确归纳成六类new一个类的实例创建对象是直接访问一个类的“实体”必须初始化。访问类的静态字段getstatic或给静态字段赋值putstatic如果该字段不是编译期常量。编译期常量的值在编译后直接写进使用方代码里根本不需要访问定义类。调用类的静态方法invokestatic。对类进行反射调用比如Class.forName(com.example.User)默认会执行初始化注意Class.forName(name, initialize, loader)有一个参数控制是否初始化默认initializetrue。初始化类时如果它的父类还没初始化先触发父类初始化。这里注意父类先于子类初始化。程序入口类也就是包含main方法的那个类JVM启动时会先初始化它。直接看到这里可能觉得很简单但考察点是“被动引用”。比如通过子类引用父类的静态字段只会触发父类的初始化子类不初始化。看这段class Parent { static int value 42; static { System.out.println(Parent init); } } class Child extends Parent { static { System.out.println(Child init); } } public static void main(String[] args) { System.out.println(Child.value); }输出是“Parent init”和42没有“Child init”。为什么因为value定义在父类访问它的字节码是getstatic Parent.valueJVM只需要确保Parent已初始化并不会因此去初始化Child。类似的Parent[] arr new Parent[10]创建的是数组对象初始化一个“数组类”不会触发Parent初始化。这些“看起来会但实际不会”的场景是最容易在面试里翻车的。3.2clinit的三个容易被忽视的坑第一个坑线程安全。JVM保证一个类的clinit方法在多线程环境下只被执行一次。当多个线程同时首次触发这个类的初始化时只有一个线程会执行clinit其他线程阻塞等待。如果执行clinit的线程陷进去了——比如静态代码块里死循环、长时间持锁、发起网络请求——其他所有使用这个类的线程全部卡住表现就是应用“假死”。第二个坑异常中断。clinit如果抛出异常JVM会给首次捕捉到这个异常的线程抛ExceptionInInitializerError并且把这个类标记为“初始化失败”。下次其他线程再访问这个类不会再尝试初始化而是直接抛NoClassDefFoundError。这个问题非常隐蔽因为日志里只会看到第一次的异常堆栈后面的NoClassDefFoundError很容易误导人往“类缺失”方向排查。第三个坑父类先初始化但接口不一定。初始化子类之前父类一定先初始化这是强约束。但接口的初始化规则不一样接口初始化时并不要求父接口先完成初始化除非子接口在运行中使用到了父接口的static字段。这是因为接口里的静态字段一旦被引用就意味着真的需要访问该接口的成员这时才初始化对应接口。实战中还有一个小坑静态字段初始化顺序。看这段public class Order { static { count 10; // 这里count已经被赋值但还看不到它的“最终初值” // 因为后面才执行 static int count 5; } static int count 5; }静态代码块里给count 10之后紧接着的static int count 5;还会再覆盖一次最终结果永远是5。所以在静态代码块里读静态字段前心里要有一张“源码顺序执行”的流程图不然很容易写出逻辑看似对、结果全错的初始化代码。4. 类加载器的三层模型与双亲委派谁是谁的爸爸上面讲的加载、链接、初始化属于“同类的一生”现在换到另一个维度这个类到底是由谁加载进来的。JVM里的类加载器不是只有一个而是一个层级体系。4.1 三个内置类加载器的管辖范围JDK 8与JDK 9差异JVM预置了三个类加载器从顶层到底层启动类加载器Bootstrap ClassLoader负责加载JDK核心类比如java.lang.String、java.lang.Object、java.util.*。它由C实现不是java.lang.ClassLoader的子类。JDK 8里加载JAVA_HOME/lib下被JVM识别的类JDK 9之后由于模块化它加载的是java.base等基础模块。扩展类加载器JDK 8叫ExtensionJDK 9后叫平台类加载器PlatformJDK 8里加载JAVA_HOME/lib/ext下的jar包以及被java.ext.dirs指定的目录。JDK 9以后Java SE标准类库被极度模块化平台类加载器负责加载的是java.sql、java.xml、java.logging这些“非基础但属于平台”的模块。应用程序类加载器AppClassLoader加载classpath下的用户类也就是我们平时的com.example这类业务代码。它是ClassLoader类的子类在sun.misc.Launcher内部创建。JDK 9之后最大的变化就是Extension被替换成Platform。面试和工作中如果看到ExtClassLoader的旧资料要知道那是JDK 8的写法到了JDK 17还照搬旧概念很容易踩坑。4.2 双亲委派执行链路与loadClass源码真相双亲委派机制定义很简洁当一个类加载器收到类加载请求时它不会自己先去加载而是把这个请求委托给父类加载器。父类加载器再委托给自己的父类加载器直到启动类加载器。如果父类加载不了子加载器才自己尝试加载。这里说的“父子关系”不是继承关系而是组合关系。每个类加载器实例内部持有parent字段指向自己的父加载器。看JDK的经典实现逻辑JDK 8的AppClassLoader、ExtClassLoader都沿用了这套模板protected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查这个类是不是已经加载过了 Class? c findLoadedClass(name); if (c null) { try { // 2. 有父加载器先让父加载器加载 if (parent ! null) { c parent.loadClass(name, false); } else { // 3. 没有父加载器说明是顶层交给启动类加载器 c findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父加载器都没找到说明这个类不在父的管辖范围 } if (c null) { // 4. 父加载不了自己来 c findClass(name); } } if (resolve) { resolveClass(c); } return c; } }注意第1步先查重。findLoadedClass检查这个类在当前类加载器里是不是已经加载过。如果已经加载直接返回不会重复加载。这是JVM保证类加载“一次”的重要机制。另外注意loadClass整体是同步的锁对象是“类加载锁”。这也是为了并发安全防止同一个类加载器并发加载同一个类时出现两份Class对象。4.3 为什么这么设计类唯一性、安全沙箱与“爸爸优先”双亲委派不是拍脑袋定的它解决三个核心问题。第一个是避免类的重复加载。如果每个类加载器都自己从头加载java.lang.String你的代码里可能会出现两个“String类”并存各自的equals判定都不一样整个类型系统直接崩溃。第二个是安全。因为每层请求都先向上委托用户自定义的类加载器根本接触不到核心类无法用偷换java.lang包的方式包藏恶意代码。启动类加载器只会加载信任目录下的核心类这种“爸爸先试”的路径天然形成一道安全沙箱。第三个是类唯一性。JVM里判断两个类是不是同一个“类”不仅看全限定名是否相同还要看它们是否由同一个类加载器加载。即使同一个class文件被两个不同的类加载器各加载一次它们也是两个不同的Class对象。这个原理在后面讲类卸载和热部署时是核心也是无数框架bug的根源。一个经典测试两个自定义加载器分别加载同一个Hello.class然后用aClass.isInstance(instanceFromB)判断返回false。不是“长得像”就等于同一个类在JVM眼里加载器不同就是“不同种族”。5. 双亲委派被破坏的典型场景SPI与热部署双亲委派不是万能的有些场景必须“逆行”才能工作。最典型的就是Java的SPI机制Service Provider Interface以及热部署、OSGi这类模块化容器。5.1 线程上下文类加载器SPI的“借道”先想一个问题数据库驱动接口定义在java.sql.Driver这个接口由启动类加载器加载。但驱动实现类比如某个厂商写的客户端Driver是放在classpath里的jar包按双亲委派路径启动类加载器根本看不到它AppClassLoader又因为有“爸爸优先”的规则而不会主动去加载。于是DriverManager在加载驱动时不能靠Class.forName(驱动类名)这种默认双亲委派的方式因为线程上下文类加载器不在默认委托链上它调用了Thread.currentThread().getContextClassLoader()拿到线程上下文类加载器然后由这个加载器去加载classpath里的数据库驱动实现类。这个上下文加载器默认是AppClassLoader所以能加载用户jar里的类。这类场景总结为核心类库在顶层加载器里但需要加载底层加载器才可见的实现类。JDK里很多SPI扩展点都用了这个模式比如JNDI、XML解析器、数据库驱动等。面试时如果被问“哪里打破了双亲委派”SPI 线程上下文类加载器是答案榜第一名。5.2 热部署/模块化打破双亲委派的代价热部署的核心需求是同一个类名在旧版本class被替换后能重新加载一份“新版本”。如果走双亲委派AppClassLoader加载了com.example.Task之后后面改成新代码findLoadedClass一查发现已经加载过直接返回旧版Class新代码根本不会生效。所以Tomcat、OSGi这类容器设计了更复杂的类加载器体系。Tomcat的每个Web应用都有自己的WebAppClassLoader放弃“永远先给父加载器”的严格规则优先加载自身WEB-INF/classes目录下的类实现应用间的类隔离和热部署。OSGi更进一步每个Bundle模块都有独立的类加载器类可见性由导入/导出包配置控制完全自定义了加载顺序。这里的代价很明显类加载顺序不再是线性委托而是变成一张复杂的依赖图。一旦多个模块里有同名类就会产生“类冲突”问题——两个Bundle各自加载出一份同名的类互相强转直接报ClassCastException而且异常信息看起来莫名其妙“A cannot be cast to A”。业界很多框架的版本冲突问题根源都在这。6. 动手写一个自定义类加载器从加密class到模拟热替换双亲委派说得再多不如自己写一个类加载器。自定义类加载器的常见需求有两个一是从特殊来源加载字节码比如文件加密、网络传输二是绕过父加载实现“同名类重新加载”来模拟热部署。6.1 重写 findClass 而不是 loadClass自定义类加载器最推荐的做法是继承ClassLoader重写findClass方法。为什么重写findClass而不是重写loadClass因为JDK源码里的loadClass已经实现了完整的双亲委派逻辑先查已加载、再找父加载器、父找不到才调findClass。我们只需要在“父加载器没找到”的兜底阶段告诉JVM“从哪些自定义位置获取字节流”剩下的事JVM帮你处理。如果直接重写loadClass等于把整条双亲委派链亲手拆掉一般只有写容器框架才需要这种操作。标准模板长这样public class FileClassLoader extends ClassLoader { private final String path; public FileClassLoader(String path) { this.path path; } Override protected Class? findClass(String name) throws ClassNotFoundException { byte[] bytes loadClassData(name); if (bytes null) { throw new ClassNotFoundException(name); } // defineClass 是 ClassLoader 提供的方法把字节数组变成 Class 对象 return defineClass(name, bytes, 0, bytes.length); } private byte[] loadClassData(String name) { String fileName path / name.replace(., /) .class; try (InputStream in new FileInputStream(fileName); ByteArrayOutputStream out new ByteArrayOutputStream()) { byte[] buffer new byte[1024]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } return out.toByteArray(); } catch (IOException e) { e.printStackTrace(); return null; } } }核心就两个方法loadClassData负责拿到字节数组defineClass负责把字节数组转换成Class对象。defineClass是ClassLoader里的一个protected方法不需要自己实现直接调用即可。6.2 加密字节码加载的实战例子如果class文件被加密过普通ClassLoader读出来的是一堆乱码字节码验证直接失败。解决办法就是在loadClassData里拿到原始字节流后、调用defineClass之前先做一次解密。实际项目中通常用AES或者异或做混淆。简单起见用一个异或加密示意public class EncryptClassLoader extends ClassLoader { private static final byte KEY 0x5A; private final String classPath; public EncryptClassLoader(String classPath) { this.classPath classPath; } Override protected Class? findClass(String name) throws ClassNotFoundException { byte[] encrypted loadClassData(name); byte[] decrypted new byte[encrypted.length]; for (int i 0; i encrypted.length; i) { decrypted[i] (byte) (encrypted[i] ^ KEY); } return defineClass(name, decrypted, 0, decrypted.length); } }这里的逻辑很简单加密时对class文件的每个字节异或一个KEY解密时再异或一次。真实防破解当然不会用这么弱的方式但思路是通用的——在字节流进入defineClass之前你有充分的自由度对字节做处理。很多商业软件把整个class加密再用native方法在启动时解密骨架都是这样。这里提醒一句加密class只能增加逆向门槛不能做到绝对安全。因为JVM最终还是要拿到明文字节码只要进程在运行字节码就在内存里可以被dump。做版权保护要考虑的是一整套方案别指望一个加密加载器解决所有问题。6.3 同名类“热替换”的简易Demo与局限模拟热部署的最简单做法不是修改父加载逻辑而是每次创建一个新的类加载器实例去加载同名类。因为JVM判断“同类”时类加载器也是标准之一。新的类加载器是新对象它的findLoadedClass缓存是空的所以同一个class文件能被再加载一遍。示例逻辑public class HotSwapDemo { public static void main(String[] args) throws Exception { while (true) { FileClassLoader loader new FileClassLoader(./classes); Class? clazz loader.loadClass(demo.Hello); Object instance clazz.newInstance(); clazz.getMethod(say).invoke(instance); Thread.sleep(3000); } } }只要我在./classes/demo/Hello.class里改了内容并重新编译下一轮循环就会加载出新的Class对象。但这里有个关键局限旧对象还在堆里新的类和旧的对象之间没有任何血缘关系。如果尝试把旧对象强转成新加载的类类型结果是ClassCastException。真正的热部署框架要复杂得多需要把对象状态迁移、线程上下文切换、资源清理都处理干净。所以“热替换”在Demo层面很容易在生产层面极难。如果你只是在本地实验理解这个概念就够了如果要上生产系统老老实实用成熟的容器和框架别自己造热部署轮子。7. 类加载相关的疑难杂症与高频辨析最后把类加载子系统里最容易混淆的几个问题一次性讲透这些也是面试和日常排查中最常遇到的点。7.1 ClassNotFoundException 与 NoClassDefFoundError 到底差在哪这两个异常名字接近但本质完全不同。ClassNotFoundException是一个受检异常checked exception通常由Class.forName()、ClassLoader.loadClass()这类显式加载方法抛出表示“你要我加载的类我在指定加载器里找不到”。它发生在“主动找类”的场景解决思路简单检查classpath、检查类名拼写、检查加载器能否看到那个类。NoClassDefFoundError是一个Error发生在“类已经被编译但运行时加载失败”或“静态初始化失败”之后。典型场景有两个第一某个类在编译期存在运行期却不在classpath里第二更阴间的是这个类在初始化阶段抛异常了JVM标记它为“初始化失败”后续再使用它时直接抛NoClassDefFoundError。线上排查时如果只看到NoClassDefFoundError先往两个方向看底层依赖丢没丢、该类初始化时有没有抛过ExceptionInInitializerError。很多时候真正的异常栈发生在很久之前被别的线程吞了要往回翻日志。7.2 Class.forName 和 ClassLoader.loadClass 的差异Class.forName(com.example.User)和loader.loadClass(com.example.User)最大的差别默认情况下Class.forName会执行类的初始化触发clinit而ClassLoader.loadClass不会。底层原理是Class.forName有一个参数boolean initialize默认true。所以经典用法是加载数据库驱动时用Class.forName(某个驱动类)因为驱动的静态代码块里做了注册动作而Spring的ClassUtils.forName、以及一些延迟加载场景会刻意用ClassLoader.loadClass或指定initializefalse就是为了“只加载类不执行静态逻辑”。这个差异在动态生成类、延迟初始化、以及判断“加载体只要Class对象不要副作用”的场景里非常关键。比如做字节码增强时如果增强类的静态块有耗时逻辑用了Class.forName会把启动时间拖慢一大截而loadClass就不会。7.3 类何时能被卸载元空间OOM的反思很多人以为类加载进来就永远留在内存里其实不是。类可以被卸载但有严格条件只有当加载这个类的类加载器本身被回收且该类没有实例、Class对象没有外部引用时这个类才能被JVM回收。内置类加载器AppClassLoader、PlatformClassLoader等通常生命周期和应用一样长它们加载的类基本不会卸载。会频繁产生“可卸载类”的场景几乎都是自定义类加载器的场景每次热部署新建一个类加载器、大量使用动态代理生成新类、频繁用反射框架做字节码生成、JSP热编译新class等。如果这类操作失控元空间Metaspace会持续增长最终触发java.lang.OutOfMemoryError: Metaspace。排查这类问题的核心就是回答一个问题除了JVM内置加载器还有谁在源源不断地创建类加载器或加载新类常见元凶包括滥用Class.forName创建大量临时类、缓存的动态代理类没有释放、每次请求都新建一个ClassLoader、脚本引擎反复编译同一个脚本。我排查过的一个案例某个接口被调用一次就创建一个动态代理Class代理类数量线性增长重启前元空间涨到几个G。这类问题光调-XX:MaxMetaspaceSize只能治标根本解法是让Class生成可控、复用缓存、及时释放类加载器引用。这几年和类加载子系统打过不少交道最大的体会是它不像GC、并发那些机制一样每天都能感知到但一旦出问题往往都是“最讲道理却最难看到全景”的类型。写业务代码时确实很少直接操作ClassLoader可只要你在做中间件、插件化、热部署、字节码增强、或者排查线上诡异类冲突这张“字节码入职流水线”的完整地图就是最趁手的工具。建议你按这篇文章的内容自己写一个自定义类加载器跑一遍加密加载和同名类加载的Demo再动手验证一下子类引用父类静态字段不初始化子类的输出顺序。做完这些类加载子系统在你这里就不再是面试提纲而是真正能指挥调试的心智模型。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑