本文主要参考https://xiaozhuanlan.com/topic/1847690325,不足之处,请批评指正。
本文主要内容:
- Java内存区域
- 创建对象的过程
- 对象访问定位的方式
- 堆内存对象分配策略
- 强引用、软引用、弱引用、虚引用
- 垃圾回收算法
- 垃圾回收器
- Class文件介绍
- 类加载过程
- 双亲委派模式
一、介绍下Java内存区域
java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的内存区域:
JDK1.8后的,JVM不再有方法区,原来方法区存储的信息分成了两部分:1.虚拟机加载的类信息;2.运行时常量池。分别被移动到元空间和堆中。
1.程序计数器(线程私有):是一块较小的内存空间,可以看作是当前线程所执行的字节码的信号指示器。在虚拟机的概念模型里,字节码解释器的工作时就是用过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。各线程的计数器独立存储,互不影响。(总之两个作用:1.实现代码流程控制;2.多线程时记录程序执行位置。)
程序计数器是唯一不会出现OutOfMemoryError的内存区域,生命周期随线程产生,随线程结束而消亡。
2.虚拟机栈(线程私有):描述的是Java方法执行的内存模型:每个方法在执行的同事都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行结束,就对应着一个栈帧从入栈到出栈的过程,java方法的两种返回方式(return、抛出异常)也都会导致栈帧被弹出。生命周期与线程相同。
局部变量表主要存放了编译器克制的各种数据类型、对象引用。
虚拟机栈会出现两种异常:StackOverflowError,线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError,如果虚拟机可以动态扩展,而扩展时无法申请到足够的内存。
3.本地方法栈(线程私有):与虚拟机栈的作用类似,区别是虚拟机栈为虚拟机执行的java方法服务,为本地方法栈则为虚拟机使用到的Native方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
会有 StackOverflowError 和 OutOfMemoryError 异常。
4.堆(线程共享):这是JVM管理的内存中最大的一块,在虚拟机启动时创建好。此内存区域的唯一目的就是存放实例,几乎所有的对象实例都在这里分配内存,存放对象实例和数组。
Java堆是垃圾收集器管理的主要内存区域,因此也成为GC堆。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
5.方法区(线程共有):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
6.运行时常量池(线程共有):是方法区的一部分,存放编译期生成的各种字面量和符号引用。Class文件中除了存有类的版本、字段、方法、接口的等描述信息,还有一项是常量池,存有这个类的编译期生成的各种字面量和符号引用。
JDK1.8后,JVM不再有方法区,运行时常量池移到了堆中。
7.元空间Metaspace(线程共有):存储已被虚拟机加载的类信息。JVM1.8将原方法区储存的信息分为了:虚拟机加载的类信息、运行时常量池,其中虚拟机加载的类信息移动到了元空间中。
8.直接内存(线程共有):非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。
在JDK1.4中新加入的NIO,引入了一种基于通道(Channel)和缓存(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。可以避免在Java堆和Native堆中来回的数据耗时操作。
可能出现OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。
二、对象的创建过程
1.类加载检查:虚拟机遇到一条new指令时,首先会检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否一被加载过、解析和初始化过。如果没有,就必须先执行相应的类加载过程。
2.分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的动作等同于把一块确定大小的内存从Java堆中划分出来。分配内存的方式有指针碰撞和空闲列表两种,由Java堆是否规整决定,Java堆是否规整由所采取的垃圾收集器是否有压缩整理功能决定。
分配内存的方式:
(1).指针碰撞:如果Java堆中的内存是排列规整的,所有被用过的内存放一边,空闲的可用内容放一边,中间放置一个指针作为它们的分界点,在需要为新生对象分配内存时,只要将指针向空闲内存那边挪动一段与对象大小相等的距离即可分配。
(2).空闲列表:如果Iava堆中内存不是规整排列的,用过的内存和可用的内存相互交错,java虚拟机需要维护一个列表用于记录哪些内存是可用的,在为新生对象分配内存的时候,在列表中寻找一块足够大的内存分配,并更新列表上的记录。
3.初始化零值:内存分配完毕后,虚拟机需要将分配的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序访问到这些字段的数据类型所对应的零值。
4.设置对象头:初始化零值完成后,虚拟机要对对象进行必要的设置,对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息都存放在对象头中。另外根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5.执行init方法:按照成员的意愿初始化对象,至此,一个真正可用的对象才算产生。
三、对象的访问定位方式有哪两种方式
Java程序需要通过栈上的reference数据来操作堆上的具体对象。对象的访问方式取决于虚拟机的实现,主流的访问方式有两种:使用句柄、直接指针。
1.句柄:使用句柄时,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
优势:使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例指针数据,而reference本身不需要修改。
2.直接指针:使用直接指针访问,Java堆对象的布局居中就必须考虑如何放置访问类型数据的相关信息,为reference中存储的直接就是对象的地址。
优势:使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
四、谈谈堆内存中对象的分配的基本策略
在JDK1.7及以前,堆内存通常被分为三块区域:新生代:老年代、永久代。
新生代又分为Eden区、From Survivor区(S0)、To Survivor区(S1)。默认8:1:1。
在JDK1.8后,存放元数据的永久区从堆内存中移到了本地内存(native memory)中。JDK1.8提供了新的设置的Metaspace(元空间)内存大小的参数,通过这个参数可以设置metaspace内存大小,这样可以根据项目的实际情况,避免浪费本地内存。
大部分情况,对象都首先在Eden渔区分配,在于一次新生代垃圾回收后,如果对象还存活,则进入S0或S1,并且对象的年龄加一,当年龄增长到一定程度(默认15岁),就会被晋升到老年代中。与之可以通过-XX:MaxTenuringThreshold
来设置。
另外,大对象和长期存活的对象会直接进入老年代。
五、Minor GC和Full GC有何不同
大多数情况下,对象在新生代中eden区分配,当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,Minor Gc非常频繁,回收速度一般也很快。
老年代GC(Major GC):指发生在老年代的垃圾收集动作,出现了Major GC经常伴随着至少一次Minor GC(并不绝对),Major GC的速度一般比Minor GC慢十倍以上。
六、如何判断对象是否死亡
堆中集合放着所有的对象实例,堆堆垃圾回收钱首先要判断哪些对象已经死亡(即不再被任何途径使用的对象)。
1.引用计数器算法:
给对象添加一个引用计数器,每当有一个地方引用它时,引用计数器加一,当引用失效时,计数器减一,任何时刻对象的引用计数器为0的对象就是不可能再被使用的。
2.可达性算法分析:
这个算法的基本思想就是通过一系列的被称为:GC Roots的对象作为起点,从这个节点开始往下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可用的。
七、介绍下强引用,软引用,弱引用,虚引用
JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据存储的数值代表的是另一块内存的其实地址,就称这块内存代表一个引用。
JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用(引用强度逐渐递减)
1.强引用:我们使用的大部分引用都是强引用,使用强引用可以直接访问目标对象。强引用所指的目标对象在任何时候都不会被垃圾回收器回收。JVM宁愿抛出OOM异常,也不会回收强引用所指的对象。
2.软引用:软引用的强度仅次于强引用,如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它,吐过内存空间不够了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的告诉缓存。
3.弱引用:弱引用的对象拥有更加短暂的生命周期。垃圾回收器扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
4.虚引用:虚引用,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
八、如何判断一个常量是废弃常量
运行时常量池主要回收的是废弃的常量。
假如在常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,就说明“abc”就是废弃常量,如果此时发生内存回收且有必要的话,“abc”就会被系统清理出常量池。
九、如何判断一个类是无用的类
方法区主要回收的是无用的类。
类需要同时满足下面3个条件才能算是“无用的类”:
1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就必然会回收。
十、HotSpot为何要分新生代和老年代
主要是为了提升GC效率,接下来的分代收集算法可以很好的解释这个问题。
十一、介绍下有哪些垃圾回收算法
1.标记-清除算法
算法可分为“标记”和“清除”两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收被标记的对象。它时最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题。
(1).效率问题
(2).空间问题(标记后悔产生大量不连续的碎片)
2.复制算法
为解决效率问题,复制收集算法出现了,它将内存分为大小相同的两块,每次使用其中一块。当这一块内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使得每次的内存回收都是对内存区间的一半进行回收。
3.标记整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量的对象死去,所以可以选择复制算法,只需要付出少量的对象复制成本就可以完成每次垃圾收集。而老年代的对象成活率是很高的,而且没有额外的空间对它进行分配担保,所以我们必须选择标记清除或者标记整理算法进行垃圾收集。
十二、常见的垃圾回收器有哪些
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为知道现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。
1.Serial收集器
Serial(串行)收集器是最基本,历史最悠久的垃圾收集器。是单线程收集器,它不仅只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作时必须赞同其他所有的工作线程,直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法。
后续的垃圾收集器设计中停顿时间在不断缩小(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍在继续)。
Serial收集器的优点是简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有现成交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。
2.ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等)和Serial完全一样。
新生代使用复制算法,老年代采用标记-整理算法。
它时许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集配合工作。
3.Parllel Scavenge收集器
Parallel Scavenge 收集器类似于ParNew 收集器。 那么它有什么特别之处呢?
-XX:+UseParallelGC
使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC
使用Parallel收集器+ 老年代并行
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代采用标记-整理算法。
4.Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集。主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
5.Parallel Old收集器
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
4.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合在注重用户体验的应用上使用。
CMS收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
CMS收集器是由标记-清除算法实现的,它的运作过程相比于前几种垃圾收集器,更加复杂。分为四步:
- 初试标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除:开启用户线程,同时GC线程开始对为标记的区域做清扫。
主要优点:并发收集、低停顿
缺点:对CPU资源敏感、无法处理浮动垃圾、使用的标记-清除算法会导致收集结束时会有大量空间碎片产生。
5.G1收集器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的及其,以极高概率满足GC停顿时间要求的同事,还具备高吞吐量性能特征。
被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。具备以下优点:
- 并发与并行:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
- 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
G1收集器的运作大致分为以下几个步骤:初试标记、并发标记、最终标记、筛选回收
十三、介绍下类文件结构
Class文件是一组以8字节为基础单位的二进制流,各个数据醒目严格按照顺序紧凑排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节 进行存储。
Class文件格式采用一种类似于C语言结构体来存储数据,其中只有两种数据类型:无符号数、表;
无符号数属于基本的数据类型,以u1、u2、u4和u8来分别代表1字节、2字节、4字节和8字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表示由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯以“_info”结尾;
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这是称这一系列连续的某一类型的数据为某一类的集合。
类文件结构图:
Class文件字节码结构组织示意图:
Class文件结构的相关组件介绍:
- 魔数:确定这个文件是否为一个能被虚拟机接收的Class文件。
- Class文件版本:Class文件的版本号,保证编译正常执行。
- 常量池:常量池主要存放两大常量:字面量和符号引用
- 访问标志:标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否为public或者abstract类型,如果是类的话是否声明为final等等。
- 当前类索引,父类索引:类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java语言的单继承性,所以父类索引只有一个,除了java.lang.Object之外,所有的java类都有父类,因此,除java.lang.Object外,所有java类的父类索引都不为0.
- 接口索引集合:接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements(如果这个类本身是接口则是extends)后的接口顺序从左往右排列在接口索引集合中。
- 字段表集合:描述接口或者类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
- 方法表集合:类中的方法
- 属性表集合:在Class文件中,字段表、方法表都可以携带自己的属性表集合
十四、介绍类的加载过程
类加载过程:加载——连接(验证—准备—解析)——初始化——使用——卸载
十五、加载做了什么
类加载过程的第一步主要完成这三件事:
1.通过全类名获取定义此类的二进制流
2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3.在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口
虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass()
方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
十六、有哪些类加载器
JVM中内置了三个重要的ClassLoader,除了BootstrapClassLoader,其他类加载器均由Java实现且全部继承自java.lang.ClassLoader:
- BootstrapClassLoader(启动类加载器):最顶层的类加载器,有C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类或者被-Xbootclasspath参数指定的路径中的所有类。
- ExtensionClassLoader(扩展类加载器):主要负责加载目录%JRE_HOME%/lib/ext目录下的jar包和类,或被java.ext.dirs系统变量锁指定的路径下的jar包。
- AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
十七、介绍下双亲委派模式
1.双亲委派模式介绍:
每个类都有一个对应的类加载器,系统中的ClassLoader在协同工作时会默认使用双亲委派模型。即在类加载时,系统会首先判断此类是否被加载过,已经被加载过则返回,否则才会尝试加载。加载的时候,首先会把请求委派给父类加载器的loadClass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器BootstrapClassLoader中,当父类无法处理时,由自己处理。当父类加载器为null时,才会使用启动类加载器BootstrapClassLoader作为父类加载器。
其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mather ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。官方API文档对这部分的描述如下:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself.
源码分析:双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中
private final ClassLoader parent;
protected 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) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
十八、双亲委派模式的优势
双亲委派模式包含智能了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API 不被篡改。如果不使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
十九、不想用双亲委派模式了怎么办
为了避免双亲委派模式,我们可以自定义一个类加载器,然后重载loadClass()即可
二十、如何自定义类加载器
需要继承ClassLoader