一、垃圾收集有哪些算法以及各自的特点
- 标记清除算法
- 复制算法
- 标记整理算法
- 分代收集算法
1.标记清除算法:将所有需要回收的对象进行标记,标记结束后对标记的对象进行回收。缺点是效率低,会造成大量的碎片。
2.复制算法:复制算法将空间分为两部分,每次使用其中的一部分。当一块内存用完了,就将这块的所有对象复制到另一块,将已使用的块清除。优点:不会产生碎片,缺点:会浪费一定的内存空间。在堆中的年轻代使用该算法,因为年轻代的对象多为生存周期比较短的对象。年轻代将内存分为一个 Eden,两个 Survivor。每次使用 Eden 与一个 Survivor。当回收时,将 Survivor 与 Eden 中存活的对象复制到另一个 Survivor,最后清理掉 Eden 与 Survivor。当 Survivor 与 Eden 中存活的对象大小超过另一个 Survivor,则需要老年代来担保。
3.标记整理算法:缺点:复制算法在对象存活率较高时,复制会使得效率降低。根据老年代的特点,使用标记整理算法。标记之后将所有存活的对象移向一端,将其他的整理。优点:解决了碎片问题。
4.分代收集算法:年轻代、老年代根据各自不同的特点采用不同的算法。
二、垃圾收集器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
1. Serial 收集器
Serial 翻译为串行,也就是说它以串行的方式执行,也就是单线程执行的,只会使用一个线程进行垃圾收集工作。
优点:简单高效,在单个 CPU 环境下,由于没有交互作用的开销,因此有最高的单线程收集效率。
缺点:由于是单线程的,因此在进行垃圾收集的时候,必须暂停其他所有的工作线程,直到垃圾收集结束。
使用的算法:新生代采用复制算法,老年代采用标记-整理算法。
2. ParNew 收集器
ParNew 收集器是 Serial 收集器的多线程版本,出了使用用多线程进行垃圾收集外,其余行为(控制参数,手机算法、回收策略等)和 Serial 收集器一样。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
3. Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代收集器。
使用的算法:采用复制算法。
Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
4. Serial Old 收集器
Serial Old 收集器 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用“标记—整理”算法。
使用的算法:采用“标记—整理”算法。
这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。
在 Server 模式下,会有两大用途:
- 在 JDK1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用;
- 作为 CMS 收集器的后备方案,在并发收集发生 Concurrent Model Failure 时使用。
5. Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本。
使用的算法:采用“标记—整理”算法。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6. CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。Mark Sweep 指的是标记 - 清除算法。
整个过程分为 4 个步骤:
- 初始标记(CMS initial marl):仅仅只是标记了一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记(CMS concurrent mark):进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记(CMS remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,此阶段停顿时间会比初始标记稍长一下,但远比并发标记的时间短,需要停顿。
- 并发清除(CMS concurrent sweep):开启用户线程,同时 GC 线程开始对为标记的区域做清扫,不需要停顿。
(注意哪个阶段不需要停顿)
缺点:
- CMS 收集器对 CPU 资源非常敏感:在并发阶段,虽然不会导致用户线程停顿,但会占用一部分线程导致程序变慢,总吞吐量降低。
- CMS 收集器无法处理浮动垃圾(Floating Garbage):CMS 并发清理过程中由于用户线程仍旧在运行,会产生垃圾,这部分垃圾出现在标记过程之后,CMS 无法再档次手机中处理掉,只能等下一次。这部分垃圾称为“浮动垃圾”。
- CMS 是基于“标记-清除”算法实现的,会产生空间碎片。
7. G1 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。这种使用 Region 划分内存空间的以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间 内可以获取尽可能高的收集效率。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记:与 CMS 相同;
- 并发标记:与 CMS 相同;
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行;
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备的特点:
- 并行与并发:能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU (CPU 或者 CPU 核心) 来缩短 Stop-The-Wolrd 停顿时间,部分其他收集器原本需要停顿 Java 线程执行 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 线程继续执行。
- 分代收集:采用不同与其他收集器的方式去处理新创建的对象和以及存活了一段时间、熬过了多次 GC 的旧对象以获取更好的收集效果。
- 空间整合:G1 从整体上看是基于“标记-清除”算法实现的收集器,从局部(两个 Region 之间) 上来看是基于“复制”算法实现的。所以,G1 收集器是不会产生内存空间碎片的,收集后能提供规整的可用内存。
- 可预测的停顿时间:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
参考:
- https://cyc2018.github.io/CS-Notes/#/notes/Java 虚拟机?id=_7-g1-收集器
- https://xiaozhuanlan.com/topic/1847690325#section127g1
三、在垃圾回收对象的时候程序的逻辑是否可以继续执行
不同的垃圾收集器不同。
Serial、ParNew 等会 Stop-The-World。而 CMS 和 G1 等会在某一阶段停顿用户线程 (CMS 是在初始标记和重新标记。而 G1 是在初始标记阶段会停顿用户线程,最终标记也需要停顿线程,但是可以并行执行;筛选回收也能做到与用户线程一起并发执行,时间是用户可控的,但是停顿用户线程能够大大提高收集效率)
四、将对象从年轻代到老年代是如何判断执行了多久的?以及什么情况下发生转移?哪些对象在老年代中?
Minor GC 发生在年轻代,频率较高速度较快;
Major GC 是清理永久代;
Full GC 是清理整个堆空间,包括年轻代和永久代。
一般新生成的对象会优先出现在 Eden 区,当 Eden 区被填满时,所有经过垃圾回收还存活的对象将被复制到两个 Suviovr 区域中的一个,我们假定是 From 区(两个 Survivor 区没有任何差别,只是为了好区分,才分了 From 和 To),当 From 区域也被填满时,这个区域经过垃圾回收仍然存活的对象将被复制进 To 区域,原 From 区域被清空,并且从 Eden 区过来的数据将直接进入 To 区域。当 To 区域也被填满时,之前从 From 区域过来的那部分数据如果仍在活动,则被放入老年代。需要注意的是,两个 Survivor 区域有一个会是空的。
大对象直接进入老年代:所谓大对象是指大量连续内存空间的 Java 对象。-XX:PretenureSizeThreshold
参数设置值,超过的直接在老年代分配。这样做避免了在 Eden 区域及两个 Suvivor 区之间发生大量的内存复制。
长期存活的对象将进入老年代:通过年龄计数器,对象每经过一个 GC 仍然存活,年龄计数加一。当年龄超过了设定的值,则将其通过担保机制转移到老年代。或者动态判定,当 Survivor 中年龄相同的多个对象的总和超过了 Survivor 一半时,则将年龄大于或等于该年龄的对象转移到老年代,无需等待设置的最大年龄值。
空间分配担保:在 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果成立, Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 值是否为 True,如果允许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行 Minor GC;如果小于,则进行 Full GC。
哪些对象在老年代中:大对象;长期存活的对象。