JVM 常用的垃圾收集器分年轻代和老年代,核心有 6 种,工作原理各有侧重。年轻代常用 3 种:Serial GC(串行收集),单线程回收,简单高效,适合小内存场景,回收时会暂停所有用户线程;ParNew GC,Serial 的多线程版本,多线程回收年轻代,减少停顿时间;Parallel Scavenge GC,多线程回收,注重吞吐量(用户代码运行时间 / 总时间),适合后台计算。老年代常用 2 种:Serial Old GC,Serial 的老年代版本,单线程标记 - 整理回收;CMS GC,并发标记清除,多线程并发回收,停顿时间短,适合对响应时间敏感的场景,但会产生内存碎片。还有 G1 GC,跨年轻代和老年代,将堆分成多个区域,优先回收垃圾多的区域,兼顾吞吐量和响应时间,是 JDK9 后的默认收集器。
JVM 主要有 4 类类加载器,按层级从高到低排序,各自分工明确。第一,启动类加载器(Bootstrap ClassLoader),最顶层,由 C++ 实现,负责加载 JDK 核心类(比如 rt.jar 里的类),比如 String、Object 类,我们无法在 Java 代码中直接获取它。第二,扩展类加载器(Extension ClassLoader),加载 JDK 扩展目录(jre/lib/ext)下的类,负责补充核心类之外的扩展功能。第三,应用程序类加载器(Application ClassLoader),也叫系统类加载器,负责加载我们自己写的代码(classpath 下的类),是我们日常开发中最常用的加载器。第四,自定义类加载器,继承 ClassLoader 类实现,可根据需求自定义加载规则,比如加载加密的 class 文件、从网络加载类,常用于框架(如 Spring)和自定义场景。它们之间遵循双亲委派模型,先让父加载器加载,父加载器加载不了再自己加载。
JVM 核心垃圾回收算法有 4 种,分别适配不同内存区域和场景,没有最优,只有最适合。第一,复制算法,主要用在年轻代,年轻代对象存活率低,原理是把年轻代分成 Eden 区和两个 Survivor 区,每次只用 Eden 和一个 Survivor,回收时把存活对象复制到另一个 Survivor,清空用过的区域,效率高、无内存碎片,但浪费部分内存。第二,标记 - 清除算法,用在老年代,先标记存活对象,再直接清除未标记的垃圾,优点是不用移动对象,缺点是产生内存碎片,影响后续对象分配。第三,标记 - 整理算法,也是老年代常用,标记存活对象后,把存活对象往一端移动,再清空另一端垃圾,解决标记 - 清除的碎片问题,但需要移动对象,效率稍低。第四,分代收集算法,不是单独算法,是结合前三种,年轻代用复制,老年代用标记 - 清除 / 整理,是目前 JVM 默认的收集方式。
JVM 判断对象死亡,核心有两种方法,实际中主要用第二种。第一种,引用计数法,简单粗暴:给每个对象加一个引用计数器,有对象引用它,计数器加 1;引用消失,计数器减 1;计数器为 0,就认为对象死亡。但它有个致命缺点,无法解决循环引用(比如 A 引用 B,B 引用 A,两者都没有其他引用,计数器都是 1,无法被回收),所以 JVM 不用这种。第二种,可达性分析算法,这是 JVM 实际使用的方法。原理是从 GC Roots(核心引用,比如主线程、静态变量、本地方法栈引用的对象)出发,遍历对象引用链,能被遍历到的对象是存活的,遍历不到的就是“不可达”,判定为死亡。GC Roots 包括虚拟机栈中引用的对象、方法区中静态变量和常量引用的对象、本地方法栈中 JNI 引用的对象,能有效解决循环引用问题。
JVM 有 4 种引用类型,从强到弱依次是:强引用、软引用、弱引用、虚引用,作用和回收时机完全不同。第一,强引用,最常用的,比如 new Object() 创建的对象,只要有强引用存在,JVM 绝对不会回收它,即使内存溢出也不回收,这也是内存泄漏的常见原因。第二,软引用(SoftReference),强度仅次于强引用,内存充足时不回收,内存不足(即将 OOM)时,会回收软引用指向的对象,常用于缓存(比如图片缓存),避免内存溢出。第三,弱引用(WeakReference),强度比软引用弱,只要发生 GC,不管内存是否充足,都会回收弱引用指向的对象,比如 WeakHashMap,key 是弱引用,无其他引用时会被 GC 回收。第四,虚引用(PhantomReference),最弱的引用,不能通过它获取对象,唯一作用是:对象被回收时,会收到一个系统通知,常用于跟踪对象回收时机,几乎不用在日常开发中。
两者都是 JVM 中存储类元信息(类结构、方法信息、常量等)的区域,核心区别是 JDK 版本和内存来源,PermGen 已被 Metaspace 替代。第一,版本差异:PermGen 是 JDK1.7 及之前的,Metaspace 是 JDK1.8 及之后的,用来替代 PermGen。第二,内存来源:PermGen 是堆的一部分,内存大小固定(可通过 -XX:PermSize/-XX:MaxPermSize 设置),容易出现 PermGen OOM;Metaspace 不在堆中,而是使用本地内存(物理内存),内存大小默认无上限(可通过 -XX:MetaspaceSize/-XX:MaxMetaspaceSize 设置),大幅减少 OOM 概率。第三,存储内容:PermGen 存储类元信息、字符串常量池、静态变量;Metaspace 只存储类元信息,字符串常量池被移到了堆中,静态变量移到了堆的普通对象区。第四,回收机制:两者都可被 GC 回收,但 Metaspace 的回收效率更高,更灵活。
堆和栈是 JVM 中最核心的两个内存区域,用途、生命周期、存储内容完全不同,简单说“堆存对象,栈存方法执行信息”。第一,存储内容:堆主要存储对象实例(new 出来的对象)和数组,还有 JDK1.8 后的字符串常量池;栈(虚拟机栈)主要存储方法执行时的栈帧,每个栈帧包含局部变量表、操作数栈、方法出口等,每个方法执行时创建一个栈帧,执行完销毁。第二,生命周期:堆是 JVM 启动时创建,整个 JVM 运行期间都存在,垃圾回收主要回收堆内存;栈是线程私有,每个线程有自己的栈,线程结束,栈就销毁,栈帧随方法执行创建、结束销毁。第三,内存分配:堆内存可动态分配,大小可通过 JVM 参数调整;栈内存是固定大小(可调整),每个栈帧的大小也是固定的。第四,线程共享:堆是所有线程共享的,存在线程安全问题;栈是线程私有,不存在线程安全问题。
Full GC 是对老年代和整个堆进行垃圾回收,会暂停所有用户线程(STW),影响程序响应,常见触发原因有 5 种。第一,老年代内存不足,这是最常见的,当对象从年轻代晋升到老年代,老年代剩余内存放不下,就会触发 Full GC。第二,永久代(JDK1.7 及之前)/ 元空间(JDK1.8 及之后)内存不足,类元信息、常量过多,导致区域满,触发 Full GC(元空间不足时,部分版本会触发 Full GC)。第三,手动调用 System.gc(),虽然只是建议 JVM 执行 GC,但大部分情况下会触发 Full GC,日常开发不建议手动调用。第四,年轻代 Minor GC 后,存活对象太多,无法放入 Survivor 区,只能直接晋升老年代,导致老年代内存骤增,触发 Full GC。第五,堆内存分配不合理,比如老年代设置太小,频繁有对象晋升,反复触发 Full GC。
双亲委派模型是 JVM 类加载器的核心加载机制,核心原则是“先找父加载器,父加载不了再自己加载”,目的是保证类加载的安全性和唯一性。具体工作流程:当一个类加载器收到类加载请求时,不会先自己加载,而是先把请求委托给它的父加载器;父加载器收到请求后,再委托给它的父加载器,直到传递到最顶层的启动类加载器;如果父加载器能加载这个类,就直接加载,返回 Class 对象;如果所有父加载器都加载不了(比如不是核心类、不是扩展类),才会由当前类加载器自己去加载。比如我们自己写的 User 类,应用程序类加载器会先委托给扩展类加载器,再委托给启动类加载器,两者都加载不了,才由应用程序类加载器加载,这样能避免核心类被篡改,保证同一个类只被加载一次。
JVM 优化核心是减少 GC 频率、缩短 GC 停顿时间、避免内存溢出,常见手段有 6 种。第一,调整 JVM 内存参数,合理设置堆大小(-Xms 初始堆、-Xmx 最大堆),年轻代和老年代比例(默认 1:2),根据程序类型调整(比如后台程序侧重吞吐量,设置大年轻代;接口程序侧重响应时间,设置合理老年代)。第二,选择合适的垃圾收集器,比如后台计算用 Parallel Scavenge,接口服务用 CMS 或 G1,减少 STW 时间。第三,优化代码,减少对象创建(比如复用对象、用对象池),避免内存泄漏(及时关闭资源、清理静态集合),减少大对象创建(避免直接进入老年代)。第四,优化类加载,减少类的数量,避免频繁加载卸载类。第五,使用监控工具(JVisualVM、JProfiler)排查问题,找到 GC 频繁、内存泄漏的原因。第六,调整元空间、直接内存大小,避免其不足触发 Full GC。
JIT 编译器就是即时编译器,核心作用是把 Java 字节码(解释执行的代码)编译成机器码,提高程序运行效率,JVM 默认开启。Java 代码运行有两种方式:解释执行(字节码一行一行翻译成机器码,启动快、执行慢)和编译执行(把热点代码编译成机器码,启动慢、执行快)。JIT 编译器的工作原理:程序运行时,JVM 会统计代码的执行频率,把频繁执行的代码(热点代码,比如循环、常用方法)标记出来;然后 JIT 编译器把这些热点代码编译成机器码,存储起来;后续再执行这些代码时,直接执行机器码,不用再解释,大幅提高执行速度。JIT 编译器分两种:Client Compiler(C1),编译速度快,优化简单,适合桌面应用;Server Compiler(C2),编译速度慢,优化彻底(比如循环展开、常量折叠),适合服务器应用,JDK 默认用 C2。
JVM 类加载过程主要分 5 个阶段,顺序固定,缺一不可:加载、验证、准备、解析、初始化,只有初始化阶段会执行代码,其他阶段都是 JVM 自动完成。第一,加载:找到 class 文件(从磁盘、网络等),把字节码读入内存,创建一个 Class 对象,是类加载的第一步。第二,验证:检查 class 文件的字节码是否合法,避免恶意字节码攻击,比如检查文件格式、语法正确性、类型兼容性,是安全保障。第三,准备:给类的静态变量分配内存,设置默认初始值(比如 int 默认 0,String 默认 null),注意此时不会执行静态代码块,只是分配内存。第四,解析:把字节码中的符号引用(比如类名、方法名)转成直接引用(内存地址),让 JVM 能直接找到对应的类和方法。第五,初始化:执行静态代码块、给静态变量赋值,只有当类被主动使用(创建实例、调用静态方法、访问静态变量)时,才会触发这个阶段,是类加载的最后一步。
happens-before 原则是 JMM 的核心,目的是解决多线程下内存可见性问题,定义了“哪些操作的执行结果,对其他操作是可见的”,不用通过同步机制就能保证有序性和可见性。简单说,只要 A 操作 happens-before B 操作,就意味着 A 操作的结果,B 操作一定能看到,不管 A 和 B 是否在同一个线程。常见的 happens-before 规则有 6 种,好记又常用:1. 程序顺序规则:同一个线程中,先写的代码 happens-before 后写的代码;2. 监视器锁规则:解锁操作 happens-before 后续的加锁操作;3. volatile 规则:volatile 变量的写操作 happens-before 后续的读操作;4. 线程启动规则:Thread.start()操作 happens-before 线程内的所有操作;5. 线程终止规则:线程内的所有操作 happens-before 线程终止检测(比如 Thread.join());6. 传递性:如果 A happens-before B,B happens-before C,那么 A happens-before C。
Finalize 方法是 Object 类的方法,作用是对象被 GC 回收前,执行一些资源释放操作(比如关闭流),但它有很多问题,日常开发不建议使用。第一,执行时机不确定,JVM 不保证 Finalize 方法一定会执行,也不保证执行顺序,比如对象被标记为垃圾后,Finalize 可能延迟很久才执行,甚至不执行,导致资源无法及时释放。第二,会影响 GC 效率,Finalize 方法会让对象暂时“复活”(比如在方法中给对象添加强引用),导致 GC 需要多次标记回收,增加 GC 负担。第三,可能导致内存泄漏,比如 Finalize 方法中出现异常,JVM 不会捕获,会直接终止 Finalize 的执行,但对象不会被回收,长期堆积导致内存泄漏。第四,性能差,Finalize 方法的执行由 GC 线程负责,会占用 GC 资源,影响程序运行效率,现在已被 try-with-resources 替代,用来释放资源更安全、高效。
逃逸分析是 JVM 的一种优化技术,核心是分析对象的引用范围,判断对象是否“逃逸”(即是否被当前方法外部引用),如果没有逃逸,就可以进行优化,提高效率。工作原理很简单:JVM 在编译阶段,分析对象的创建和引用路径,判断对象是否只在当前方法内使用,没有被外部方法引用、没有被静态变量引用、没有作为返回值返回(这些都是逃逸行为)。如果判断对象没有逃逸,JVM 会做两个核心优化:1. 栈上分配:把对象分配到栈上,而不是堆上,线程结束后栈帧销毁,对象自动回收,不用 GC,减少 GC 压力;2. 标量替换:把对象拆分成基本类型(比如 User 对象拆分成 name、age),直接分配到局部变量表,避免创建对象,节省内存。逃逸分析默认开启(JDK1.8 及之后),能有效提升程序性能,尤其适合频繁创建短生命周期对象的场景。
内存泄漏是对象无用但无法被 GC 回收,长期堆积导致 OOM,诊断和解决分 3 步。第一步,诊断内存泄漏:用监控工具(JVisualVM、JProfiler)查看堆内存使用情况,比如堆内存持续增长,Full GC 后内存不下降,就大概率有内存泄漏;然后 dump 堆内存快照(.hprof 文件),分析快照,找到哪些对象数量异常多、没有被回收,以及它们的引用链。第二步,定位泄漏原因:常见泄漏场景有静态集合持有对象(比如 static List)、忘记关闭资源(流、数据库连接)、监听器未移除、线程池核心线程持有对象引用,通过引用链找到持有这些对象的地方。第三步,解决泄漏问题:针对性修改代码,比如用完静态集合后及时 clear()、用 try-with-resources 关闭资源、移除无用监听器、合理设置线程池参数,避免核心线程长期持有对象;修改后再用工具监控,确认泄漏已解决。
栈溢出是虚拟机栈内存不足导致的,核心原因是方法调用层级太深,或者栈内存设置太小。产生原因:虚拟机栈是线程私有,每个方法调用会创建一个栈帧,压入栈中,方法执行完弹出;如果方法递归调用(比如无限递归),或者调用层级极深(比如递归几千次),栈帧会不断堆积,超过栈内存的最大容量,就会抛出 StackOverflowError。比如写一个无终止条件的递归方法,执行后很快就会栈溢出。避免方法:第一,调整栈内存大小,通过 -XX:Xss 参数(比如 -XX:Xss1m)增大栈内存,适合调用层级确实较深的场景。第二,避免无限递归,给递归方法添加终止条件,控制递归层级(比如递归不超过 1000 次)。第三,优化代码,把深层递归改成循环,减少方法调用层级,比如用迭代替代递归,从根本上减少栈帧的创建。
JVM 内存分配主要针对堆内存(对象主要分配在这里),回收策略对应不同的内存区域,核心遵循“分代分配、分代回收”。分配策略:1. 优先在年轻代 Eden 区分配,大部分对象创建后先放 Eden 区,Eden 区满了触发 Minor GC(年轻代回收)。2. 大对象直接进入老年代,比如超大数组、大对象,避免在年轻代频繁复制,通过 -XX:PretenureSizeThreshold 参数设置大对象阈值。3. 长期存活的对象晋升老年代,年轻代对象每经历一次 Minor GC 存活下来,年龄加 1,达到阈值(默认 15)就晋升老年代,通过 -XX:MaxTenuringThreshold 设置阈值。回收策略:1. 年轻代用复制算法,Minor GC 只回收年轻代,STW 时间短。2. 老年代用标记 - 清除 / 标记 - 整理算法,Full GC 回收老年代 + 年轻代,STW 时间长。3. 元空间 / 永久代回收,当类元信息过多时,触发回收(元空间回收效率更高)。4. 逃逸分析优化,无逃逸对象分配到栈上,不用 GC。
直接内存不是 JVM 堆内存的一部分,而是直接使用操作系统的物理内存,也叫堆外内存,由 Java 代码通过 NIO 的 ByteBuffer.allocateDirect()创建,不受 JVM 堆内存大小限制,但受物理内存大小限制。它的核心作用是提高 IO 效率,传统 IO 是“Java 堆内存→直接内存→磁盘 / 网络”,而直接内存可以直接和磁盘 / 网络交互,减少一次内存拷贝,适合高并发 IO 场景(比如 NIO 服务器)。特点:1. 分配和释放效率比堆内存低,因为需要调用操作系统接口。2. 不受 JVM 垃圾回收管理,JVM 不会自动回收直接内存,需要手动调用 ByteBuffer 的 cleaner() 方法释放,或者等待 JVM 触发 Full GC 时间接回收。3. 容易出现直接内存溢出(OutOfMemoryError: Direct buffer memory),比如频繁创建直接内存对象,不及时释放,导致物理内存不足。
JIT 编译器和解释器都是 JVM 执行 Java 代码的方式,核心区别是执行速度、启动速度和优化程度,JVM 采用“解释执行 + 编译执行”的混合模式。解释器工作机制:把 Java 字节码一行一行翻译成机器码,逐行执行,不用提前编译,启动速度快,但是执行速度慢,适合程序启动初期(代码执行次数少),或者非热点代码。JIT 编译器工作机制:程序运行时,统计热点代码(频繁执行的代码),把热点代码一次性编译成机器码,存储起来,后续执行时直接调用机器码,不用再逐行解释,执行速度快,但编译需要时间,启动速度慢,适合程序运行后期(热点代码稳定),或者热点代码。两者配合:程序启动时,解释器先执行,保证快速启动;运行过程中,JIT 编译热点代码,提升执行效率;非热点代码继续用解释器执行,兼顾启动速度和运行效率。
两者都是 JVM 为了安全执行 GC(避免 GC 时线程正在操作对象,导致对象引用混乱)设计的,核心是让线程在 GC 时进入“安全状态”。安全点(Safepoint):是线程执行过程中的一些特定位置(比如方法调用、循环结束、异常抛出),线程执行到这些位置时,会暂停下来,等待 GC 完成。JVM 触发 GC 时,会通知所有线程到达最近的安全点后暂停,这样 GC 就能安全回收垃圾,不会出现对象引用不一致的问题。安全区域(Safe Region):是针对线程处于休眠状态(比如 Thread.sleep()、wait())的场景,此时线程无法到达安全点,就需要安全区域。线程进入休眠前,会标记自己处于安全区域,GC 时发现线程在安全区域,就可以直接回收垃圾,不用等待线程;线程唤醒后,会先检查 GC 是否完成,完成后再继续执行。两者互补,确保 GC 能安全、高效执行。
方法区和运行时常量池是 JVM 规范中的概念,两者是包含关系,运行时常量池是方法区的一部分。方法区:是 JVM 用来存储类元信息的区域,比如类的结构、方法信息、字段信息、静态变量、常量等,JDK1.7 及之前,方法区对应永久代(PermGen);JDK1.8 及之后,方法区对应元空间(Metaspace),只是实现方式不同,功能不变。运行时常量池:是方法区的一部分,存储类的常量信息,比如字符串常量、数字常量、符号引用(类名、方法名)等。它是从 Class 文件的常量池加载过来的,Class 文件中的常量池是静态的,加载到 JVM 后,就变成运行时常量池,运行时可以动态添加常量(比如 String.intern() 方法,把字符串添加到常量池)。注意:JDK1.8 后,字符串常量池被移到了堆中,运行时常量池只保留符号引用等信息。
类卸载就是 JVM 将已经加载的类,从方法区(元空间 / 永久代)中移除,释放内存,类卸载的条件很严格,不是所有加载的类都会被卸载。类卸载的核心条件(必须同时满足):1. 该类的所有实例都已经被 GC 回收,没有任何对象引用该类。2. 加载该类的类加载器已经被 GC 回收,比如自定义类加载器,没有任何引用指向它。3. 该类的 Class 对象没有被任何地方引用,比如没有通过 Class.forName() 获取该类的 Class 对象,没有反射引用该类。满足这三个条件后,JVM 在执行 GC 时,会将该类的类元信息从方法区中卸载。注意:启动类加载器加载的核心类(比如 String、Object),永远不会被卸载,因为它们会被 JVM 一直引用,无法满足卸载条件;扩展类加载器和应用程序类加载器加载的类,满足条件后可以被卸载。
两者都是 JVM 判断对象是否死亡的核心算法,用途和优缺点不同,实际中只使用可达性分析算法。引用计数法:原理简单,给每个对象分配一个引用计数器,有对象引用它,计数器加 1;引用消失,计数器减 1;计数器为 0,判定对象死亡。优点是实现简单、效率高,缺点是无法解决循环引用问题(比如 A 引用 B,B 引用 A,两者无其他引用,计数器都是 1,无法回收),所以 JVM 不使用这种算法。可达性分析算法:JVM 实际使用的算法,原理是从 GC Roots(核心引用,比如主线程、静态变量、本地方法栈引用的对象)出发,遍历对象引用链,能被遍历到的对象是存活的,遍历不到的是不可达对象,判定为死亡。优点是能解决循环引用问题,判断准确,缺点是实现复杂、效率比引用计数法低,但能保证 GC 的正确性,是 JVM 的首选。
JVM 监控和调优工具主要分两类:命令行工具(轻量、便捷)和图形化工具(直观、功能强),日常开发常用以下几种。第一,命令行工具:1. jps:查看正在运行的 JVM 进程,获取进程 ID;2. jstat:监控 JVM 的 GC 情况、类加载情况,比如查看年轻代、老年代的内存使用、GC 次数和时间;3. jmap:生成堆内存快照,查看堆内存使用情况,排查内存泄漏;4. jstack:查看线程堆栈信息,排查死锁、线程阻塞问题;5. jinfo:查看和修改 JVM 的运行参数,比如查看堆大小、GC 收集器类型。第二,图形化工具:1. JVisualVM:JDK 自带,功能全面,可监控 GC、查看堆快照、分析线程、生成火焰图,适合日常排查;2. JProfiler:功能更强大,可精准分析内存泄漏、CPU 占用、方法执行耗时,适合复杂项目调优;3. Arthas:阿里开源,命令行 + 图形化结合,可在线排查问题,不用重启应用,适合生产环境。
JVM 类加载机制有 4 个核心特点,决定了类加载的安全性和灵活性。第一,双亲委派模型,这是最核心的特点,类加载时先委托父加载器加载,父加载不了再自己加载,保证核心类不被篡改,同一个类只被加载一次。第二,懒加载(延迟加载),类不会在 JVM 启动时就全部加载,而是在被主动使用时才加载(比如创建实例、调用静态方法),节省内存,提高启动速度。第三,独立性,每个类加载器加载的类是独立的,即使是同一个 class 文件,被不同类加载器加载,也会成为两个不同的 Class 对象,互不影响。第四,不可变性,类一旦被加载、初始化完成,就不能再修改其类元信息(比如方法、字段),保证类的安全性和稳定性,避免运行时被篡改。
Minor GC 和 Full GC 是 JVM 中两种核心 GC 类型,触发场景完全不同,影响也不同。Minor GC(年轻代 GC):只回收年轻代(Eden 区 +Survivor 区),STW 时间短,频率高。触发情况:1. 最常见:Eden 区内存不足,当创建对象时,Eden 区没有足够空间分配,就会触发 Minor GC,回收 Eden 区和用过的 Survivor 区的垃圾,存活对象复制到另一个 Survivor 区。2. 手动调用 System.gc(),可能触发 Minor GC(但不推荐)。Full GC(全局 GC):回收老年代 + 年轻代 + 元空间 / 永久代,STW 时间长,频率低,应尽量避免。触发情况:1. 老年代内存不足,对象从年轻代晋升到老年代,老年代剩余空间放不下;2. 元空间(JDK1.8+)/ 永久代(JDK1.7 及之前)内存不足;3. Minor GC 后,存活对象太多,无法放入 Survivor 区,直接晋升老年代,导致老年代内存骤增,触发 Full GC;4. 手动调用 System.gc(),大概率触发 Full GC;5. JVM 调优参数设置不合理,比如老年代太小,频繁触发 Full GC。
JVM 中 4 类类加载器(启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器)之间,是“父子关系”(逻辑上的,不是继承关系),遵循双亲委派模型,层级分明。具体关系:1. 启动类加载器是最顶层的“父加载器”,没有父加载器,它是扩展类加载器的父加载器(逻辑上)。2. 扩展类加载器的父加载器是启动类加载器,它是应用程序类加载器的父加载器。3. 应用程序类加载器的父加载器是扩展类加载器,是自定义类加载器的默认父加载器。4. 自定义类加载器的父加载器可以自定义(继承 ClassLoader 时指定),默认是应用程序类加载器。注意:这种父子关系不是通过继承实现的,而是通过“父加载器引用”实现的,比如扩展类加载器持有启动类加载器的引用,加载时先委托给父加载器,形成层级委托关系,保证类加载的安全性和唯一性。