前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试官:垃圾回收算法用的多吗?

面试官:垃圾回收算法用的多吗?

作者头像
麦洛
发布2021-08-23 13:54:37
3530
发布2021-08-23 13:54:37
举报

一期一图

我觉得这个图有问题,但是逻辑分析完没问题

前言

Java虚拟机是一种抽象化的计算机,作用是可以在多种平台上不加修改地运行。

Java虚拟机通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。

Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码,就可以在多种平台上不加修改地运行。

正文

JVM内存模型

JVM 内存区域主要分为线程私有区域:程序计数器、虚拟机栈、本地方法区;线程共享区域:JAVA 堆、方法区;直接内存

线程私有区域

线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 HotspotVM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的"存/否"跟随本地线程的"生/死"对应)。

程序计数器

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,当前线程所执行的行号指示器(当前指令的地址)。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。

程序计数器存储的是字节码文件的行号,行号是分配的前提,在一开始分配内存时就可以分配一个绝对不会溢出的内存(不存在OutOfMemoryError)。

虚拟机栈

描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

本地方法区

本地方法区和 Java Stack 作用类似虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务。

线程共享区域

堆区(Heap)--运行时数据区

创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。从GC的角度堆区细分为年轻代和老年代,其中年轻代又分为Eden、S0、S1 三个部分,他们默认的比例是8:1:1的大小。

元空间

在Java1.7之前,包含方法区的概念,常量池就存在于方法区(永久代)中,⽽⽅法区本身是⼀个逻辑上的概念,在1.7之后则是把常量池移到了堆内,1.8之后移出了永久代的概念(⽅法区的概念仍然保留),实现方式则是现在的元数据。它包含类的元信息和运行时常量池。

Class文件就是类和接口的定义信息。

运行时常量池就是类和接⼝的常量池运⾏时的表现形式

直接内存

直接内存并不是 JVM 运行时数据区的一部分, 但在部分IO中它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作,这样就避免了在 Java堆和 Native 堆中来回复制数据。

对象已死?

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。

引用计数算法 (Reference Counting)

算法思路:

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为零的对象就是不可能再被使用的。

虽然占用了额外的内存空间来进行计数,但它的原理简单,判定效率很高。

但是,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,其中很难解决对象之间相互循环引用的问题。

可达性分析算法 (Reachability Analysis)

算法思路:

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的(可回收的)。

可以作为GC ROOT的对象包括:

1.虚拟机栈中引用的对象。我们程序在虚拟机的栈中执行,每次函数调用调用都是一次入栈。在栈中包括局部变量表和操作数栈,局部变量表中的变量可能为引用类型(reference),他们引用的对象即可作为GC Root。不过随着函数调用结束出栈,这些引用便会消失。

2.是方法区中类静态属性引用的对象简单的说就是我们在类中使用的static声明的引用类型字段

代码语言:javascript
复制
Class Dog {
    private static Object tail;
}

3.方法区中常量引用的对象简单的说就是我们在类中使用final声明的引用类型字段

代码语言:javascript
复制
Class Dog {
    private final Object tail;
}

4.本地方法栈中引用的对象就是程序中native本地方法引用的对象。

再谈引用

把一个对象赋给一个引用变量,这个引用变量就是一个强引用。即类似“Objectobj=new Object()”当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

垃圾回收算法

标记-清除

算法原理:

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。

缺点:

第一个是执行效率不稳定,需要清理的对象多是,标记,清理的效率太低。

第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次GC.

标记-复制

标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。

算法原理:

“半区复制”(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效。

缺陷:

复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费太多。

标记-整理

算法原理:

针对老年代再用复制算法显然不合适,因为进入老年代的对象存活率都比较高了,这时候再频繁的复制,对性能影响就较大,而且也不会再有另外的空间挪用,所以针对老年代的特点,通过标记-整理算法,标记出所有存活对象,让所有存活的对象都向一端移动,然后清理掉边界以外的内存空间。

比较:

指标

标记-清理

标记-整理

标记-复制

速率

中等

最慢

空间

少,堆积碎片

少,不堆积碎片

需要活对象的2倍,不堆积

移动对象

三色标记法和读写屏障

我们把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记为三种颜色:

白色:尚未访问过。黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。

假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:

  1. 初始时,所有对象都在 【白色集合】中;
  2. 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
  3. 从灰色集合中获取对象: 3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中; 3.2. 将本对象 挪到 【黑色集合】里面。
  4. 重复步骤3,直至【灰色集合】为空时结束。
  5. 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。

注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。

当Stop The World (以下简称 STW)时,对象间的引用 是不会发生变化的,可以轻松完成标记。

而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化多标漏标的情况就有可能发生。

多标-浮动垃圾

假设已经遍历到E(变为灰色了),此时应用执行了 objD.fieldE = null :

此刻之后,对象E/F/G是“应该”被回收的。然而因为E已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存

这部分本应该回收 但是 没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。

另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障

假设GC线程已经遍历到E(变为灰色了),此时应用线程先执行了:

代码语言:javascript
复制
var G = objE.fieldG; 
objE.fieldG = null;  // 灰色E 断开引用 白色G 
objD.fieldG = G;  // 黑色D 引用 白色G

此时切回GC线程继续跑,因为E已经没有对G的引用了,所以不会将G放到灰色集合;尽管因为D重新引用了G,但因为D已经是黑色了,不会再重新做遍历处理。

最终导致的结果是:G会一直停留在白色集合中,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。

漏标只有同时满足以下两个条件时才会发生:

条件一:灰色对象 断开了 白色对象的引用(直接或间接的引用);即灰色对象 原来成员变量的引用 发生了变化。

条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。

解决办法:

将对象G存储到特定集合中,等并发标记遍历完毕后再对集合中对象进行重新标记。

分代收集算法

Java堆分为新生代和老年代,根据每个年代的特点,采用不同的收集算法。

当一个新的对象来申请内存空间的时候,如果Eden区无法满足内存分配需求,则出发YGC,使用中的Survivor区和Eden区存活对象送到未使用的Survivor区,如果YGC后还放不下,则直接进入老年代,如果老年代也无法分配空间,出发FGC,FGC后还是放不下,则报出OOM异常。

老年代使用标记-回收算法、标记-整理算法。

新生代使用复制算法。

年轻代存在的意义:

分代的好处就是优化GC性能,如果没有分代每次扫描所有区域频繁的GC,98%的对象都是朝生夕死,所以,实际上存活的对象并不是很多,完全用不到一半内存的浪费。

年轻代

年轻代用来存放新近创建的对象,尺寸随堆大小的增加和减少而相应的变化,默认值是保持为堆的1/15。 年轻代的大小可以通过-xmn设置固定大小,也可以通过-xx:newratio设置年轻代和年老代的比例。 年轻代中存在的对象是死亡非常快的。存在朝生夕死的情况。 所以为了提高年轻代的垃圾回收效率,又将年轻代划分为三个区域,一个eden和两个sunrvivor from。

eden和survivor默认比例是8:1:1,进行垃圾回收采用的是分代复制算法。每次新生代的使用,会是eden区和一块survivor区。

当我们进行垃圾回收的时候,清除正在使用的区域,将其中的存货对象,放入到另一个survivor区域,并进行整理,保证空间的连续。如果对象长时间存活,则将对象移动到老年区。存活下来的对象,年龄会增长1。当对象的年龄一次次存活,一次次增长,到达15的时候,这些对象就会移步到老年代。

在年轻代执行gc的时候,如果老年代的连续空间小于新生代对象的总大小,就会触发一次full gc。是为了给新生代做担保,保证新生代的老年对象可以顺利的进入到老年代的内存区。

老年代

老年代中存放的对象是存活了很久的,年龄大于15的对象。在老年代触发的gc叫major gc也叫full gc。full gc会包含年轻代的gc。但老年代只要执行gc就一定是full gc。

full gc采用的是标记-清除算法。会产生内存碎片。在执行full gc的情况下,会阻塞程序的正常运行。老年代的gc比年轻代的gc效率上慢10倍以上。对效率有很大的影响。

CardTable

使用分代回收会面临一个问题,在进行Minor GC时,老年代是不用被回收的,则对新生代,除了在引用链上的对象不被回收以外,被老年代对象引用的对象也不被回收。若想找到这些对象,需要扫描整个老年代,这就大大增加了STW时间。

CardTable就是为了解决这一问题:

它以byte[]数组的形式记录老年代的某一块区域是否持有新生代的引用,卡表的数量取决于老年代的大小和每张卡对应的内存大小,每张卡在卡表中对应一个比特位,当老年代中的某个对象持有了新生代对象的引用时,JVM就把这个对象对应的Card所在的位置标记为dirty(bit位设置为1),这样在Minor GC时就不用扫描整个老年代,而是扫描Card为Dirty对应的那些内存区域。

元空间

-XX:MetaspaceSize 初始空间的大小。达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

永久代的回收会随着full gc进行移动,消耗性能。每种类型的垃圾回收都需要特殊处理元数据。将元数据剥离出来,简化了垃圾收集,提高了效率。

什么时候启动GC

Minor GC启动时机:Eden区域满了新创建的对象大小大于Eden所剩的空间

Full GC启动时机:主动调用System.gc(),老年代空间不足。

内存分配与回收

对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。

少数情况下也可能会直接分配在老年代中(大对象直接分到老年代),分配的规则并不是百分百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

对象优先在Eden分配

大多数情况下,对象会在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机会发起一次 Minor GC。Minor GC相比Major GC更频繁,回收速度也更快。

通过Minor GC之后,Eden区中绝大部分对象会被回收,而那些存活对象,将会送到Survivor的From区(若From区空间不够,则直接进入Old区) 。

Survivor区

Survivor区相当于是Eden区和Old区的一个缓冲,类似于我们交通灯中的黄灯。

Survivor又分为2个区,一个是From区,一个是To区。每次执行Minor GC,会将Eden区中存活的对象放到Survivor的From区,而在From区中,仍存活的对象会根据他们的年龄值来决定去向。

(From Survivor和To Survivor的逻辑关系会发生颠倒:From变To ,To变From,目的是保证有连续的空间存放对方,避免碎片化的发生)

Survivor存在的意义:

如果没有Survivor区,Eden区每进行一次Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次Minor GC没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。

Survivor的存在意义就是减少被送到老年代的对象,进而减少Major GC的发生。

Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

大对象直接进入老年代 所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来 “安置” 它们。 虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用的是复制算法)。 YFC之后,存活的对象将会被复制到未使用的Survivor区,如果s区放不下,则直接更换到老年代,而对于那些一直在Survivor区来回复制的对象,通过-XX: MaxTenuringThreshold配置交换阈值,默认15次,如果超过次数同样进入老年代。 还有一种动态年龄的判断机制,不需要MaxTenuringThreshold就能进入老年代。 如果在Survivor空间中相同年龄所有对象大小的总和大于Survive空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代。

GC垃圾收集器

Serial:单线程版本收集器,进行垃圾回收的时候STW(Stop The World),也就是进行垃圾回收的时候其他工作线程必须停止。

ParNew:Serial的多线程版本,用于和CMS配合使用。

Parallel Scavenge:可以并行收集的多线程垃圾收集器

Serial Old:Serial的老年代版本,也是单线程

Parallel Old:Parallel Scavenge的老年代版本

CMS

CMS(Concurrent Mark Sweep):CMS收集器是以获取最短停顿时间为目标的收集器,相对于其他的收集器STW的时间更短暂,可以并行收集是他的特点,同时他基于标记-清除算法,整个GC过程分为为4步。

1. 初始标记:标记GC ROOT能关联到的对象,需要STW

2. 并发标记:从GCRoot的直接关联对象开始遍历整个对象图的过程不需要STW

3. 重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生改变的标记,需要STW.

4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要STW从整个过程来看,并发标记和并发清除的时间最长,但不需要停止用户线程,而初始标记和重新标记的时间较短,但需要停止用户线程,总体而言,整个过程造成的停顿时间较短,大部分时候是可以和用户线程一起工作的。

G1(Garbage First):G1收集器是JDK9的默认垃圾收集器,而且不再区分老年代和年轻代进行区分。

G1原理

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:

1.基于标记-整理算法,不产生内存碎片。

2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。

区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

new个对象

当虚拟机遇见new关键字时候,实现判断当前类是否已经加载,如果类没有加载,首先执行类的加载机制,加载完成后再为对象分配空间、初始化等。

1. 首先校验当前类是否被加载,如果没有加载,执行加载机制

2. 加载:就是从字节码加载成二进制流的过程

3. 验证:当然加载完成之后,当然需要校验Class文件是否符合虚拟机规范,跟我们接口请求一样,第一件事当然是先做参数校验。

4. 准备:为静态变量,常量赋默认值

5. 解析:把常量池中符号引用(以符号描述引用的目标)替换为直接引用(指向目标的指针或者句柄等)的过程。

6. 初始化:执行static代码块(cinit)进行初始化,如果存在父类,先对父类进行初始化。

static代码块一定是线程安全的吗?

静态代码块绝对是线程安全的,只能隐式被java虚拟机在类加载过程中初始化调用

当类加载完成之后,就是对象分配内存空间和初始化的过程

1.首先为对象分配合适大小的内存空间

2.接着为实例变量赋默认值

3.设置对象头信息,对象hash码,GC分代年龄,元数据信息等

4.执行构造函数(init)初始化

双亲委派模型

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

类加载器

类加载器自顶向下分为:

1. Bootstrap ClassLoader启动类加载器:默认回去加载JAVA_HOME/lib目录下的jar

2. Extention ClassLoader扩展类加载器:默认去加载JAVA_HOME/lib/ext目录下的jar

3. Application ClassLoader应用程序类加载器:比如我们的web应用,会加载web程序中ClassPath下的类

4. User ClassLoader用户自定义类加载器:由用户自己定义

当我们在加载类的时候,首先都会向上询问自己的父加载器师范已经被加载,如果没有,则依次向上询问

如果没有加载,则从上到下依次尝试是否能加载当前类,直到加载成功

双亲委派机制的作用

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。

2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

巨人的肩膀

https://www.jianshu.com/p/12544c0ad5c1

https://www.processon.com/view/link/5e69db12e4b055496ae4a673

https://mp.weixin.qq.com/s/tmMpFmmCx2sWqpB_KibdQw

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 今日Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 标记-清除
  • 标记-复制
  • 标记-整理
  • 三色标记法和读写屏障
  • 分代收集算法
    • 什么时候启动GC
    • 对象优先在Eden分配
    • Survivor区
    • 大对象直接进入老年代 所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来 “安置” 它们。 虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用的是复制算法)。 YFC之后,存活的对象将会被复制到未使用的Survivor区,如果s区放不下,则直接更换到老年代,而对于那些一直在Survivor区来回复制的对象,通过-XX: MaxTenuringThreshold配置交换阈值,默认15次,如果超过次数同样进入老年代。 还有一种动态年龄的判断机制,不需要MaxTenuringThreshold就能进入老年代。 如果在Survivor空间中相同年龄所有对象大小的总和大于Survive空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代。
    • CMS
    • G1原理
    • 类加载器
    • 双亲委派机制的作用
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档