ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java类加载过程梳理,一篇搞定2万字详解

Java类加载过程梳理,一篇搞定2万字详解 引言为什么要深入理解类加载很多 Java 工程师写了多年业务代码对集合、并发、Spring 等框架使用得炉火纯青但一被问到「类的加载过程是怎样的」「双亲委派机制为什么这么设计」「什么场景会打破双亲委派」时往往只能说出一两个名词再往深处追问就开始含糊。类加载机制是 JVM 的核心支柱之一它决定了类什么时候被加载进内存、如何被验证和初始化也直接影响了热部署、字节码增强、依赖隔离、插件化架构等高级特性的实现。本文将从类加载的生命周期出发完整梳理加载、验证、准备、解析、初始化等各个阶段深入剖析类加载器体系与双亲委派模型并结合 JDK 源码、实战代码和面试高频问题带你彻底吃透 Java 类加载过程。全文约两万字建议配合 JDK 源码和调试工具一起阅读。阅读提示本文默认读者已经掌握 Java 基础语法并对 JVM 内存区域有一定了解。文章中的代码均基于 JDK 8 与 JDK 11 的 HotSpot 虚拟机实现进行说明。一、类加载机制概述Java 语言最初的设计目标之一就是「一次编写到处运行」。为了实现这个目标Java 编译器并不会像 C/C 那样把源代码直接编译成与特定硬件平台绑定的本地机器码而是把.java源文件编译成与平台无关的字节码文件.class。这个.class文件里保存的是 JVM 认识的指令集真正负责把这些字节码转变为可以在内存中使用、能被执行的数据结构的过程就是类加载机制要完成的工作。从更高的视角看类加载机制回答了三个核心问题类从哪来、类怎么进来、类何时进来。类可以来自本地磁盘、网络、JAR 包、数据库甚至是运行时动态生成的字节码类进来之后要经过一系列校验、转换、解析最终形成方法区在 JDK 8 及以后是元空间中可用的Class对象而类的加载时机则由 JVM 根据「首次主动使用」的原则来决定。与 C/C 的静态链接不同Java 采用的是动态加载与动态连接机制。程序运行时哪个类被真正用到了JVM 才会去加载它这意味着开发者可以在运行时按需加载类甚至可以自定义类加载器从任意来源加载类这也为后续要讲的自定义类加载器、热部署、OSGi 等项目铺平了道路。二、类加载的生命周期2.1 类从字节码到可用的完整链路一个类从磁盘或者网络上的字节码文件到最终能够在 JVM 中创建对象、调用方法需要经历一个漫长而严谨的过程。按照《Java 虚拟机规范》的定义类的完整生命周期包括七个阶段加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization、使用Using和卸载Unloading。这七个阶段中验证、准备、解析三个阶段又可以统称为「连接」Linking。它们的先后关系如下表所示阶段英文名称归属核心工作加载Loading独立阶段读取字节码生成 Class 对象验证Verification连接确保字节码安全合法准备Preparation连接为静态变量分配内存并赋零值解析Resolution连接把符号引用替换为直接引用初始化Initialization独立阶段执行类构造器 static 代码使用Using独立阶段程序正常使用该类卸载Unloading独立阶段类被 GC 回收需要注意的是虽然规范把生命周期描述为一条顺序执行的链路但实际执行时并不强制要求必须等待一个阶段完全结束后再开始下一个阶段。最典型的就是「解析」阶段它有时候会在「初始化」之后才执行这就是 Java 语言运行期绑定的体现。此外加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的类是必须按照这个顺序按部就班地「开始」而解析阶段则可以在初始化之前或之后进行。2.2 类生命周期的触发时机JVM 规范并没有明确规定「加载」阶段必须在什么时刻发生它可以由虚拟机自行决定。但对于「初始化」阶段规范则做出了严格规定有且只有六种情况必须立即对类进行初始化这六种情况也被称为「主动引用」。除此之外的所有引用类的方式都不会触发初始化被称为「被动引用」。主动引用与被动引用会在后文专章讲解这里先建立整体认识类加载并不是一下子完成的而是在某个触发点到来时沿着生命周期逐步推进。还有一个容易被忽视的细节类加载过程中的「使用」并不是一个可以独立划分时间段的阶段它是类初始化之后程序执行过程中持续发生的状态。所以平时面试时说的「类的五个阶段」一般是指加载、验证、准备、解析、初始化这五个可以被观察和分析的阶段。三、类加载过程详解3.1 加载阶段把字节码变成元数据加载是类加载过程的第一个阶段它的任务用一句话概括就是把类的字节码数据从各种来源读取到内存中并转化为方法区JDK 8 之前叫永久代JDK 8 及以后叫元空间中的运行时数据结构同时在堆内存中生成一个对应的java.lang.Class对象作为访问这个类元数据的入口。具体来说加载阶段要完成三件事获取字节流通过类的全限定名来获取定义此类的二进制字节流。这里的「获取」来源非常广泛可以是从 ZIP、JAR、WAR 包中读取也可以从网络中获取甚至可以在运行时动态生成。转化为运行时数据把这个字节流所代表的静态存储结构转化为方法区内的运行时数据结构。生成 Class 对象在堆中生成一个代表这个类的java.lang.Class对象作为方法区中该类数据的外部访问入口。很多人会把「加载」和「类加载」两个概念混淆。类加载是包含加载、验证、准备、解析、初始化在内的一整套过程而加载只是其中的第一个阶段。加载阶段所需要的字节流来源在 JVM 规范中并没有限定必须来自.class文件这就给了开发者极大的发挥空间。像常见的动态代理技术就是在运行时通过Proxy直接生成字节码字节数组再由类加载器加载像 ASM、CGLIB 等字节码框架也都是在运行时生成新的类。对于数组类来说情况比较特殊数组类本身不通过类加载器创建而是由 JVM 在内存中直接动态构造出来。但数组里的元素类型也就是数组去掉所有维度后的那个类仍然需要通过类加载器完成加载。比如String[]这个数组类由 JVM 直接创建而元素类型String则由启动类加载器去加载。加载阶段完成之后字节流中的数据本质上还没有真正「安家落户」。此时Class对象虽然在堆中生成了但代码还不能安全执行因为字节码可能是被篡改过的、可能是不合法的。所以 JVM 紧接着要进行下一阶段的验证。3.2 验证阶段JVM 的安全防护门验证是连接阶段的第一步它的目的是确保被加载类的字节流符合 JVM 规范不会对虚拟机自身的安全造成危害。如果把 JVM 比作一台机器那么验证阶段就是进料口的一道安检任何不合规的东西都别想进来。为什么验证如此重要因为 Java 语言本身是相对安全的编译器会阻止很多危险操作比如数组越界访问会被编译期检查、强制类型转换不合法的会编译报错。但 JVM 并不只接受 Java 编译器产生的字节码它也可能加载手工编写、甚至恶意构造的字节码。如果不做验证攻击者完全有可能构造出绕过安全检查的字节码破坏运行时的类型系统甚至直接访问 JVM 内部内存。HotSpot 虚拟机历史上就出现过不少通过字节码进行攻击的漏洞验证阶段正是抵御这类攻击的第一道防线。验证阶段的工作量很大大致可以细分为四个子阶段文件格式验证、元数据验证、字节码验证和符号引用验证。3.2.1 文件格式验证文件格式验证发生在把字节流加载到方法区之前的阶段主要由ClassFileParser这类解析器完成。它检查的是字节流的整体结构是否符合ClassFile结构的规范具体包括字节流是否以魔数0xCAFEBABE开头。每个合法的.class文件前四个字节都必须是这个魔数它就像是字节码文件的身份证前缀。主版本号和次版本号是否在当前虚拟机可接受的范围内。高版本 JDK 编译出的类在低版本 JVM 上运行时会在这里被拦下报出大家熟悉的UnsupportedClassVersionError。常量池中的常量类型是否合法常量池索引是否指向正确的常量。类文件的各个部分常量池、字段表、方法表、属性表等长度是否正确是否有被截断或多余的数据。只有通过了文件格式验证的字节流才会被允许进入方法区进行存储。后续的三个验证阶段都是在方法区上进行的不再直接操作字节流。3.2.2 元数据验证元数据验证是对字节码所描述的语义信息进行静态分析确保它们符合 Java 语言规范的要求。举例来说这个类是否有父类除了Object之外所有类都必须有父类。这个类的父类是否继承了不允许被继承的类比如final修饰的类。如果这个类不是抽象类是否实现了其父类或接口中要求实现的所有抽象方法。类中的字段和方法是否与父类产生了不允许的矛盾比如覆盖了父类的final方法、出现了不符合规则的方法重载等。元数据验证阶段的目的是对类的元数据进行语义校验保证不存在不符合 Java 语言规范的元数据信息。这个阶段报错通常是NoSuchMethodError、NoSuchFieldError等在运行期才会暴露的问题实际上早已埋下的定时炸弹。3.2.3 字节码验证字节码验证是整个验证过程中最复杂、开销最大的一个阶段它对类的方法体进行校验确保方法在运行时不会做出危害虚拟机安全的操作。这一步需要分析数据流和控制流主要检查保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作不会出现如操作数栈中放了一个int却按long来处理的情况。保证跳转指令不会跳转到方法体以外的字节码指令上。保证类型转换始终是有效的不会出现把父类对象赋值给子类引用而不做检查的情况。字节码验证是一件非常耗时的事情因此在 JDK 6 之后HotSpot 做了一项重要优化给方法体的Code属性新增了一个名为StackMapTable的属性。这个属性在编译期记录了基本块开头位置的操作数栈和局部变量表中类型状态验证时只需对照StackMapTable里的记录做一致性检查而不用去推演整个数据流。这个优化叫作「类型检查验证器」它极大缩短了验证时间。在 JDK 7 之后字节码验证已经强制要求基于StackMapTable进行这意味着如果你用非常古老的字节码生成工具生成了不含该属性的类在高版本 JVM 上可能直接验证失败。3.2.4 符号引用验证符号引用验证发生在解析阶段把符号引用转换为直接引用的时候。它检查的内容包括符号引用中通过字符串描述的全限定名能否找到对应的类。指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。符号引用中的类、字段、方法的可访问性private、protected、public、默认包访问权限是否可以被当前类访问。符号引用验证的作用是保证解析动作能够正常执行如果无法通过验证就会抛出IllegalAccessError、NoSuchFieldError、NoSuchMethodError等异常。值得一提的是验证阶段虽然重要但并不是所有类都必须经过完整验证。对于已经被验证过并被反复使用的类虚拟机可以通过缓存来跳过重复验证。同时在Server模式下也可以使用-Xverify:none参数关闭大部分验证措施以缩短启动时间不过这种做法牺牲了安全性生产环境一般不建议轻易使用。3.3 准备阶段为静态变量赋零值准备阶段是连接阶段的第二步这个阶段正式为类的静态变量分配内存并设置变量的初始值。这里要特别强调「零值」这个概念因为这是一个非常高频的面试陷阱。准备阶段给静态变量设置的是数据类型的零值而不是代码里写的那个初始值。各数据类型的零值如下表数据类型零值byte(byte) 0short(short) 0int0long0Lfloat0.0Fdouble0.0Dchar\u0000booleanfalse引用类型null比如下面这段代码javapublic class PrepareDemo { public static int value 123; public static final int CONSTANT_VALUE 456; public static String name Java; }在准备阶段value会被赋予零值0而不是123name会被赋为null而不是字符串Java。真正的123和Java要在后面的初始化阶段才会被赋予。但是这里有一个非常重要的例外如果一个静态变量被final和static同时修饰并且它的值在编译期就能确定为字面量常量那么准备阶段就会直接把它设置为代码中定义的值而不是零值。上面的CONSTANT_VALUE就属于这种情况它的值456在准备阶段就已经确定甚至会被直接内联到使用它的地方。为什么会这样因为static final常量在编译时会被记录在类的常量池中JVM 在准备阶段读取常量池时就可以直接获取它的值。但要注意如果static final修饰的字段的值不是编译期常量比如static final int RANDOM new Random().nextInt();那么它依然只能在初始化阶段被赋值准备阶段还是零值。准备阶段的内存分配主要针对静态变量。在 JDK 7 及之前类的元数据和静态变量存放在永久代JDK 8 移除永久代后类的元数据进入元空间Metaspace但静态变量并没有随之搬走而是仍然存放在堆中具体来说是存放在这个类所对应的Class对象里。换句话说静态变量在运行时更像Class对象的实例字段这也是反射能够直接读取到它们的原因。另外需要强调的是准备阶段只处理被static修饰的类变量。实例变量不会在这一阶段分配它们要等到对象真正被new出来、执行构造方法时才会随对象一起在堆中分配内存并初始化。3.4 解析阶段把符号引用变为直接引用解析是连接阶段的第三个环节它的核心任务是把常量池内的符号引用替换为直接引用。所谓符号引用就是用一组符号来描述所引用的目标比如类的全限定名、字段的名称和描述符、方法的名称和签名。它与内存布局无关只负责在字面上告诉虚拟机「我要引用谁」。而直接引用则可以理解为能够直接定位到目标的指针、相对偏移量或者方法区的句柄它与虚拟机运行时的内存布局强相关拿到它就能立刻找到目标。解析动作主要针对以下四类符号引用展开类或接口的解析根据全限定名找到对应的类或接口。字段解析定位到字段在其类中的具体偏移量如果解析失败会抛出NoSuchFieldError。类方法解析也就是静态方法解析为某个类中确定的方法入口。接口方法解析解析接口中定义的方法运行期配合多态完成分派。解析与初始化之间没有严格的先后顺序。虚拟机可以在类被初始化之前就完成某些符号引用的解析这种称为静态解析也可以把解析推迟到实际调用发生时再执行称为晚期解析。正是这种晚期解析机制支撑了 Java 语言的重载、重写与运行期多态。例如invokevirtual指令在真正分派方法时才会根据接收者的实际类型去解析具体的方法版本这也是动态绑定的体现。解析失败通常意味着程序结构存在不一致常见的错误包括NoClassDefFoundError、NoSuchMethodError、IllegalAccessError它们往往不是编译器能提前发现的而是类版本不匹配、依赖冲突等问题的表现。3.5 初始化阶段真正执行 Java 代码的时刻初始化是类加载流程的最后一个关键阶段也是开发者最熟悉的阶段因为到了这一步类的静态变量赋值、static代码块才会真正执行。在初始化阶段虚拟机会为类生成一个特殊的类构造器方法clinit()。这个方法不是我们手写出来的而是由编译器自动收集类中所有静态变量的赋值动作和所有static代码块中的语句按它们在源码中出现的顺序合并而成的。clinit()只在类第一次被主动初始化时执行一次并且 JVM 会保证它在多线程环境下被正确加锁和同步也就是说如果多个线程同时触发同一个类的初始化只有一个线程会真正执行clinit()其余线程会被阻塞直到初始化完成。举一个典型例子javapublic class InitDemo { static { value 10; // System.out.println(value); 这里虽然可以赋值但不能访问 } private static int value 20; public static void main(String[] args) { System.out.println(value); // 输出 20 } }这段代码最终输出 20因为静态变量的赋值动作和静态代码块会按顺序合并进clinit()先执行代码块里的value 10再执行后面的显式初始化value 20。同时要注意静态代码块只能访问出现在它之前的静态变量对出现在它之后的静态变量只能赋值、不能读取否则会触发「非法前向引用」的编译错误。那么什么样的操作会触发一个类的初始化规范规定有且只有六种「主动引用」会立即触发初始化使用new创建对象实例或者读写一个类型的静态字段getstatic、putstatic或者调用一个类型的静态方法invokestatic。但访问编译期常量除外。使用java.lang.reflect包的方法对类型进行反射调用时如果类还未初始化则先触发初始化。当初始化一个类时发现它的父类还没有初始化则需要先初始化它的父类。虚拟机启动时加载的主类也就是包含main方法的类会最先被初始化。JDK 7 引入动态语言支持后如果MethodHandle解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic等静态句柄则需要先初始化对应类。如果一个接口定义了default方法那么其实现类初始化时该接口需要先被初始化。与之相对的是「被动引用」它不会触发类的初始化。下面三种情况就是典型javapublic class PassiveReferenceDemo { public static void main(String[] args) { // 1. 通过子类引用父类的静态字段只会初始化父类不会初始化子类 System.out.println(SubClass.VALUE); // 2. 通过数组定义来引用一个类不会触发该类的初始化 Parent[] arr new Parent[3]; // 3. 访问编译期常量不会触发定义该常量的类初始化 System.out.println(ConstClass.HELLO); } }四、类加载器体系谁负责把类搬进来前面讲的是「类被加载时经历了什么」接下来要回答「类到底由谁加载」。JVM 中真正负责把字节码加载进内存的角色就是类加载器ClassLoader。从逻辑上看类加载器可以分为四个层次启动类加载器Bootstrap ClassLoader由 C 实现是 JVM 的一部分负责加载JAVA_HOME/lib目录或者-Xbootclasspath参数指定的核心类库例如java.lang、java.util等。在 Java 代码中它的引用通常返回null因为它是本地实现没有对应的 Java 对象。扩展类加载器Extension ClassLoader由 Java 实现继承自ClassLoader负责加载JAVA_HOME/lib/ext目录或者java.ext.dirs指定的扩展类库。JDK 9 引入模块化后它被平台类加载器Platform ClassLoader取代。应用程序类加载器Application ClassLoader也称为系统类加载器负责加载应用 classpath 下的类是ClassLoader.getSystemClassLoader()的返回值也是绝大多数用户代码实际使用的默认加载器。自定义类加载器Custom ClassLoader由开发者继承ClassLoader自行实现用于从数据库、网络、加密文件等非常规来源加载类。这些类加载器并不是彼此孤立而是通过「双亲」关系构成了一条自上而下的链。可以用下面这段代码直观地观察javapublic class ClassLoaderDemo { public static void main(String[] args) { ClassLoader appClassLoader ClassLoader.getSystemClassLoader(); System.out.println(应用程序类加载器 appClassLoader); ClassLoader extClassLoader appClassLoader.getParent(); System.out.println(扩展类加载器 extClassLoader); ClassLoader bootstrapClassLoader extClassLoader.getParent(); System.out.println(启动类加载器 bootstrapClassLoader); System.out.println(String 的类加载器 String.class.getClassLoader()); } }在 JDK 8 中这段代码的输出大致是三类非空加载器而String的加载器和最顶层的父加载器都会打印null这正是因为核心类由启动类加载器加载它在 Java 层没有对象形态。五、双亲委派模型类加载的默认秩序双亲委派模型又称父委托模型指类加载器在尝试自己加载一个类之前先把请求委派给父类加载器去完成。对每个类加载器来说真正的逻辑是「先问爸爸爸爸搞不定我再上」。它对应的工作流程可以分为三步首先检查目标类是否已经加载过如果加载过就直接返回否则把加载请求委派给父类加载器只有当父类加载器反馈无法完成加载时当前加载器才尝试自己加载。JDK 中这一套逻辑就实现在java.lang.ClassLoader#loadClass方法里javaprotected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class? c findLoadedClass(name); if (c null) { long t0 System.nanoTime(); try { if (parent ! null) { c parent.loadClass(name, false); } else { c findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父类加载器无法完成加载交由当前加载器处理 } if (c null) { long t1 System.nanoTime(); c findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }沿着父链逐层向上所有加载请求最终都会到达启动类加载器。只有启动类加载器也找不到目标类时请求才会一层层返回让下一级加载器尝试加载。这样的设计至少带来三个直接好处避免类的重复加载类一旦被上层加载器加载就不会再被下层重复加载保证了同一个类在 JVM 中有唯一身份。保护核心库不被篡改无论应用如何自定义类加载器java.lang.String这类核心类都会优先交给启动类加载器加载防止恶意代码用自己的同名类替换 JDK 核心类。保证类型一致性同一个类加载器加载的两个同名类才能正常赋值与比较双亲委派让依赖关系更加稳定减少了ClassCastException类冲突问题的出现。六、打破双亲委派的现实场景双亲委派是默认的、好用的秩序但它并不是强制性的。现实中有一部分场景恰恰需要通过打破这个模型才能正常工作。6.1 JDBC 与线程上下文类加载器JDBC 是双亲委派失效最常见的一个例子。DriverManager位于 JDK 核心包内由启动类加载器加载而具体数据库驱动例如 MySQL 的com.mysql.cj.jdbc.Driver则由我们放进 classpath 中的 JAR 包提供由应用程序类加载器加载。按照双亲委派的逻辑启动类加载器根本无法「向下」看到由应用程序类加载器加载出来的驱动实现类。Java 给出的解法是线程上下文类加载器Thread Context ClassLoader。DriverManager在初始化时会调用ServiceLoader.load(Driver.class)后者使用当前线程的上下文类加载器来加载驱动实现相当于让父级加载器反过来向子级加载器借力。线程上下文类加载器默认指向应用程序类加载器也可以由开发者手动设置这一机制也被 Spring、MyBatis 等框架广泛使用。6.2 Tomcat 的 Web 应用类加载Tomcat 需要在一个 JVM 里同时运行多个 Web 应用而不同应用可能依赖同一个库的不同版本。如果完全遵守双亲委派所有类都从同一个上层加载版本冲突几乎是必然的。因此 Tomcat 为每个 Web 应用都创建了独立的WebappClassLoader并调整了加载顺序先尝试从当前 Web 应用自己的WEB-INF/classes、WEB-INF/lib中加载类找不到再委派给父加载器。通过这种「儿子优先」的策略不同应用可以加载各自的类版本实现应用隔离与热部署。当然像java.lang这类核心类仍然遵循双亲委派不会被 Web 应用覆盖。6.3 OSGi 与热部署OSGi 是另一个典型的破局者。它把每一个模块称为 Bundle每个 Bundle 拥有独立的类加载器模块之间通过Import-Package和Export-Package声明依赖关系形成一个网状结构而不是简单的父子链。这种设计使得不同的 Bundle 可以同时存在不同版本的同一个类也可以在不重启系统的情况下独立安装、卸载和更新模块。代价是类加载关系变得非常复杂一旦依赖声明不当很容易陷入「Class 找不到」的泥潭。热部署与字节码增强类框架本质上也都是通过自定义类加载器绕开默认模型以达到运行时替换类的目的。七、实战手写一个自定义类加载器理解概念之后做一个最小可运行的自定义类加载器会有助于加深印象。下面是一个从指定目录加载.class文件的实现javapublic class CustomClassLoader extends ClassLoader { private String classPath; public CustomClassLoader(String classPath) { this.classPath classPath; } Override protected Class? findClass(String name) throws ClassNotFoundException { byte[] data loadClassData(name); if (data null) { throw new ClassNotFoundException(name); } return defineClass(name, data, 0, data.length); } private byte[] loadClassData(String name) { String path classPath name.replace(., /) .class; try (InputStream in new FileInputStream(path); ByteArrayOutputStream out new ByteArrayOutputStream()) { byte[] buffer new byte[4096]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } return out.toByteArray(); } catch (IOException e) { return null; } } }使用时只要把编译好的Demo.class放到自定义目录再通过反射调用即可javapublic class CustomLoaderTest { public static void main(String[] args) throws Exception { CustomClassLoader loader new CustomClassLoader(D:/myclasses/); Class? clazz loader.loadClass(com.example.Demo); Object obj clazz.newInstance(); clazz.getMethod(sayHello).invoke(obj); } }这里的关键是findClass内的defineClass方法它会把字节数组真正转换成 JVM 中的Class对象。如果我们的需求是「隔离同名类的不同版本」还可以进一步重写loadClass先自己加载、失败后再交给父加载器从而打破双亲委派。八、高频面试问答Q1类加载的过程可以分为哪几个阶段答加载、验证、准备、解析、初始化五个核心阶段外加使用和卸载。其中验证、准备、解析合称连接阶段。Q2双亲委派机制的原理是什么为什么要这么设计答类加载器收到加载请求时先委派给父加载器父加载器无法完成时才自己加载。这样能避免类重复加载、保护核心类不被篡改、保证类的唯一性和类型一致性。Q3什么是主动引用和被动引用答主动引用指会触发类初始化的六种情况包括 new 对象、访问静态字段或方法、反射、初始化子类时先初始化父类、启动主类、MethodHandle 静态句柄和接口 default 方法被动引用则不会触发初始化比如通过子类引用父类静态字段、创建数组、访问编译期常量。Q4准备阶段和初始化阶段对静态变量的处理有什么不同答准备阶段为静态变量分配内存并赋零值但static final且编译期可确定值的常量会直接赋真实值初始化阶段才执行静态变量的显式赋值和静态代码块。Q5有哪些场景会打破双亲委派答JDBC 通过线程上下文类加载器让父加载器使用子加载器加载的驱动Tomcat 为每个 Web 应用使用独立的类加载器实现应用隔离OSGi 通过 Bundle 间的网状依赖实现模块热部署自定义类加载器也可以通过重写loadClass实现逆序加载。Q6如何判断两个类是否是同一个类答需要同时满足「类的全限定名相同」和「由同一个类加载器加载」两个条件。不同类加载器加载的同名类在 JVM 中属于不同的类互相赋值会抛出ClassCastException。Q7什么是线程上下文类加载器答它保存在Thread.currentThread().getContextClassLoader()中默认是应用程序类加载器可以手动设置。它用于让父加载器能够反向使用子加载器加载的类是打破双亲委派的常见实现方式。九、总结类加载机制是 JVM 中连接字节码与运行时的桥梁。把本文内容提炼成几句话类加载的五个核心阶段是加载、验证、准备、解析、初始化验证、准备、解析合称连接。加载阶段负责把字节码读进内存并生成Class对象来源可以是磁盘、网络、动态生成等。验证阶段分为文件格式、元数据、字节码、符号引用四个子步骤保护 JVM 不被非法字节码攻击。准备阶段为静态变量赋零值static final编译期常量是例外。初始化阶段执行clinit()由静态变量赋值和静态代码块按源码顺序合并而成。类加载器分为启动、扩展、应用和自定义四类通过双亲委派保证类的一致性和核心库安全。线程上下文类加载器、Tomcat、OSGi 和自定义类加载器是打破双亲委派的典型场景。
返回列表