JVM
- 1、JVM 的运行时内存区域是怎样的
- 2、堆和栈的区别
- 3、Java 中的对象一定在堆上分配内存吗
- 4、什么是 Stop The World
- 5、JVM 如何判断对象是否存活
- 6、JVM 有哪些垃圾回收算法
- 7、什么是三色标记算法
- 8、新生代和老年代的GC算法
- 9、新生代和老年代的垃圾回收器有何区别
- 10、Java 中的四种引用有什么区别
- 11、Java 中类加载的过程是怎么样的
- 12、Java中的类什么时候会被加载
- 13、什么是双亲委派
1、JVM 的运行时内存区域是怎样的
根据Java虚拟机规范的定义,V的运行时内存区域注要由堆
、虚拟机栈
、本地方法栈
、方法区
和程序计数器
以及运行时常量池
组成。其中堆、方法区以及运行时常量池是线程之间共享的区域,而栈(本地方法栈+虚拟机栈)、程序计数器都是线程独享的。
JVM 程序计数器
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。在线程中程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。所以程序计数器一定是线程私有的。
JVM 虚拟机栈
JVM中的方法栈是线程私有的,每一个方法的调用会在方法栈中加入一个栈帧,比如这样启动 main 方法
public static void main(String[] args) {
methodA();
}
public static void methodA() {
int a = 0;
int b = a + 3;
methodB();
}
public static void methodB() {
}
栈中压入 main 方法的栈帧,执行到 methodA 方法,栈中压入 methodA 方法的栈帧,执行到 methodB 方法,栈中压入 methodB 方法的栈帧,每个方法执行完成之后,这个方法所对应的栈帧就会出栈,每个栈帧中大概存储这五个内容:局部变量表(存储局部变量的空间)、操作数栈(线程执行时使用到的数据存储空间)、动态链接(方法区的引用,例如类信息,常量、静态变量)、返回地址(存储这个方法被调用的位置,因为方法执行后还需要到方法被调用的位置)、附加信息(增加的一些规范里面没有的信息,可以添加自己的附加信息),这就是栈和栈帧。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
堆
堆:是存储对象实例的运行时内存区域。它是虚拟机运行时的内存总体的最大的一块,也一直占据着虚拟机内存总量的一大部分。Java堆由ava虚拟机管理,用于存放对象实例,是几乎所有的对象实例都要在上面分配内存。此外,Java堆还用于垃圾回收,虚拟机发现没有被引用的对象时,就会对堆中对象进行垃圾回收,以释放内存空间。
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
Java 7及之前堆内存逻辑上分为三部分:新生代+老年代+永久代,Java 8及之后堆内存逻辑上分为三部分:新生代+老年代+元空间
设置堆大小与OOM
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项"-Xmx"和"-Xms"来进行设置
- "-Xms"用于表示堆区的起始内存,等价于-XX: InitialHeapsize。
- "-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapsize。
一旦堆区中的内存大小超过"-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下,初始内存大小:物理电脑内存大小/ 64 最大内存大小:物理电脑内存大小/4。
注意:设置的堆大小不包含元空间(或永久代)
年轻代与老年代
存活在JVM中的对象可以分为两类,一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速,另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
而在JVM堆内存中,又给分为年轻代(Young Generation)和老年代(Old Generation),年轻代中又分为伊甸园区(Ende)和from区(或者叫做Survivor0区)以及to区(或者叫做Survivor1区)。
其中它们的比例如下
年轻代整个占用堆空间的1/3,老年代占用整个堆空间的2/3,而Ende区占用年轻代的8/10,S1和S0分别占用年轻代的1/10。
对象分配过程
当程序启动,对象开始创建会放在Eden区,当Eden区满了会触发一次MinorGC(或者叫做YoungGC),将没有引用的对象清除,存在引用的对象移动到S0区,将移动过的对象年龄标识加1。
这时Eden区为空,当第二次Eden区满了后再次触发MinorGC,将没有引用的对象清除,存在引用的对象移动到S1区,同时检查S0区中对象,如果没有引用的对象清除,存在引用的对象也移动到S1区,将移动过的对象年龄标识加1。
当对象年龄标志大于15将放入老年代
JVM在进行GC时,并非每次都对上面三个内存(新生代,老年代,方法区)区域一起回收,大部分回收都是指的新生代,里面的GC按照回收区域又分为两大类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)。
- 部分收集:不是完整的收集整个java堆的垃圾收集,其中又分为
- 新生代收集(Minor GC/Young GC):只是对新生代(Eden/S0/S1)的垃圾收集。
- 老年代收集(Major GC/Old GC):只是对老年代(Old)的垃圾收集。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集(只有G1垃圾收集器会有这种行为)。
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
TLAB
先来看对象的创建过程,为对象分配空间的任务等同于在java堆中划分一块大小确定的内存出来,假设java堆空间内存是绝对规整的,所有使用过的内存都放在一边,没有使用过的内存放在另一半,中间放着一个指针作为分界点的指示器,当分配内存就仅仅是把指针想空闲的空间移动一段与对象大小相等的距离,这种分配方法称为指针碰撞(Bump The Pointer)。
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又使用了原来的指针来分配内存的情况。
解决方案其一就是使用本地线程分配缓冲(TLAB),对Eden区继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden区内,即每个线程在java堆中预先分配一小块内存,哪个线程需要分配内存,就在哪个线程的本地缓冲区中分配,当本地缓冲使用完,分配新的缓存区时才需要同步锁定。
尽管不是所有对象实例都能在TLAB中成功分配内存,但是JVM确定是将TLAB作为内存分配的首选,可以通过选项 -XX:UseTLAB 设置是否开启TLAB空间,默认的情况下TLAB占用的内存非常小,仅占用Eden空间的1%。
堆的内存分配过程
- 最开始应该使用线程分配缓冲区(tlab)来给对象分配空间,每个线程都有一个tlab,它可以保证线程的安全。
- 使用tlab分配空间失败时考虑通过加锁的方式(多线程),在eden区分配空间,如果eden区满了,就会触发一次minor gc,它会清除掉没有用的对象,判断一个对象是否能被搜集通常有两种算法:引用计数器法、可达性分析法;存活下来的对象将会进入eden的from区,然后清空eden区。
- 当eden区满了,会第二次触发minor gc,他会将eden存活下来的对象放入to区,from存活下来的对象年龄+1后也进入to区,然后清空eden区和from区。
- 当eden区再次满时,第三次执行minor gc,这一次eden区存活下来的对象进入from区,to区存活下来的对象年龄+1也会进入from区,然后清空eden区和to区。
- 随着对象的数量增加,不停的做上面两次操作,到对象的年龄到达老年带所规定的年龄阈值的时候,对象从新生代进入老年代。
- 随着对象增加,老年代满时会执行major gc操作,gc后对象仍然无法保存报内存溢出。
方法区
方法区:用于存储已被加载的类信息、常量、静态变量、即时编译后的代码等数据的内存区域。每加载一个类,方
法区就会分配一定的内存空间,用于存储该类的相关信息,这部分空间随着需要而动态变化。方法区的具体实现形
式可以有多种,比如堆、永久代、元空间等。
栈,堆,方法区中的交互关系
方法区简述
方法区与java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译后的代码缓存等数据。方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆区分开来。
所以,方法区看作是一块独立于堆的内存空间,方法区在JVM启动时候就被创建,并且它的实际物理内存空间和java堆区一样都是可以不连续的,方法区的大小可以选择固定大小,也可以选择可扩展,方法区的大小决定了可以保持多少个类,如果系统定义了太多的类导致方法区溢出,虚拟机同样会抛出内存溢出异常:java.lang.OutOfMemoryError:PermGen Space或java.lang.OutOfMemoryError,为什么是两种异常呢?因为在JDK8之前,方法区又称为"永久代",而JDK8以及8之后改为"元空间"。
方法区,元空间,永久代三者关系是什么呢?方法区是java虚拟机规范的一部分,而元空间和永久代是一个具体的实现,元空间的本质和永久代类似,都是对JVM规范方法区的实现,不过两者最大的区别就是:元空间不在虚拟机中设置内存,而是直接使用本地内存
。
运行时常量池
运行时常量池是每一个类或接口的常量池的运行时表示形式,具体体现就是在Java编译后生成的.class文件中,会有class常量池,也就是静态的运行时常量池,在不同版本的JDK中,运行时常量池所处的位置也不一样,JDK1.7及之前方法区位于永久代,由于一些原因在JDK1.8之后彻底祛除了永久代用元空间代替,所以运行时常量池一直是方法区的一部分。
存放编译期生成的各种字面量和符号引用,运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。 此时不再是常量池中的符号地址了,这里换为真实地址。
2、堆和栈的区别
堆和栈是)va程序运行过程中主要存储区域,经常被拿来对北比,他们主要有以下区别(这里的栈主要指的是虚拟机栈):
- 存储位置不同,堆是在堆内存中分配空间,而栈是在的栈内存中分配空间。
- 存储的内容不同,堆中主要存储对象,栈中主要存储本地变量。
- 堆是线程共享的,栈是线程独享的。
- 堆是垃圾回收的主要区域,不再引用这个对象,会被垃圾回收机制会自动回收。栈的内存使用是一种先进后出的机制,栈中的变量会在程序执行完毕后自动释放。
- 栈的大小比堆要小的多,一般是几百到几干字节。
- 栈的存储速度比堆快,代码执行效率高。
- 堆上会发生OutofMemoryError,栈上会发生StackOverflowError。
3、Java 中的对象一定在堆上分配内存吗
不一定,在HotSpot虚拟机中,存在T优化的机制,T优化中可能会进行逃逸分析,当经过逃逸分析发现某一个局部对象没有逃逸到方法外的话,那么这个对象就可能不会在堆上分配内存,而是进行栈上分配。
4、什么是 Stop The World
Java中Stop-The-Vorld机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。这是
Java中一种全局暂停现象,全局停顿,所有java代码停止,native代码可以执行,但不能与JVM交互。
不管选择哪种GC算法,stop-the-world都是不能彻底避免的,只能尽量降低STW的时长。
为什么需要STW呢?
首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和错标。
- 多标:原本不是垃圾,但是GC的过程中,用户线程将其引用关系修改,导致GC Roots不可达,成为了垃圾。这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。
- 漏标:原本是垃圾,但是GC的过程中,用户线程将引用重新指向了它,这时如果GC一旦将其回收,将会导致程序运行错误。
5、JVM 如何判断对象是否存活
JVM有两种算法来判断对象是否存活,分别是引用计数法和可达性分析算法
- 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
- 可达性分析算法:这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。
但是,并不是说当进行完可达性分析算法后,即可证明某对象可以被GC。对象是否存活,需要两次标记:
- 第一次标记通过可达性分析算法。如果没有GC Roots相连接的引用链,那么将第一次标记。
- 如果对象的finalize()方法被覆盖并且没有执行过,则放在F-Queuel队列中等待执行不一定会执行),如果一段时间后该队列的finalize()方法被执行且和GC Roots关联,则移出“即将回收”集合。如果仍然没有关联,则进行第二次标记,才会对该对象进行回收不过现在都不提倡覆盖finalize方法,它的本意是像Cpp一样在对象销毁前执行,但是它影响了JAVA的安全和GC的性能,所以第二种判断会越来越少。
6、JVM 有哪些垃圾回收算法
① 标记 - 清除算法(Tracing Collector)
标记-清除 算法是最基础的收集算法,它是由 标记 和 清除 两个步骤组成的。第一步是标记存活的对象,第二步是清除没有被标记的垃圾对象。
该算法的优点是当存活对象比较多的时候,性能比较高,因为该算法只需要处理待回收的对象,而不需要处理存活的对象。但是缺点也很明显,清除之后会产生大量不连续的内存碎片。导致之后程序在运行时需要分配较大的对象时,无法找到足够的连续内存。
② 标记 - 整理算法(Compacting Collector)
上述的 标记-清除 算法会产生内存区域使用的间断,所以为了将内存区域尽可能地连续使用, 标记-整理 算法应运而生。标记-整理 算法也是由两步组成,标记 和 整理。
和标记清除算法一样,先进行对象的标记,通过GC Roots节点扫描存活对象进行标记,将所有存活对象往一端空闲空间移动,按照内存地址依次排序,并更新对应引用的指针,然后清理末端内存地址以外的全部内存空间。
但是同样,标记整理算法也有它的缺点,一方面它要标记所有存活对象,另一方面还添加了对象的移动操作以及更新引用地址的操作,因此标记整理算法具有更高的使用成本。
③ 复制算法(Copying Collector)
无论是标记-清除算法还是垃圾-整理算法,都会涉及句柄的开销或是面对碎片化的内存回收,所以,复制算法 出现了。
复制算法将内存区域均分为了两块(记为S0和S1),而每次在创建对象的时候,只使用其中的一块区域(例如S0),当S0使用完之后,便将S0上面存活的对象全部复制到S1上面去,然后将S0全部清理掉。复制算法主要被应用于新生代,它将内存分为大小相同的两块,每次只使用其中的一块。在任意时间点,所有动态分配的对象都只能分配在其中一个内存空间,而另外一个内存空间则是空闲的。
但是缺点也是很明显的,可用的内存减小了一半,存在内存浪费的情况。所以 复制算法 一般会用于对象存活时间比较短的区域,例如 年轻代,而存活时间比较长的 老年代 是不适合的,因为老年代存在大量存活时间长的对象,采用复制算法的时候会要求复制的对象较多,效率也就急剧下降,所以老年代一般会使用上文提到的 标记-整理算法。
单纯的从时间长短上面来看:标记-清除 < 标记-复制 < 标记-整理。单纯从结果来看:标记-整理 > 标记-复制 >= 标记-清除
7、什么是三色标记算法
三色标记算法是一种垃圾回收的标记算法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。 JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。
三色标记法将对象分为三种状态:白色、灰色和黑色。
- 白色:该对象没有被标记过。
- 灰色:该对象已经被标记过了,但该对象的引用对象还没标记完。
- 黑色:该对象已经被标记过了,并且他的全部引用对象也都标记完了。
三色标记法的标记过程可以分为三个阶段:初始标记(Initial Marking)、并发标记(Concurrent Marking)和
重新标记(Remark)。
- 初始标记:遍历所有的根对象,将根对象和直接引用的对象标记为灰色。在这个阶段中,垃圾回收器只会扫描
被直接或者间接引用的对象,而不会扫描整个堆。因此,初始标记阶段的时间比较短。(Stop The World) - 并发标记:在这个过程中,垃圾回收器会从灰色对象开始遍历整个对象图,将被引用的对象标记为灰色,并将
已经遍历过的对象标记为黑色。并发标记过程中,应用程序线程可能会修改对象图,因此垃圾回收器需要使用 写屏障(Vrite Barrier)技术来保证并发标记的正确性。(不需要STW) - 重新标记:重新标记的主要作用是标记在并发标记阶段中被修改的对象以及未被遍历到的对象。这个过程中,
垃圾回收器会从灰色对象重新开始遍历对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑 色。(Stop The World)
在重新标记阶段结束之后,垃圾回收器会执行清除操作,将未被标记为可达对象的对象进行回收,从而释放内存空
间。这个过程中,垃圾回收器会将所有未被标记的对象标记为白色(White)。
以上三个标记阶段中,初始标记和重新标记是需要STW的,而并发标记是不需要STW的。其中最耗时的其实就是
并发标记的这个阶段,因为这个阶段需要遍历整个对象树,而三色标记把这个阶段做到了和应用线程并发执行,大
大降低了GC的停顿时长。
8、新生代和老年代的GC算法
因为对于新生代来说,一般来说GC的次数是要比老年代高很多的,所以需要一个效率更高的算法,而且最好不要
有碎片,因为很多对象都是需要先在新生代分配空间的,如果碎片太多的话,那么就会导致很多对象无法正常分配
了。
所以,新生代选择了标记复制算法进行垃圾回收,但是标记复制算法存在一个缺点就是会浪费空间,新生代为了解
决这个问题,把区域进一步细分成一个Eden区和两个Survivorl区,同时工作的只有一个Eden区+一个Survivor区,这样,另外一个Survivor主要用来复制就可以了。只需要动态的调整Eden区和Survivor[区的比例就可以降低
空间浪费的问题。
对于老年代来说,通常会采用标记整理算法,虽然效率低了一点,但是可以减少空间的浪费并且不会有空间碎片等
问题。在有些回收器上面,如CMS,为了降低STW的时长,也会采用标记清除算法。
9、新生代和老年代的垃圾回收器有何区别
常见的垃圾回收器如下:
- 串行垃圾回收器(Serial Garbage Collector)如:Serial GC,Serial Old
- 并行垃圾回收器(Parallel Garbage Collector)如:Parallel Scavenge,Parallel Old,ParNew
- 并发标记扫描垃圾回收器(CMS Garbage Collector)
- G1垃圾回收器(G1 Garbage Collector,JDK7中推出,JDK9中设置为默认)
- ZGC垃圾回收器(The Z Garbage Collector,JDK11推出)
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行 | 老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 适用于单CPU环境下的Client模式 |
Paraller Old | 并行 | 老年代 | 标记-整理(压缩)算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发 | 老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
G1 | 并发、并行 | 新生代、老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 响应速度优先 |
新生代收集器有Serial、ParNew、Parallel Scavenge。
老年代收集器有Serial Old、Parallel Old、CMS。
整堆收集器有G1、ZGC。
10、Java 中的四种引用有什么区别
1、强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object();
obj = null;
只要强引用存在,垃圾回收器将永远不会回收被引用的对象。如果想被回收,可以将对象置为null
2、软引用
在内存足够的时候,软引用不会被回收,只有在内存不足时,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会跑出内存溢出异常。
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
3、弱引用
进行垃圾回收时,弱引用就会被回收。
4、虚引用(PhantomReference)
5、引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用、虚引用一起配合使用。当垃圾回收器准备回收一个对象时,如果发现它还有引用,就会在回收对象之前,把这个引用加入到引用队列中。程序可以通过判断引用队列中是否加入了引用,来判断被引用的对象是否将要被垃圾回收,这样可以在对象被回收之前采取一些必要的措施。
11、Java 中类加载的过程是怎么样的
类加载主要分为三个阶段,加载,链接,初始化,加载就是把class文件加载到方法区,链接又分为三个子阶段,验证,准备和解析,验证阶段主要验证加载的class是否正确,比如文件格式检验,准备阶段会为静态变量分配内存并赋0值,解析阶段会将符号引用解析为直接引用,在一个字节码文件中会用到其他类,但是字节码文件只会存用到类的类名,解析阶段就是会根据类名找到该类加载后在方法区的地址,也就是直接引用,并替换符号引用,这样运行到字节码时,就能直接找到某个类了,最后初始化阶段会给静态变量赋值,并执行静态代码块,这就是类加载过程。
12、Java中的类什么时候会被加载
Java中的类在以下几种情况中会被加载:
- 当创建类的实例时,如果该类还没有被加载,则会触发类的加载。例如,通过关键字new创建一个类的对象
时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。 - 当使用类的静态变量或静态方法时,如果该类还没有被加载,则会触发类的加载。例如,当调用某个类的静态方法时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
- 当使用反射机制问类时,如果该类还没有被加载,则会触发类的加载。例如,当使用Class.forName方法加载某个类时,M会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
- 当JVM启动时,会自动加载一些基础类,例如java.lang.Object类和java.lang.Class类等。
总之,Jv中的类加载其实是延迟加载的,除了一些基础的类以外,其他的类都是在需要使用类时才会进行加
载。同时,Java还支持动态加载类,即在运行时通过程序来加载类,这为ava程序带来了更大的灵活性。
13、什么是双亲委派
java中存在3种类型的类加载器:引导类加载器,扩展类加载器和系统类加载器。三者是的关系是:引导类加载器是扩展类加载器的父类,扩展类加载器是系统类加载器的父类。
- Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
- ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
- AppClassLoader:主要负责加载应用程序的主函数类。
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
为什么使用双亲委派
- 安全:就算自己定义了一个Java.lang.String,加载器也会通过AppClassLoader->ExtClassLoader->BootstrapLoader路径加载到核心jar包。这样便可以防止核心 API库被随意篡改。
- 避免类重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一。
- 90%以上的类都是应用加载器进行加载,虽然第一次加载类的时候需要经历一次AppClassLoader->ExtClassLoader->BootstrapLoader。但是第二次用的时候就不需要了。如果直接从BootstrapLoader找有没有加载的话,第一次很快。但是已加载的类,特别是应用类加载器加载的,每次都需要经历引导类加载器和扩展类加载器,这样就太慢了。