前言
本篇文章为我个人在学习JVM时所记录的笔记,内容把部分来自《深入理解java虚拟机》一书,笔记中总结了JVM中一些比较重要的知识点并作出了自己的解释。
java运行时数据区域
程序计数器(线程内私有)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里[1],字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
总结:来记录何时入栈与出栈起到辅助作用
虚拟机栈(线程内私有)
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
总结:存放方法入栈,将其顺序拷贝运行
本地方法栈(线程内私有)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务。
总结:用来翻译栈,翻译为操作系统认识的方法(其他语言中也需要被翻译),翻译为操作系统内核方法,这样才能调动驱动。
因此Java在运行时不只会使用Java内部内存区的本地内存,因为使用本地方法栈,所以也会消耗操作系统本身的内存
方法区(所有线程共享)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作"非堆"目的是与Java堆区分开来。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
总结:存放类信息,常量和静态信息常量池
堆(所有线程共享)
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里"几乎"所有的对象实例都在这里分配内存。
总结:存放对象和字符串常量池
内存分配的两种方式:
空闲列表
指针碰撞
数据区域的生命周期
- 线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁
- 线程共享区域随虚拟机的启动/关闭而创建/销毁
常量池
类常量池,字符串常量池。
一直存在,减少重复的创建和销毁,作用类似于缓存,提升速度。
直接内存
操作系统不能直接把数据交给虚拟机,只能虚拟机交给操作系统,操作系统可以有放到操作系统内存。
频繁的io操作会让直接内存和本地内存占满。
dump:一瞬间的内存状态形成文件。
如何判断对象已死?
引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
可达性分析
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析算法来判定对象是否存活的。
这个算法的基本思路就是通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的
- 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
- NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象"临时性"地加入,共同构成完整GC Roots集合。
有哪几种引用方式?
强引用,软引用,弱引用,虚引用(强度越来越差)
引用的强度越弱,对内存的压力越低
ThreadLocal的内部是弱引用
垃圾回收机制
分代收集理论
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
Java堆划分为新生代和老年代两个区域
在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
年轻代是怎么回收的?有哪几个区域
利用了标记复制,有三个区域:有一个Eden区(对象优先在新生代Eden区中分配),两个Survior-Ratio区(同一时刻只能有一个在运作,另一个用来拷贝),每个区域采用不同的垃圾回收算法
当一次垃圾回收后,Eden区和一个Survior-Ratio区中保留的对象拷贝到另一个Survior-Ratio区,再将其清空
年轻代中什么时候进行垃圾回收?
当空间满时或是某些对象年龄超过15岁进行垃圾回收,再或者就是人工主动触发,会有延迟
新生代中Eden和Survior-Ratio的默认比例是8:1
大对象直接进入年老代
**动态对象年龄判定:**如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,
年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:
标记-清除算法
"标记-清除"算法是最早出现也是最基础的垃圾收集算法是,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
优点:
算法比较简单,实现起来比较容易
缺点:
- 执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作。
- 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片。
标记-复制算法
标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低
的问题,1969年Fenichel提出了一种称为"半区复制"(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
- 实现简单,运行高效
- 没有碎片化的问题
缺点:内存缩小为了以前的一半,空间浪费严重
标记-整理算法
针对老年代对象的存亡特征,其中的标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
标记-整理算法结合了标记-清除算法和复制算法的优点,这种算法可以减少内存碎片,但需要移动对象,可能会影响程序的执行性能。
垃圾收集器
查看默认垃圾收集器
-XX:+PrintCommandLineFlags
:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID
Serial
(单线程、 复制算法)
最基本的垃圾收集器,使用复制算法,单线程,虽然收集垃圾时需要暂停其他所有的工作线程,但简单高效,是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
- 在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial Old GC
ParNew
(Serial的多线程版本、 复制算法)
是 Serial 收集器的多线程版本 ,除了多线程进行GC外,其他与Serial一样,默认开启和 CPU 数目相同的线程数 。是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
- 可以通过选项"-XX:+UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代
- 这里的多线程指的是垃圾收集时,多线程并行,并不是垃圾收集与程序运行并行
- 收集垃圾时,也需要暂停其他所有工作线程,然后多线程收集垃圾。
- 单CPU环境下,因为线程切换,性能较差。
Parallel Scavenge
(多线程复制算法、高效)
关注程序的吞吐量,即吞吐量优先。主要适用于在后台运算而不需要太多交互的任务。 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
-
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) ; 吞吐量优先,意味着在单位时间内,STW的时间最短;与之相对应的 低延迟 就是暂停时间优先,尽可能让单次STW时间最短;这两个无法同时实现。
-
收集垃圾时,也需要暂停其他所有工作线程,然后多线程收集垃圾。
参数配置
-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
-XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。
分别适用于新生代和老年代。默认jdk8是开启的。
上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
-XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STw的时间)。单位是毫秒。
为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。
对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
该参数使用需谨慎。
-XX:GCTimeRatio 垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。
取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%。
与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
-XX:+UseAdaptivesizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills,让虚拟机自己完成调优工作。
Serial Old
(单线程、标记整理算法)
是Serial的老年代版本,收集垃圾时也需要暂停其他所有的工作线程。
是Client模式下默认的老年代垃圾收集器
Server模式下,搭配新生代的Parallel Scavenge 收集器使用(在 JDK1.5 之前版本中)。同时也作为老年代中使用 CMS 收集器的后备垃圾收集方案(当CMS发生Concurrent Mode Failure)。
Parallel Old
(多线程、标记整理算法)
Parallel Scavenge的老年代版本
吞吐量优先,意味着在单位时间内,STW的时间最短;与之相对应的 低延迟 就是暂停时间优先,尽可能让单次STW时间最短;这两个无法同时实现。
若相同对于吞吐量要求较高,可以Parallel Scavenge搭配Parallel Old使用。
CMS
(Concurrent mark sweep)(多线程、标记清除算法 低延迟)
CMS是一款并发、使用标记-清理(Mark-Sweep)算法的gc,是针对老年代进行回收的GC。
Mark-Sweep标记清理算法
-
阶段1: Mark-Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;
-
阶段2: Compact 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列(节省内存资源)。
CMS的执行阶段:
- 初始标记(STW) :会STW(停止用户线程),标记GC ROOT能直接关联到的对象。注意是直接关联,间接关联的对象在下一阶段标记。
- 并发标记:与用户线程并发执行,遍历上一步被标记的对象,继续标记可达的对象,由于用户线程还在执行,就可能会导致老年代出现(某些对象从新生代晋升到老年代,老年的引用发生改变,有些对象直接分配到老年代)
- 并发预清理 :标记老年代存活的对象,目的是为了让重新标记阶段的STW尽可能短。这个阶段也需要扫描新生代+老年代。
- 重心标记(STW):重新扫描堆中的对象,进行可达性分析,标记活着的对象,这个阶段扫描的目标是:新生代的对象 + Gc Roots + 前面被标记为dirty的card对应的老年代对象。
- 并发清理:并发清理阶段,主要工作是清理所有未被标记的死亡对象,回收被占用的空间。
- 重置:CMS内部重置回收器状态,准备进入下一个并发回收周期
CMS重要参数
-
-XX:+UseParNewGC, -XX:+UseConcMarkSweepGC
-
分别表示使用并行收集器 ParNew 对新生代进行垃圾回收,使用并发标记清除收集器 CMS 对老年代进行垃圾回收。
-
-XX:ParallelGCThreads, -XX:ParallelCMSThreads
分别表示 Young GC 与 CMS GC 工作时的并行线程数,建议根据处理器数量进行合理设置。
-
-XX:+UseCMSCompactAtFullCollection
由于 CMS GC 会产生内存碎片,且只在 Full GC 时才会进行内存碎片压缩(因此 使用 CMS 垃圾回收器避免不了 Full GC)。这个参数表示开启 Full GC 时的压缩功能,减少内存碎片。
-
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction
-XX:CMSInitiatingOccupancyFraction 表示触发 CMS GC 的老年代使用阈值,一般设置为 70~80(百分比),设置太小会增加 CMS GC 发现的频率,设置太大可能会导致并发模式失败或晋升失败。
-
-XX:+UseCMSInitiatingOccupancyOnly 表示 CMS GC 只基于 CMSInitiatingOccupancyFraction 触发,如果未设置该参数则 JVM 只会根据 CMSInitiatingOccupancyFraction 触发第一次 CMS GC ,后续还是会自动触发。建议同时设置这两个参数。
-
-XX:+CMSClassUnloadingEnabled
表示开启 CMS 对永久代的垃圾回收(或元空间),避免由于永久代空间耗尽带来 Full GC。
-
-XX:+CMSParallelRemarkEnabled
开启降低标记停顿。
G1(Garbage first)
目前垃圾收集器理论发展最前沿的成果,对比CMS收集器有两点改进:一是采用标记整理算法,不产生内存碎片,二是可以精确控制停顿时间,在不牺牲吞吐量的前提下,减少垃圾回收停顿。使用-XX:+UseG1GC来启用。
G1收集器将堆内存划分为大小固定的几个区域(局部压缩),以分区为单位进行回收,将存活对象复制到另一个空闲空间。并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率
内存分区上不存在老年代和新生代的区别,只是在逻辑上有分代的概念,随着G1的运行,每个分区都可能在不同代之间切换。
G1的收集都是STW,属于混合收集的方式,每次收集时可能只收集年轻代的区域,也可能收集年轻代的同时,也包含部分老年代区域。
G1内存模型如下,对于老年代和新生代只有逻辑上的分代。heap被划分为一系列大小相等的"小堆区",也称为region。
-
将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
-
每个Region都是通过指针碰撞来分配空间
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域
可预测的停顿时间模型(即:软实时soft real-time)
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
-
由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
-
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
-
相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1的垃圾收集过程
G1GC的垃圾回收过程主要包括如下三个环节:
-
年轻代GC(Young GC)
-
老年代并发标记过程(Concurrent Marking)
-
混合回收(Mixed GC)
CMS收集器和G收集器的对比
CMS:特点是并发收集,低停顿(如果是单核处理器,多线程会更慢,一定是多核下多线程才会更快)
G1:进行垃圾回收时用户线程不用停,某个区域会有短时间停顿,整体来看没有停顿
衡量垃圾收集器的三个重要指标
内存占用,吞吐量和延迟
几乎找不到一种收集器可以解决碎片化问题
调优指令
jps命令:虚拟机进程状态工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称
以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)
jps指令格式: jps [options] [hostid]
jstat命令:虚拟机统计信息监视工具
jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。
它可以显示本地或者远程[1]虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
指令格式:jstat [ option vmid [interval[s|ms] [count]] ]
参数interval和count代表查询间隔和次数,如果省略这2个参数,说明只查询一次。假设需要每250
毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:jstat -gc 2764 250 20
jinfo:Java配置信息工具
实时查看和调整虚拟机各项参数
jinfo命令格式:jinfo [ option ] pid
jmap:Java内存映像工具
jmap(Memory Map for Java)命令用于生成堆转储快照
jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的
详细信息,如空间使用率、当前用的是哪种收集器等。
jmap命令格式:jmap [ option ] vmid
jhat:虚拟机堆转储快照分析工具
JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照
jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者
javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的
目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂
起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,
就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack命令格式:jstack [ option ] vmid
类加载机制
类加载过程和作用?
加载:类进入内存、这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。
验证:检测是否符合某种格式、确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求
准备:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段(赋'零'值)
- 解析:虚拟机将常量池中的符号引用替换为直接引用的过程
- 初始化:用户写的代码,按照用户的逻辑去运行。
- 使用
- 卸载
那种情况会触发类的初始化?
类第一次使用:
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
使用new关键字实例化对象的时候。
读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
调用一个类型的静态方法的时候。
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先
初始化这个主类。
当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解
析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句
柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有
这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
那种情况不会执行初始化
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
定义对象数组,不会触发该类的初始化。
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触
发定义常量所在的类。
通过类名获取 Class 对象,不会触发类的初始化。
通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初
始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
双亲委派
优先级:
各种类加载器之间的层次关系被称为类加载器的"双亲委派模型(ParentsDelegationModel)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型 :当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
保证了使用不同的类加载器最终得到的都是同样一个 Object 对象
tomcat破坏了双亲委派。