运行时数据区域

程序计数器
记录正在执行的虚拟机字节码指令的地址,如果是本地方法则为空.
Java虚拟机栈
每个 Java ⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。从⽅法调⽤直⾄执⾏完成的过程,对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程。
该区域可能抛出的异常:
- 当线程请求的栈深度超过最⼤值,会抛出 StackOverflowError 异常;
- 栈进⾏动态扩展时如果⽆法申请到⾜够内存,会抛出 OutOfMemoryError 异常。
本地方法栈
本地方法栈和Java虚拟机栈类似,区别在于本地方法栈为虚拟机使用到的 Native ⽅法服务.
堆
所有对象都在这⾥分配内存,是垃圾收集的主要区域(”GC 堆”)。
现代垃圾收集器基本都是采⽤分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
- 新⽣代(Young Generation)
- ⽼年代(Old Generation)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
⽅法区
⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆⼀样不需要连续的内存,并且可以动态扩展,动态扩展失败⼀样会抛出 OutOfMemoryError 异常。
在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放⼊堆中。
运行时常量池
是⽅法区的⼀部分。
Class ⽂件中的常量池(编译器⽣成的字⾯量和符号引⽤)会在类加载后被放⼊这个区域。
除了在编译期⽣成的常量,还允许动态⽣成,例如 String 类的 intern()。
直接内存
JDK 1.4 新引⼊了 NIO 类,它可以使⽤ Native 函数库直接分配堆外内存,然后通过 Java 堆⾥的 DirectByteBuffer对象作为这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼性能,因为避免了在堆内存和堆外内存来回拷⻉数据。
理解运行时的数据区


垃圾收集
垃圾收集主要是针对堆和⽅法区进⾏。程序计数器、虚拟机栈和本地⽅法栈这三个区域属于线程私有的,只存在于线程的⽣命周期内,线程结束之后就会消失,因此不需要对这三个区域进⾏垃圾回收。
判断一个对象是否可被回收
1. 引⽤计数算法
为对象添加⼀个引⽤计数器,当对象增加⼀个引⽤时计数器加 1,引⽤失效时计数器减 1。引⽤计数为 0 的对象可被回收。
但由于对象之间循环引用的存在,引⽤计数器也会失效。
2. 可达性分析算法
以 GC Roots 为起始点进⾏搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使⽤该算法来判断对象是否可被回收,GC Roots ⼀般包含以下内容:
- 虚拟机栈中局部变量表中引⽤的对象
- 本地⽅法栈中 JNI 中引⽤的对象
- ⽅法区中类静态属性引⽤的对象
- ⽅法区中的常量引⽤的对象
3. 方法区的回收
因为⽅法区主要存放永久代对象,⽽永久代对象的回收率⽐新⽣代低很多,所以在⽅法区上进⾏回收性价⽐不⾼。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在⼤量使⽤反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满⾜以下三个条件,并且满⾜了条件也不⼀定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地⽅被引⽤,也就⽆法在任何地⽅通过反射访问该类⽅法。
4. finalize()
类似 C++ 的析构函数,⽤于关闭外部资源。但是⽤ try-finally 可以做得更好,并且 finalize ⽅法运⾏代价很⾼,不确定性⼤,⽆法保证各个对象的调⽤顺序,因此最好不要使⽤。
引用类型
⽆论是通过引⽤计数算法判断对象的引⽤数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引⽤有关。Java 提供了四种强度不同的引⽤类型。
1. 强引⽤
被强引⽤关联的对象不会被回收。
使⽤ new ⼀个新对象的⽅式来创建强引⽤。Object obj = new Object();
2. 软引⽤
被软引⽤关联的对象只有在内存不够的情况下才会被回收。使⽤ SoftReference 类来创建软引⽤。
1 | Object obj = new Object(); |
3. 弱引⽤
被弱引⽤关联的对象⼀定会被回收,也就是说它只能存活到下⼀次垃圾回收发⽣之前。使⽤ WeakReference 类来
创建弱引⽤。
1 | Object obj = new Object(); |
4. 虚引⽤
⼜称为幽灵引⽤或者幻影引⽤,⼀个对象是否有虚引⽤的存在,不会对其⽣存时间造成影响,也⽆法通过虚引⽤得到⼀个对象。
为⼀个对象设置虚引⽤的唯⼀⽬的是能在这个对象被回收时收到⼀个系统通知。使⽤ PhantomReference 来创建虚引⽤。
1 | Object obj = new Object(); |
垃圾收集算法
1. 标记-清除算法
最基础的收集算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:
- 效率问题,标记和清除两个过程的效率都不⾼;
- 空间问题,会产⽣⼤量不连续的内存碎⽚,导致⽆法给⼤对象分配内存。
2. 标记-整理算法
让所有存活的对象都向⼀端移动,然后直接清理掉端边界以外的内存。
- 优点: 不会产⽣内存碎⽚
- 不⾜: 需要移动⼤量对象,处理效率⽐较低。
3. 复制算法
将内存划分为⼤⼩相等的两块,每次只使⽤其中⼀块,当这⼀块内存⽤完了就将还存活的对象复制到另⼀块上⾯,然后再把使⽤过的内存空间进⾏⼀次清理。
不⾜是只使⽤了内存的⼀半。
4. 分代收集算法
现在的商业虚拟机采⽤分代收集算法,它根据对象存活周期将内存划分为⼏块,不同块采⽤适当的收集算法。⼀般将堆分为新⽣代和⽼年代。
- 新⽣代使⽤:复制算法
- ⽼年代使⽤:标记 - 清除 或者 标记 - 整理 算法
垃圾收集器

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使⽤。
- 单线程与多线程:单线程指的是垃圾收集器只使⽤⼀个线程,⽽多线程使⽤多个线程;
- 串⾏与并⾏:串⾏指的是垃圾收集器与⽤户程序交替执⾏,这意味着在执⾏垃圾收集的时候需要停顿⽤户程
序;并⾏指的是垃圾收集器和⽤户程序同时执⾏。除了 CMS 和 G1 之外,其它垃圾收集器都是以串⾏的⽅式执⾏。
1. Serial 收集器

串行的垃圾收集器,是最基本、历史最悠久的垃圾收集器。单CPU环境下,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。
- 优点: 简单、容易实现
- 缺点: 单线程、停顿时间⻓
2. ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本,它是 Server 场景下默认的新⽣代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使⽤。
3. Parallel Scavenge 收集器

与 ParNew ⼀样是多线程收集器。
其它收集器⽬标是尽可能缩短垃圾收集时⽤户线程的停顿时间,⽽它的⽬标是达到⼀个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这⾥的吞吐量指 CPU ⽤于运⾏⽤户程序的时间占总时间的⽐值。
4. Serial Old 收集器

是 Serial 收集器的⽼年代版本,也是给 Client 场景下的虚拟机使⽤。如果⽤在 Server 场景下,它有两⼤⽤途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞⽣以前)中与 Parallel Scavenge 收集器搭配使⽤。
- 作为 CMS 收集器的后备预案,在并发收集发⽣ Concurrent Mode Failure 时使⽤。
5. Parallel Old 收集器

是 Parallel Scavenge 收集器的⽼年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6. CMS 收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
- 初始标记:仅仅只是标记⼀下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进⾏ GC Roots Tracing 的过程,它在整个回收过程中耗时最⻓,不需要停顿。
- 重新标记:为了修正并发标记期间因⽤户程序继续运作⽽导致标记产⽣变动的那⼀部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
在整个过程中耗时最⻓的并发标记和并发清除过程中,收集器线程都可以与⽤户线程⼀起⼯作,不需要进⾏停顿。
具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利⽤率不够⾼。
- ⽆法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于⽤户线程继续运⾏⽽产⽣的垃圾,这部分垃圾只能到下⼀次 GC 时才能进⾏回收。由于浮动垃圾的存在,因此需要预留出⼀部分内存,意味着 CMS 收集不能像其它收集器那样等待⽼年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启⽤ Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎⽚,往往出现⽼年代空间剩余,但⽆法找到⾜够⼤连续空间来分配当前对象,不得不提前触发⼀次 Full GC。
7. G1 收集器
G1(Garbage-First),它是⼀款⾯向服务端应⽤的垃圾收集器,在多 CPU 和⼤内存的场景下有很好的性能。
堆被分为新⽣代和⽼年代,其它收集器进⾏收集的范围都是整个新⽣代或者⽼年代,⽽ G1 可以直接对新⽣代和⽼年代⼀起回收。
G1 把堆划分成多个⼤⼩相等的独⽴区域(Region),新⽣代和⽼年代不再物理隔离。
通过引⼊ Region 的概念,从⽽将原来的⼀整块内存空间划分成多个的⼩空间,使得每个⼩空间可以单独进⾏垃圾回收。
这种划分⽅法带来了很⼤的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护⼀个优先列表,每次根据允许的收集时间,优先回收价值最⼤的 Region。
每个 Region 都有⼀个 Remembered Set,⽤来记录该 Region 对象的引⽤对象所在的 Region。通过使⽤Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作⼤致可划分为以下⼏个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因⽤户程序继续运作⽽导致标记产⽣变动的那⼀部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs ⾥⾯,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并⾏执⾏。
- 筛选回收:⾸先对各个 Region 中的回收价值和成本进⾏排序,根据⽤户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与⽤户程序⼀起并发执⾏,但是因为只回收⼀部分 Region,时间是⽤户可控制的,⽽且停顿⽤户线程将⼤幅度提⾼收集效率。
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运⾏期间不会产⽣内存空间碎⽚。
- 可预测的停顿:能让使⽤者明确指定在⼀个⻓度为 M 毫秒的时间⽚段内,消耗在 GC 上的时间不得超过 N 毫秒。
内存分配与回收策略’
Minor GC 和 Full GC
- Minor GC:回收新⽣代,因为新⽣代对象存活时间很短,因此 Minor GC 会频繁执⾏,执⾏的速度⼀般也会⽐较快。
- Full GC:回收⽼年代和新⽣代,⽼年代对象存活时间⻓,因此 Full GC 很少执⾏,执⾏速度会⽐ Minor GC 慢很多。
内存分配策略
- 对象优先在 Eden 分配
⼤多数情况下,对象在新⽣代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。 - ⼤对象直接进⼊⽼年代
⼤对象是指需要连续内存空间的对象,最典型的⼤对象是那种很⻓的字符串以及数组。
经常出现⼤对象会提前触发垃圾收集以获取⾜够的连续空间分配给⼤对象。
-XX:PretenureSizeThreshold,⼤于此值的对象直接在⽼年代分配,避免在 Eden 和 Survivor之间的⼤量内存复制。 - ⻓期存活的对象进⼊⽼年代
为对象定义年龄计数器,对象在 Eden 出⽣并经过 Minor GC 依然存活,将移动到 Survivor中,年龄就增加 1 岁,增加到⼀定年龄则移动到⽼年代中。
-XX:MaxTenuringThreshold ⽤来定义年龄的阈值。 - 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升⽼年代,如果在Survivor 中相同年龄所有对象⼤⼩的总和⼤于 Survivor 空间的⼀半,则年龄⼤于或等于该年龄的对象可以直接进⼊⽼年代,⽆需等到 MaxTenuringThreshold 中要求的年龄。 - 空间分配担保
在发⽣ Minor GC 之前,虚拟机先检查⽼年代最⼤可⽤的连续空间是否⼤于新⽣代所有对象总空间,如果条件成⽴的话,那么 Minor GC 可以确认是安全的。
如果不成⽴的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查⽼年代最⼤可⽤的连续空间是否⼤于历次晋升到⽼年代对象的平均⼤⼩,如果⼤于,将尝试着进⾏⼀次 Minor GC;如果⼩于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进⾏⼀次 Full GC。
Full GC 的触发条件
对于 Minor GC,其触发条件⾮常简单,当 Eden 空间满时,就将触发⼀次 Minor GC。⽽ Full
GC 则相对复杂,有以下条件:
- 调⽤ System.gc()
只是建议虚拟机执⾏ Full GC,但是虚拟机不⼀定真正去执⾏。不建议使⽤这种⽅式,⽽是让
虚拟机管理内存。 - ⽼年代空间不⾜
⽼年代空间不⾜的常⻅场景为前⽂所讲的⼤对象直接进⼊⽼年代、⻓期存活的对象进⼊⽼年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过⼤的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调⼤新⽣代的⼤⼩,让对象尽量在新⽣代被回收掉,不进⼊⽼年代。还可以通过 -XX:MaxTenuringThreshold 调⼤对象进⼊⽼年代的年龄,让对象在新⽣代多存活⼀段时间。 - 空间分配担保失败
使⽤复制算法的 Minor GC 需要⽼年代的内存空间作担保,如果担保失败会执⾏⼀次 FullGC。具体内容请参考上⾯的第 5 ⼩节。 - JDK 1.7 及以前的永久代空间不⾜
在 JDK 1.7 及以前,HotSpot 虚拟机中的⽅法区是⽤永久代实现的,永久代中存放的为⼀些Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调⽤的⽅法较多时,永久代可能会被占满,在未配置为采⽤CMS GC 的情况下也会执⾏ Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,可采⽤的⽅法为增⼤永久代空间或转为使⽤ CMS GC。 - Concurrent Mode Failure
执⾏ CMS GC 的过程中同时有对象要放⼊⽼年代,⽽此时⽼年代空间不⾜(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不⾜),便会报 Concurrent Mode Failure 错误,并触发Full GC。
类加载机制
类是在运⾏期间第⼀次使⽤时动态加载的,⽽不是⼀次性加载所有类。因为如果⼀次性加载,会占⽤很多的内存。
类的生命周期

包括以下 7 个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使⽤(Using)
- 卸载(Unloading)
类的加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。
1. 加载
加载是类加载的⼀个阶段,注意不要混淆。加载过程完成以下三件事:
- 通过类的完全限定名称获取定义该类的⼆进制字节流。
- 将该字节流表示的静态存储结构转换为⽅法区的运⾏时存储结构。
- 在内存中⽣成⼀个代表该类的 Class 对象,作为⽅法区中该类各种数据的访问⼊⼝。
其中⼆进制字节流可以从以下⽅式中获取:
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从⽹络中获取,最典型的应⽤是 Applet。
- 运⾏时计算⽣成,例如动态代理技术,在 java.lang.reflect.Proxy 使⽤ProxyGenerator.generateProxyClass 的代理类的⼆进制字节流。由其他⽂件⽣成,例如由 JSP ⽂件⽣成对应的 Class 类。
2. 验证
确保 Class ⽂件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机⾃身的安全。
3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使⽤的是⽅法区的内存。应该注意到,实例化不是类加载的⼀个过程,类加载发⽣在所有实例化操作之前,并且类加载只进⾏⼀次,实例化可以进⾏多次。
初始值⼀般为 0 值,例如下⾯的类变量 value 被初始化为 0 ⽽不是 123。public static int value = 123;
如果类变量是常量,那么它将初始化为表达式所定义的值⽽不是 0。例如下⾯的常量 value 被
初始化为 123 ⽽不是 0。public static final int value = 123;
4. 解析
将常量池的符号引⽤替换为直接引⽤的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了⽀持 Java 的动态绑定。
5. 初始化
初始化阶段才真正开始执⾏类中定义的 Java 程序代码。
初始化阶段是虚拟机执⾏类构造器<clinit>() ⽅法的过程。在准备阶段,类变量已经赋过⼀次系统要求的初始值,⽽在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
<clinit>() 是由编译器⾃动收集类中所有类变量的赋值动作和静态语句块中的语句合并产⽣的,编译器收集的顺序由语句在源⽂件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
1 | public class Test { |
由于⽗类的
类。例如以下代码:
1 | static class Parent { |
静态代码块和赋值动作是按照代码的顺序执行的。
接⼝中不可以使⽤静态语句块,但仍然有类变量初始化的赋值操作,因此接⼝与类⼀样都会⽣成 <clinit>() ⽅法。
但接⼝与类不同的是,执⾏接⼝的 <clinit>() ⽅法不需要先执⾏⽗接⼝的 <clinit>() ⽅法。只有当⽗接⼝中定义的变量使⽤时,⽗接⼝才会初始化。另外,接⼝的实现类在初始化时也⼀样不会执⾏接⼝的 <clinit>() ⽅法。虚拟机会保证⼀个类的 <clinit>() ⽅法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化⼀个类,只会有⼀个线程执⾏这个类的 <clinit>() ⽅法,其它线程都会阻塞等待,直到活动线程执⾏ <clinit>() ⽅法完毕。
如果在⼀个类的<clinit>()⽅法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
类的初始化时机
1.主动引用
虚拟机规范中并没有强制约束何时进⾏加载,但是规范严格规定了有且只有下列五种情况必须对类进⾏初始化(加载、验证、准备都会随之发⽣):
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进⾏过初始化,则需要先触发其初始化。
- 使⽤ java.lang.reflect 包的方法对类进⾏反射调⽤的时候,如果类没有进⾏过初始化,则需要先触发其初始化。
- 当初始化⼀个类时,如果发现其父类还没有进⾏过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,⽤于执⾏主类(包含 main() ⽅法的那个类)的初始化。
- 使⽤ JDK 7 新加入的动态语言⽀持时,如果⼀个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的⽅法句柄,并且这个⽅法句柄所对应的类没有进⾏过初始化,则需要先出触发其初始化。
2.被动引用
以上 5 种场景中的⾏为称为对⼀个类进⾏主动引⽤。除此之外,所有引⽤类的⽅式都不会触发初始化,称为被动引⽤。被动引⽤的常⻅例⼦包括:通过⼦类引⽤⽗类的静态字段,不会导致⼦类初始化。
1 | System.out.println(SubClass.value); // value 字段在 SuperClass 中定义 |
通过数组定义来引⽤类,不会触发此类的初始化。该过程会对数组类进⾏初始化,数组类是⼀个由虚拟机⾃动⽣成的、直接继承⾃ Object 的⼦类,其中包含了数组的属性和⽅法。
1 | SuperClass[] sca = new SuperClass[10]; |
常量在编译阶段会存⼊调⽤类的常量池中,本质上并没有直接引⽤到定义常量的类,因此不会触发定义常量的类的初始化。
1 | System.out.println(ConstClass.HELLOWORLD); |
类加载器分类
从 Java 虚拟机的⻆度来讲,只存在以下两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),使⽤ C++ 实现,是虚拟机⾃身的⼀部分;
- 所有其它类的加载器,使⽤ Java 实现,独⽴于虚拟机,继承⾃抽象类java.lang.ClassLoader。
从 Java 开发⼯程的⻆度来看,类加载器可以划分得更细致一些:
- 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在
<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引⽤,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器去处理,那么直接⽤ null 代替即可。 - 扩展类加载器(Extension ClassLoader):这个类加载器是在类 java.lang.ClassLoader 的构造函数中被调⽤的。它负责将
/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使⽤扩展类加载器。 - 应用程序类加载器(Application ClassLoader):这个类加载器是在类 java.lang.ClassLoader 的构造函数中被调⽤的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以也被称为系统类加载器。它负责将⽤户类路径(ClassPath)上所指定的类库加载到内存中。开发者可以直接使⽤这个类加载器,如果应⽤程序中没有定义过⾃定义的类加载器,一般情况下这个就是程序中默认的类加载器。

- ⼯作过程
⼀个类加载器⾸先将类加载请求转发到⽗类加载器,只有当⽗类加载器⽆法完成时才尝试⾃⼰加载。 - 好处
使得 Java 类随着它的类加载器⼀起具有⼀种带有优先级的层次关系,从⽽使得基础类得到统⼀。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外⼀个 java.lang.Object 并放到 ClassPath中,程序可以编译通过。
由于双亲委派模型的存在,所以在 rt.jar 中的 Object ⽐在 ClassPath 中的 Object 优先级更⾼,这是因为 rt.jar 中的 Object 使⽤的是启动类加载器,⽽ ClassPath 中的 Object 使⽤的是应⽤程序类加载器。rt.jar 中的 Object 优先级更⾼,那么程序中所有的 Object 都是这个Object。