[关闭]
@Alpacadh 2022-09-19T23:16:11.000000Z 字数 8061 阅读 257

GC

JVM


1、什么是垃圾回收系统?

2、哪些内存区域需要回收?

3、如何判断对象是否可以回收?

Java 在垃圾回收之前,需要判断对象是否存活,只有死亡的对象才能被 GC 回收。常用的两种方式是:引用计数法和可达性分析。

4、GC Roots 对象有哪些?

1)虚拟机栈中引用的对象;

2)方法区中(1.8称为元空间)的类静态属性引用的对象;

3)方法区中的常量引用的对象;

4)本地方法栈中的JNI(native方法)引用的对象。

5、对象的回收过程?

JVM 在对 Java 对象回收阶段进行两次标记,一次筛选。

1)如果可达性分析算法发现对象没有在 GC ROOT 引用链中,进行第一次标记并筛选是否需要调用重写的 finalize() 方法。

2)筛选的依据是被回收对象是否重写过 finalize() 方法,且该重写方法并未被虚拟机调用过,不满足直接进行回收。

3)如果该对象有必要执行 finalize() 方法,该对象就会被放置在一个叫 F-Queue 的队列之中 ,并在稍后会由虚拟机自动创建一个低优先级的线程 Finalizer 线程去执行它。这个线程只会触发这个方法,不一定会等它结束,原因是防止一个 finalize() 方法在虚拟机中执行缓慢或死循环,导致 F-Queue 队列中其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将会对 F-Queue 队列中的对象进行第二次小规模的标记,如果对象在 finalize() 方法中重新与引用链上的任何一个对象建立联系,就可以拯救自己。

6、方法区能否被回收?

方法区的垃圾收集主要回收两部分,废弃的常量和不再使用的类型。

该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM 被允许对满足上述三个条件的无用类进行回收,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 还提供了 -Xnoclassgc 参数进行控制。

7、四种引用类型?

8、有哪些垃圾回收算法?

由于 Java 虚拟机规范中并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

常见的算法有新生代的复制算法,以及老年代的标记清除和标记整理算法。

![](https://secure2.wostatic.cn/static/4b6NAY4KKMh6xMDZSkfvCX/image.png)

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)。

9、新生代为划分空间的比例?

通常将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,其比例是 8:1:1。这是一个根据统计学得出的数据,新生代对象生存时间比较短,大约 80% 对象被回收。

10、常见的垃圾回收器

image.png-46.9kB

图中展示了 7 种作用于不同分代的收集器,分别是新生代收集器 Serial、ParNew、Parallel Scavenge,老年代收集器 CMS、Serial Old、Parallel Old 以及整堆收集器 G1。如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

11、CMS 是如何工作的?

CMS 收集器的运行过程分为下列 4 步:

1)初始标记阶段,标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。

2)并发标记阶段,进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。

3)重新标记阶段,为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,也存在 STW 问题。

4)并发清除阶段,对标记的对象进行清除回收。

![](https://secure2.wostatic.cn/static/yJPSuciE8XZy3WJaLW6WA/image.png)

CMS 收集器的缺点:

对 CPU 资源非常敏感。
无法处理浮动垃圾,可能出现 Concurrent Model Failure 失败而导致另一次 Full GC 的产生。
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次 Full GC。

12、G1 收集器是如何工作的?

如果不计算维护 Remembered Set 的操作,G1 收集器大致可分为如下步骤:

1)初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

2)并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

3)最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

4)筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

![](https://secure2.wostatic.cn/static/vGCEbsP67vQ7frSVt8GFu6/image.png)

13、触发 GC 的条件?

先来了解一下什么是 MinorGC(Young GC) 和 Major GC/FullGC。

1)MinorGC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,也叫Young GC。因为 Java 对象大多具备朝生夕死的特征,所以MinorGC非常频繁,一般回收速度也比较快。一般采用复制算法。

MinorGC 触发条件也很简单:当年轻代空间不足时,就会触发 MinorGC,注意这里的年轻代满指的是 Eden 区,Survivor 满不会引发 MinorGC。

同时 Minor GC 会引发 STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

2)Major GC

CMS 收集器中,当老年代满时会触发 Major GC。且目前只有 CMS 收集器会有单独收集老年代的行为。

3)Full GC

Full GC 对收集整堆(新生代、老年代)和方法区的垃圾收集。

当年老代满时会引发 Full GC,将会同时回收新生代、年老代 ;当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载。

调用System.gc时,系统建议执行Full GC,但是不一定会执行;
老年代空间不足;
方法区空间不足;
通过 Minor GC 后进入老年代的空间大于老年代的可用内存;
由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

14、什么是三色标记?

在垃圾回收算法中,标记总是必可少的一步。要找出存活对象,根据可达性分析,从 GC Roots 开始进行遍历访问,同时我们把遍历对象图过程中遇到的对象,按是否访问过这个条件标记成以下三种颜色:

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

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

1)初始时,所有对象都在白色集合中;

2)将 GC Roots 直接引用到的对象挪到灰色集合中;

3)从灰色集合中获取对象:将本对象引用到的其他对象全部挪到灰色集合中,最后将本对象本身挪到黑色集合里面;

重复步骤 3),直至灰色集合为空时结束。结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收。

15、什么是多标和漏标?

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

![](https://secure2.wostatic.cn/static/3hfMX75sLcgRDYhQxJ8Gc7/image.png)

假设 GC 线程已经遍历到 E(变为灰色了),此时应用线程先将 E 与 G 的引用关系断开,再让 D 引用到 G。切回GC线程继续执行回收算法时,因为 E 已经没有对 G 的引用了,所以不会将 G 放到灰色集合;尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。最后导致 G 被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。

16、如何解决漏标问题?

从代码角度看,漏标只有同时满足以下逻辑时才会发生:

var G = objE.fieldG; // 1. 读
objE.fieldG = null; // 2. 写
objD.fieldG = G; // 3. 写
1)读取灰色对象的成员变量属性值 ref;
2)将灰色对象对应属性设置为 null;
3)将属性 ref 赋值给黑色对象。

不难看出,只要在上面这三步中的任意一步中做一些手脚,通过读写屏障将引用对象记录起来,然后作为灰色对象再进行遍历即可。比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再对该集合的对象遍历即可(重新标记)。

重新标记通常是需要 STW 的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不完。

在现代垃圾回收器中,不同的收集器对漏标的处理方案有所不同:

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注