> 文章列表 > 垃圾收集器面试总结(二)

垃圾收集器面试总结(二)

垃圾收集器面试总结(二)

G1 收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:

  • 并行与并发: G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  • 分代收集: 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

  • 空间整合: 与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“标记—复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿: 这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

垃圾收集器面试总结(二)

在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才有,只是在G1中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。

在其他垃圾收集器中,通过CardTable来维护老年代对年轻代的引用,CardTable可以说是Remembered Set(RS)的一种特殊实现,是Card的集合。Card是一块2的幂字节大小的内存区域,例如HotSpot用512字节,里面可能包含多个对象。CardTable要记录的是从它覆盖的范围出发指向别的范围的指针。以分代式GC的CardTable为例,要记录老年代指向年轻代的跨代指针,被标记的Card是老年代范围内的。当进进行年轻代的垃圾收集时,只需要扫描年轻代和老年代的CardTable即可保证不对全堆扫描也不会有遗漏。CardTable通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为dirty card。

在G1收集器中,也有和上面一样的CardTable。另外G1中每个Region还有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个 Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

垃圾收集器面试总结(二)

G1收集器的垃圾收集分两种:Minor GC(Young GC)和 Mixed GC(Old GC)。

G1:Minor GC

Young GC大致可以分为5个阶段

  • 根扫描: 静态和本地对象被扫描。
  • 更新RS: 处理dirty card队列更新RS。
  • 处理RS: 检测从年轻代指向年老代的对象。
  • 对象拷贝: 拷贝存活的对象到survivor/old区域。
  • 处理引用队列: 软引用,弱引用,虚引用处理。

G1:Mixed GC

Mixed GC大致可划分为全局并发标记(global concurrent marking)和拷贝存活对象(evacuation)两个大部分:

global concurrent marking是基于SATB形式的并发标记,包括以下4个阶段:初始标记(Initial Marking)、并发标记(Concurrent Marking)、最终标记(Final Marking)、清理(Clean Up)。

  • 初始标记(initial marking): 暂停阶段(会 STW)。扫描根集合,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈(marking stack)中等待后续扫描。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。在分代式G1模式中,初始标记阶段借用young GC的暂停,因而没有额外的、单独的暂停阶段。

  • 并发标记(concurrent marking): 并发阶段(不会 STW)。不断从扫描栈取出引用递归扫描整个堆里的对象图。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程直到扫描栈清空。过程中还会扫描SATB write barrier所记录下的引用。

  • 最终标记(final marking,在实现中也叫remarking): 暂停阶段(会 STW)。在完成并发标记后,每个Java线程还会有一些剩下的SATB write barrier记录的引用尚未处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。 注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。

  • 清理(cleanup)阶段: 暂停阶段(会 STW)。清点和重置标记状态。这个阶段有点像mark-sweep中的sweep阶段,不过不是在堆上sweep实际对象,而是在marking bitmap里统计每个region被标记为活的对象有多少。这个阶段如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。

Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去,然后回收原本的region的空间。

Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet),靠per-region remembered set(简称RSet)实现。这是regional garbage collector的特征。

在选定CSet后,evacuation其实就跟ParallelScavenge的young GC的算法类似,采用并行copying(或者叫scavenging)算法把CSet里每个region里的活对象拷贝到新的region里,整个过程完全暂停。从这个意义上说,G1的evacuation跟传统的mark-compact算法的compaction完全不同:前者会自己从根集合遍历对象图来判定对象的生死,不需要依赖global concurrent marking的结果,有就用,没有拉倒;而后者则依赖于之前的mark阶段对对象生死的判定。

纯G1模式下,CSet的选定完全靠统计模型找处收益最高、开销不超过用户指定的上限的若干region。由于每个region都有RSet覆盖,要单独evacuate任意一个或多个region都没问题。

垃圾收集器面试总结(二)

对象漏标

垃圾回收的并发标记阶段,gc线程和应用线程是并发执行的,所以一个对象被标记之后,应用线程可能篡改对象的引用关系,从而造成对象的漏标、误标,其实误标没什么关系,顶多造成浮动垃圾,在下次gc还是可以回收的,但是漏标的后果是致命的,把本应该存活的对象给回收了,从而影响的程序的正确性。

为了解决在并发标记过程中,存活对象漏标的情况,GC HandBook把对象分成三种颜色(三色标记):

  • 黑色:从GCRoots开始,已扫描过它全部引用的对象,标记为黑色;
  • 灰色:扫描过对象本身,还没完全扫描过它全部引用的对象,标记为灰色;
  • 白色:还没扫描过的对象,标记为白色。

所以,漏标的情况只会发生在白色对象中,且同时满足下面两个条件:

  • 有至少一个黑色对象在自己被标记之后指向了这个白色对象;
  • 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用。

例如:

D对象引用E对象,E引用G,此时GC正好处于D已经变成黑色,E处于灰色;
G是白色的情况下,此时因为业务逻辑的变化,E不引用G了,D对象引用了G;
按照三色标记法看,黑色对象是已完成状态,不可能再去找子引用,所以G就不会变成灰色,这样就会造成白色对象此时正在被线程使用中,
但是无法被标记成灰色或者白色,造成一个正在被使用的对象被错误回收。

这两个条件,必须全满足,才会造成漏标问题。换言之,我们破坏任何一个条件。这个白色对象,就不会再被漏标,这样就产生了两个解决办法。

CMS采用的是增量更新

增量更新破坏的是第一个条件,我们在这个黑色对象增加了对白色对象的引用之后,将它的这个引用记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描。
​可以简单理解为,当一个黑色对象增加了对白色对象的引用,那么这个黑色对象就被变灰。这样有一个缺点,就是会重新扫描这个黑色对象的所有引用,比较浪费时间。

G1采用的是原始快照(SATB)

原始快照破坏的是第二个条件,我们在这个灰色对象取消对白色对象的引用之前,将这个引用记录下来,在最后标记的时候,再以这个引用指向的白色对象为根,对它的引用进行扫描。

可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象被变灰。

​这样做的缺点就是,这个白色对象有可能并没有黑色对象去引用它,但是它还是被变灰了,就会导致它和它的引用,本来应该被垃圾回收掉,但是此次GC存活了下来,就是所谓的浮动垃圾.其实这样是比较可以忍受的,只是让它多存活了一次GC而已,浪费一点点空间,但是会比增量更新更省时间.

SATB

SATB全称snapshot-at-the-beginning,由Taiichi Yuasa为增量式标记清除垃圾收集器开发的一个算法,主要应用于垃圾收集的并发标记阶段,解决了CMS垃圾收集器重新标记阶段长时间STW的潜在风险。Region包含了5个指针,分别是bottom、previous TAMS、next TAMS、top和end。

垃圾收集器面试总结(二)
垃圾收集器面试总结(二)

JVM运行模式

JVM(Java虚拟机)有两种运行模式:Client模式和Server模式。它们的主要区别在于优化策略和内存管理方式。

  1. Client模式
    Client模式主要是针对启动时间短,但运行时间比较短的Java应用程序(如小型GUI应用程序等),通过减少JVM启动时间来提高应用程序的性能。在Client模式下,JVM会使用较少的内存来运行,以及使用较少的线程来处理请求。这些优化措施可以提高应用程序的启动速度和响应速度。
    优点:启动速度快,占用内存少。
    缺点:在长时间运行的情况下,由于内存和线程的限制,可能会导致应用程序的性能和响应速度下降。

  2. Server模式
    Server模式主要是针对长时间运行的Java应用程序(如Web服务器、应用服务器等)进行优化,通过提高JVM的性能和响应速度来处理高并发的请求。在Server模式下,JVM会使用更多的内存来运行,以及使用更多的线程来处理请求。这些优化措施可以提高应用程序的性能和响应速度。
    优点:针对长时间运行的Java应用程序进行了优化,可以提高应用程序的性能和响应速度。
    缺点:启动时间较长,占用的内存较多。

  3. 区别

     内存管理:Client模式下使用较少的内存,而Server模式下使用更多的内存。线程管理:Client模式下使用较少的线程,而Server模式下使用更多的线程。优化策略:Client模式主要优化启动时间和响应速度,而Server模式主要优化性能和响应速度。启动时间:Client模式启动时间短,而Server模式启动时间较长。
    

总之,选择JVM运行模式需要根据具体的应用场景来确定。对于启动时间短、运行时间短的Java应用程序,可以选择Client模式;而对于长时间运行的Java应用程序,可以选择Server模式。