4

美团 Java 最新面试题(JVM与调优)-(2)

 2 years ago
source link: https://zhukqian.github.io/2019/11/15/%E7%BE%8E%E5%9B%A2%E6%9C%80%E6%96%B0%E9%9D%A2%E8%AF%95%E9%A2%98%E8%AF%A6%E7%BB%86%E8%AE%B2%E8%A7%A3-JVM%E4%B8%8E%E8%B0%83%E4%BC%98/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1:Java 类加载过程?

1、加载:查找和导入Class文件

2、链接:其中解析步骤是可以选择的 (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用

3、初始化:对静态变量,静态代码块执行初始化工作

https://www.cnblogs.com/luohanguo/p/9469851.html

2:描述一下 JVM 加载 Class 文件的原理机制?

Java语言是一种具有动态性的解释型语言,类(class)只有被加载到JVM后才能运行。当运行指定程序时,JVM会将编译生成的.class文件按照需求和一定的规则加载到内存中, 并组织成为一个完整的Java应用程序。这个加载过程是由类加载器完成,具体来说,就是由ClassLoader和它的子类来实现的。类加载器本身也是一个类,

其实质是把类文件从硬盘读取到内存中。

类的加载方式分为隐式加载和显示加载。 隐式加载指的是程序在使用new等方式创建对象时,会隐式地调用类的加载器把对应的类加载到JVM中。显示加载指的是通过直接调用class.forName()方法来把所需的类加载到JVM中。

任何一个工程项目都是由许多类组成的,当程序启动时,只把需要的类加载到JVM中,其他类只有被使用到的时候才会被加载,采用这种方法一方面可以加快加载速度,另一方面可以节约程序运行时对内存的开销。此外,在Java语言中,每个类或接口都对应一个.class文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。

在Java语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到JVM中,至于其他类,则在需要的时候才加载。

3:Java 内存分配。

Java程序运行在JVM(Java Virtual Machine,Java虚拟机)上,可以把JVM理解成Java程序和操作系统之间的桥梁,JVM实现了Java的平台无关性。

寄存器:JVM内部虚拟寄存器,存取速度非常快,程序不可控制。
栈:保存局部变量的值:包括1.基本数据类型的值。2.保存类的实例,即堆区对象的引用(指针)。3.保存加载方法时的帧。
堆:用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类拥有各自的成员变量,存储在堆中的不同位置,但是同一个类不同实例的他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。
常量池:JVM为每个加载的类型维护一个常量池,常量池是这个类型用到的常量的集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用(1)。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于方法区中,而方法区存在于堆中。

4:GC 是什么? 为什么要有 GC?

GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,

Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

GC是垃圾收集器。Java 程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:

System.gc() 
Runtime.getRuntime().gc() 

Java是由C++发展来的。

它摈弃了C++中一些繁琐容易出错的东西。其中有一条就是这个GC。

写C/C++程序,程序员定义了一个变量,就是在内存中开辟了一段相应的空间来存值。内存再大也是有限的,所以当程序不再需要使用某个变量的时候,就需要释放这个内存空间资源,好让别的变量来用它。在C/C++中,释放无用变量内存空间的事情要由程序员自己来解决。就是说当程序员认为变量没用了,就应当写一条代码,释放它占用的内存。这样才能最大程度地避免内存泄露和资源浪费。

但是这样显然是非常繁琐的。程序比较大,变量多的时候往往程序员就忘记释放内存或者在不该释放的时候释放内存了。而且释放内存这种事情,从开发角度说,不应当是程序员所应当关注的。程序员所要做的应该是实现所需要的程序功能,而不是耗费大量精力在内存的分配释放上。

Java有了GC,就不需要程序员去人工释放内存空间。当Java虚拟机发觉内存资源紧张的时候,就会自动地去清理无用变量所占用的内存空间。当然,如果需要,程序员可以在Java程序中显式地使用System.gc()来强制进行一次立即的内存清理。

因为显式声明是做堆内存全扫描,也就是 Full GC,是需要停止所有的活动的(Stop The World Collection),你的应用能承受这个吗?而其显示调用System.gc()只是给虚拟机一个建议,不一定会执行,因为System.gc()在一个优先级很低的线程中执行。

5:简述 Java 垃圾回收机制

https://cloud.tencent.com/developer/article/1517916

Java的垃圾回收,主要是针对JVM内存中的堆空间。

6:如何判断一个对象是否存活?(或者 GC 对象的判定方法)

 堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象)

1、引用计数算法(Reference Counting)

很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1; 任何时刻计数器都为0的对象就是不可能再被使用的。

引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存, 其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。

例如: 在testGC()方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外这两个对象再无任何引用,实际上这两个对象都已经不能再被访问,但是它们因为相互引用着对象方,异常它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

2、可达性分析算法(GC Roots Analysis):主流用这个判断

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点, 从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,

在Java语言里,可作为GC Roots对象的包括如下几种: a.虚拟机栈(栈桢中的本地变量表)中的引用的对象 b.方法区中的类静态属性引用的对象 c.方法区中的常量引用的对象 d.本地方法栈中JNI的引用的对象

3、finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。   1).第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

2).第二次标记 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。 Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己—-只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

可以在Eclipse设置GC日志输出

java就是可达性分析算法+finalize方法得出是否该死亡

7:垃圾回收的优点和原理。并考虑 2 种回收机制

1.java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题。

2.由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。

3.垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。

4.垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。

5.程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。

6.垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。

8:垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

基本原理(对象引用遍历方式):

对于GC(垃圾收集)来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。 通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。 当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。

垃圾回收器不可以马上回收内存。 垃圾收集器不可以被强制执行,但程序员可以通过调研System.gc方法来建议执行垃圾收集。 记住,只是建议。一般不建议自己写System.gc,因为会加大垃圾收集工作量

程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

9:Java 中会存在内存泄漏吗,请简单描述

理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象, 这些对象不能被GC回收也会发生内存泄露。 一个例子就是hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象。

10:深拷贝和浅拷贝

深拷贝和浅拷贝的区别

1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

为什么要使用深拷贝?

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

深拷贝的要求程度

我们在使用深拷贝的时候,一定要弄清楚我们对深拷贝的要求程度:是仅“深”拷贝第一层级的对象属性或数组元素,还是递归拷贝所有层级的对象属性和数组元素?

11:System.gc() 和 Runtime.gc() 会做什么事情?

java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同 System.gc()和runtime.gc()用于提示jvm进行垃圾回收,但是否立即回收还是延迟回收由java虚拟机决定

12:finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?

1、finalize()用在当垃圾回收器,因内存紧张,而去回收某些对象时,这时候会去调用其finalize()方法;而如果内存不紧张,就不会去回收对象,那finalize()就不会被调用;

 但是呢,考虑到JNI(java native interface),有时候finalize()就可以去回收这部分的内存;

参考:《深入理解Java虚拟机》

对于Java而言:

调用时机:当垃圾回收器要宣告一个对象死亡时,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有和GC Roots相连接的引用链,就会被第一次标记,并且判断是否执行finalizer( )方法,如果对象覆盖finalizer( )方法且未被虚拟机调用过,那么这个对象会被放置在F-Queue队列中,并在稍后由一个虚拟机自动建立的低优先级的Finalizer线程区执行触发finalizer( )方法,但不承诺等待其运行结束。 finalization的目的:对象逃脱死亡的最后一次机会。(只要重新与引用链上的任何一个对象建立关联即可。)但是不建议使用,运行代价高昂,不确定性大,且无法保证各个对象的调用顺序。可用try-finally或其他替代。

1)垃圾回收器(garbage collector)决定回收某对象时,就会运行该对象的finalize()方法;

2)GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。

首先根据可达性算法分析是否有与GC ROOT相连接的引用连,没有就会被标记,然后判断是否执行过finalize()方法,如果执行过就加入F-Queue队列

finalization是逃脱死亡的最后一次机会,只要重新与引用链上的任何一个对象建立关联即可.但不建议使用

13:如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?

不会立即释放对象占用的内存。 如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,而垃圾收集器是运行在后台的线程, 只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,这时候仍然不会立即释放该对象内存, 因为有些对象是可恢复的(在 finalize方法中恢复引用 )。只有确定了对象无法恢复引用的时候才会清除对象内存。

14:什么是分布式垃圾回收(DGC)?它是如何工作的?

RMI使用DGC来做自动垃圾回收。因为RMI包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的。DGC使用引用计数算法来给远程对象提供自动内存管理。

概念:
1)Java虚拟机中,一个远程对象不仅会被本地虚拟机内的变量引用,还会被远程引用。
2)只有当一个远程对象不受到任何本地引用和远程引用,这个远程对象才会结束生命周期。

说明:  
    1)服务端的一个远程对象在3个地方被引用:  
        1>服务端的一个本地对象持有它的本地引用  
        2>服务端的远程对象已经注册到rmiregistry注册表中,也就是说,rmiregistry注册表持有它的远程引用。  
        3>客户端获得远程对象的存根对象,也就是说,客户端持有它的远程引用。  
    2)服务端判断客户端是否持有远程对象引用的方法:  
        1>当客户端获得一个服务端的远程对象的存根时,就会向服务器发送一条租约(lease)通知,以告诉服务器自己持有了这个远程对象的引用了。  
        2>客户端定期地向服务器发送租约通知,以保证服务器始终都知道客户端一直持有着远程对象的引用。  
        3>租约是有期限的,如果租约到期了,服务器则认为客户端已经不再持有远程对象的引用了。

15:串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

串行GC:整个扫描和复制过程均采用单线程的方式,相对于吞吐量GC来说简单;适合于单CPU、客户端级别。

吞吐量GC:采用多线程的方式来完成垃圾收集;适合于吞吐量要求较高的场合,比较适合中等和大规模的应用程序。

吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。

而串行收集器对大多数的小应用(在现代处理器上需要大概100M左右的内存)就足够了。

16:在 Java 中,对象什么时候可以被垃圾回收?

当一个对象到GC Roots不可达时,在下一个垃圾回收周期中尝试回收该对象,如果该对象重写了finalize()方法,并在这个方法中成功自救(将自身赋予某个引用),那么这个对象不会被回收。但如果这个对象没有重写finalize()方法或者已经执行过这个方法,也自救失败,该对象将会被回收。

17:简述 Java 内存分配与回收策率以及 Minor GC 和 Major GC。

  1. 栈区:栈可分为Java虚拟机和本地方法栈

  2. 堆区:堆被所有线程共享,在虚拟机启动时创建,是唯一的目的是存放对象实例,是gc的主要区域。通常可分为两个区块年轻代和年老代。更新一点年轻代可分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存 gc 后幸存下的对象,默认情况下各自占比 8:1:1。

  3. 方法区:被所有线程共享,用于存放已被虚拟机加载的信息,常量,静态变量等,是Java虚拟机描述为堆的一个逻辑部分。习惯被称为永久代。

  4. 程序计数器:是当前线程所执行的行号指示器,跳转指令等都依赖这个完成,线程私有。

回收策略以及Minor GC 和 Major GC(Full GC)

  1. 对象优先在堆区的Eden区分配。

  2. 大对象直接进入老年代。

  3. 长期存活的对象直接进入老年代。

回收:当Eden区没有足够的空间分配时,虚拟机会执行一次Minor GC .Minor GC通常发生在Eden新生代,因为这个区的对象生存期短,发生频率高,回收速度快。Major GC发生在老年代,一般触发老年代的GC不会触发Minor GC ,但是通过配置,可以在之前进行一次Minor GC,能加快老年代的回收速度

18:JVM 的永久代中会发生垃圾回收么?

永生代也是可以回收的,条件是

1.该类的实例都被回收。

2.加载该类的classLoader已经被回收

3.该类不能通过反射访问到其方法,而且该类的java.lang.class没有被引用

当满足这3个条件时,是可以回收,但回不回收还得看jvm。

19:Java 中垃圾收集的方法有哪些?

分代划分内存介绍

整个JVM内存总共划分为三代:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)

1、年轻代:所有新生成的对象首先都放在年轻代内存中。年轻代的目标就是尽可能快速的手机掉那些生命周期短的对象。 年轻代内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior.当回收时,将Eden和Survior中还存活的对象一次性拷贝到另外一块Survior空间上, 最后清理Eden和刚才用过的Survior空间。

2、年老代:在年轻代经历了N次GC后,仍然存活的对象,就会被放在老年代中。因此可以认为老年代存放的都是一些生命周期较长的对象。

3、持久代:基本固定不变,用于存放静态文件,例如Java类和方法。持久代对GC没有显著的影响。持久代可以通过-XX:MaxPermSize=进行设置。

一、引用计数算法(Reference Counting)

介绍:给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的即可被回收。

优点:实现简单,判断效率高

缺点:很难解决对象之间的相互循环引用(objA.instance = objB; objB.instance = objA)的问题,所以java语言并没有选用引用计数法管理内存

二、根搜索算法(GC Root Tracing)

Java和C#都是使用根搜索算法来判断对象是否存活。通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。

在Java中哪些对象可以成为GC Root?

虚拟机栈(栈帧中的本地变量表)中的引用对象

方法区中的类静态属性引用的对象

方法区中的常量引用对象

本地方法栈中JNI(即Native方法)的引用对象

三、标记-清除算法(Mark-Sweep)

这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记那些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:

1.效率不高,标记和清除的效率都很低;

2.会产生大量不连续的内存碎片,导致以后程序在分配交大的对象时,由于没有充足的连续内存而提前触发一次GC动作。

四、复制算法(Copying)

为了解决效率问题,复制算法将可用内存按容量划分相等的两部分,然后每次只使用其中的一块,当第一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,在将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一块内存。

于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大的那份内存叫Eden区,其余两块较小的内存叫Survior区。每次都会先使用Eden区,若Eden区满,就将对象赋值到第二块内存上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制赋值到老年代中。(java堆又分为新生代和老年代)。

五、标记-整理算法(Mark-Compact)

该算法是为了解决标记-清楚,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收的对象移动一端,然后清除掉端边界以为的对象,这样就不会产生内存碎片。

六、分代收集算法(Generational Collection)

根据对象的存活周期的不同将内存划分为几块,一般就分为新生代和老年代,根据各个年代的特点采用不同的收集算法。新生代(少量存活)用复制算法,老年代(对象存活率高)“标记-清理”算法

20:什么是类加载器,类加载器有哪些?

类加载器按照层次,从顶层到底层,分为以下三种:

(1)启动类加载器(Bootstrap ClassLoader)

这个类加载器负责将存放在JAVA_HOME/lib下的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。

(2)扩展类加载器(Extension ClassLoader)

这个加载器负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器

(3)应用程序类加载器(Application ClassLoader)

这个加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Classpath)上所指定的类库,可直接使用这个加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器

双亲委派模型:

双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是都使用组合关系来复用父加载器的代码

工作过程:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,判断两个类是否相同是通过classloader.class这种方式进行的,所以哪怕是同一个class文件如果被两个classloader加载,那么他们也是不同的类。

21:类加载器双亲委派模型机制?

1.极其重要的一个方法 loadClass

protected Class<?> loadClass(String name, boolean resolve)

        throws ClassNotFoundException

    {

        synchronized (getClassLoadingLock(name)) {

            // First, check if the class has already been loaded

            //findLoadedClass() 是去内存中查找是否加载过这个名字为 name的类,上面的注释是源码中的注释,也是在说 检查这个类是否已经被加载了

            Class<?> c = findLoadedClass(name);

            if (c == null) {

                long t0 = System.nanoTime();

                try {

                      //parent为classload类中的属性,表示它的父加载器,如果父加载器不为空则调用父类的loadClass方法

                     //可以想到如果当前类加载器没有在内存中找到该类,那么它的父类加载器就会去内存中寻找是否加载了该类

                      if (parent != null) {

                        c = parent.loadClass(name, false);

                    } else {

                        //如果父加载器为空,那么调用BootstrapClassLoader去寻找该类,这也证明了BootstrapClassLoader在java中是

                       //以null的形式存在的

                        c = findBootstrapClassOrNull(name);

                    }

                } catch (ClassNotFoundException e) {

                    // ClassNotFoundException thrown if class not found

                    // from the non-null parent class loader

                }

 

                if (c == null) {

                   //如果还是没有找到,那么调用findClass去查找 也就是从上面提到的路径中找

                    // If still not found, then invoke findClass in order

                    // to find the class.

                    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;

        }

    }


我把上述过程总结一下:

!(https://images2018.cnblogs.com/blog/1256203/201807/1256203-20180714171531925-1737231049.png)[]

双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此, 因此所有的类加载请求最终都会传送到顶端的启动类加载器; 只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。 这里有几个流程要注意一下:

子类先委托父类加载
父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类
子类在收到父类无法加载的时候,才会自己去加载

https://www.cnblogs.com/joemsu/p/9310226.html

比如:类隔离,在一个项目中实用不容版本的jar包



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK