一、GC基本信息
1.1 什么是GC,垃圾回收?
JVM的垃圾回收(Garbage Collection,GC)是一种自动内存管理机制,其主要目的是识别并清除不再使用的对象,释放内存空间以供应用程序中的其他部分使用。GC的要点和机制主要包括以下几个方面:
1. 分代收集算法:JVM通常将堆内存分为新生代和老年代,新生代存放新创建的对象,老年代存放长时间存活的对象。新生代的GC(Minor GC)频繁且速度快,老年代的GC(Major GC 或 Full GC)频率低但耗时长。
2. 垃圾回收算法:常见的垃圾回收算法包括标记-清除、复制、标记-整理等。标记-清除算法分为标记和清除两个阶段,但可能产生内存碎片;复制算法通过复制存活对象到另一块内存来减少碎片,但需要额外内存空间;标记-整理算法通过移动对象来减少碎片,但需要更多计算。
3. 垃圾回收器类型:JVM提供了多种垃圾回收器,如Serial、Parallel、CMS、G1等,每种回收器适用于不同的应用场景和性能要求。
4. 性能调优:通过监控和分析GC日志,可以对JVM进行性能调优。例如,可以通过调整堆大小、选择合适的垃圾回收器、优化代码等方式来提高应用程序的性能。
5. 内存分配策略:对象通常首先在新生代的Eden区分配,经过GC后存活的对象会进入Survivor区,并随着年龄增长可能被晋升到老年代。
6. 可达性分析:JVM使用可达性分析来确定对象是否存活,以GC Roots为起点,通过引用链判断对象是否可达。
7. GC日志监控与分析:使用工具如jstat、VisualVM等监控GC事件和内存使用情况,分析GC日志以优化GC配置。
通过深入理解JVM的垃圾回收机制,开发者可以更有效地优化Java应用程序的性能,处理内存相关问题,并提高系统的稳定性和响应速度。
1.2 如何定位垃圾、判断对象是否死亡?
堆中几乎放着所有的对象实例,对堆垃圾回收前的第⼀步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
(1)引用计数法
给对象中添加⼀个引⽤计数器,有对象引用时,计数器就加1;当引用失效,计数器就减1;计数器为0就可以被回收。
(缺点:引用计数法无法解决对象之间循环引用的问题:A对象引用B,B对象引用A)
(2)可达性分析算法
通过一些列的称为GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots之间没有任何引用链相连,则证明这个对象是不可用的。
(3)GC Roots包括哪些?
1、虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2、方法区中静态变量、常量引用的对象。譬如Java类的引用类型 静态变量。
3、 【本地Native方法】中引用的对象
4、 所有被同步锁(synchronized关键字)持有的对象。
5、 Java虚拟机内部的引用,如基本数据类型对应的Class对象,
6、 字符串常量池里的引用。
7、一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
判断不可达一定会被回收吗 ?
即使在可达性分析算法判断不可达的对象,也并非是“非死不可的”。
如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被调用过,finalize()方法都不会执行,该对象将会被回收。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,然后由Finalizer线程去执行。GC将会对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,那么在第二次标记时将会被移除“即将回收”的集合,否则该对象将会被回收。
(比如:把自己(this关键字)赋值给某个类变量(static修饰)或者对象的成员变量)
二、常见的垃圾回收算法有哪些?(标记清除、标记整理、复制算法)
(1) 标记清除
(Mark-Sweep):该算法会从每个GC Roots出发,依次标记有引用关系的对象,最后将没有被标记的对象清除。
缺点:效率不高,会产生碎片
为了解决这个问题,又提出了“标记-整理算法”,该算法类似计算机的磁盘整理,首先会从GC Roots出发标记存活的对象,然后将存活对象整理到内存空间的一端,形成连续的已使用空间,最后把已使用空间之外的部分全部清理掉,这样就不会产生空间碎片的问题
(2) 标记复制---用于survivor区域
(Mark-Copy):将空间分为大小相等的两块,只将数据存储在其中一块上
-
当需要回收时,首先标记垃圾对象
-
然后将存活对象复制到另一块内存
-
最后将第一块内存空间全部清除!
分析
-
避免了空间碎片,但内存缩小了一半。
-
效率不高:每次都需将有用数据全部复制到另一片内存
(3) 标记整理算法--用于老年代
(Mark-Compact):在回收前,标记过程仍与"标记-清除"一样 但后续不是直接清理可回收对象,而是
-
将所有存活对象移到一端
-
直接清掉端边界之外内存
分析:
这是一种老年代垃圾收集算法. 老年代中对象一般寿命较长,每次垃圾回收会有大量对象存活 因此如果选用"复制"算法,每次需要較多的复制操作,效率低
而且,在新生代中使用"复制"算法 当 Eden+Survior 都装不下某个对象时,可使用老年代内存进行"分配担保"
而如果在老年代使用该算法,那么在老年代中如果出现 Eden+Survior 装不下某个对象时,没有其他区域给他作分配担保
因此,老年代中一般使用标记整理算法
三种算法的缺点:
-
标记清除:先标记,标记完毕之后再清除,效率不高,会产生大量的空间碎片。导致需要分配一个较大连续空间时容易触发FullGC。
-
标记复制:有一半的空间是浪费的,分为 8:1 的 Eden 区和 survivor 区,就是上面谈到的 YGC
-
标记整理:效率不高,标记完毕之后,让所有存活的对象向一端移动
三、 GC分哪三种,分别采用什么算法?
堆内存空间分为较大的Eden和两块较小的Survivor,每次只使用Eden和Survivor区的一块。在新生代中,由于大量对象都是"朝生夕死",也就是一次垃圾收集后只有少量对象存活 因此我们可以将内存划分成三块。
-
Eden、Survior0、Survior1
-
内存大小分别是8:1:1
分配内存时,只使用Eden和一块Survior0,当发现Eden+Survior0的内存即将满时,JVM会发起一次Minor GC
,清除掉废弃的对象,并将所有存活下来的对象复制到Survior1中。
通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题.
分配担保
准备为一个对象分配内存时,发现此时Eden+Survior中空闲的区域无法装下该对象 就会触发MinorGC
(新生代 GC 算法),对该区域的废弃对象进行回收。
但如果MinorGC过后只有少量对象被回收,仍然无法装下新对象
-
那么此时需要将Eden+Survior中的
所有对象
都转移到老年代
中,然后再将新对象存入Eden区.这个过程就是"分配担保".
在发生 minor gc 前,虚拟机会检测老年代最大可用连续空间是否大于新生代所有对象总空间
若成立,minor gc 可确保安全 若不成立,JVM会查看 HandlePromotionFailure
是否允许担保失败
-
伊甸园Eden,最初对象都分配到这里,与幸存区合称新生代
-
幸存区 Survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成 from 和 to,采用标记复制算法
-
老年代 old,当幸存区对象熬过15次回收,晋升到老年代【幸存区内存不足或大对象会导致提前晋升】
(1)Minor GC:
发生在新生代的垃圾回收,暂停时间。短清理整个YouGen的过程,eden的清理
(2)Mixed GC:
新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
(3)Full GC:
新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免。Full GC 是清理整个堆空间—包括年轻代和永久代
Minor GC 和Full GC区别?
新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC
-
Minor GC: 发生在新生代的垃圾回收
-
Full GC: 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免。
Full GC 触发的场景?
-
显示调用了System.gc()。
-
堆内存分配过小
-
大对象
-
内存泄漏
-
老年代空间不足。Minor GC时空间分配担保失败,新生代对象晋级老年代,大对象分配空间等情况。如果Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space。
-
永久代或者元空间满了。如果经过Full GC仍然回收不了,那么JVM会抛出错误信息:java.lang.OutOfMemoryError: PermGen space 。
-
大量的线程回收,导致GC时间很长。
频繁FullGC如何排查?
首先判断FullGC后能不能有效的把内存回收掉,同时把堆dump下来,分析FullGC的原因:
如果FullGC没有效果,产生了内存泄漏,查看代码。
如果FullGC有效果,有可能是堆内存分配不合理。
-
如果是一次fullgc后,剩余对象不多。那么说明你eden区设置太小,导致短生命周期的对象进入了old区。
-
如果一次fullgc后,old区回收率不大,那么说明old区太小。
四、垃圾回收器有哪些?
垃圾回收器主要分为两种模式:一个是分代模型,一个是分区模型
垃圾收集器主要有:Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1
(1)Serial、Serial Old(几兆-几十兆)
Serial收集器:一个新生代、单线程的收集器,采用标记-复制算法。它在进行垃圾回收时,必须暂停所有用户线程,直到它收集结束。(stop the world)
Serial Old收集器:一个老年代、单线程的收集器,采用标记-整理算法。它在进行垃圾回收时,必须暂停所有用户线程,直到它收集结束。(stop the world)
(2)Parallel【JDK1.8默认--多线程】
Parallel Scavenge、Parallel Old
JDK 1.8 默认采用的是ParallelGC ,老年代默认就是Parallel old 了
PS+PO、并行(几十兆-几百兆)
大部分系统使用PS+PO(分代模型 )
-
①eden 内存不足发生 Minor GC,标记复制 STW
-
②old 内存不足发生 Full GC,标记整理 STW
-
③注重吞吐量
在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑PS+PO收集器这个组合。
Parallel Scavenge收集器是一款新生代收集器,基于标记-复制算法实现的。能够进行并行收集的多线程收集器。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集(多个GC线程),基于标记-整理算法实现。
(3)CMS+ParNew
ParNew收集器:一个新生代、多线程的收集器,采用标记-复制算法。只有Serial和ParNew收集器能与CMS配合工作。ParNew收集器是激活CMS后的默认新生代收集器。
CMS收集器(ConcurrentMarkSweep)是老年代收集器,基于标记-清除算法实现的,适合8G-16G左右的机器;【标记清理算法 】
- 一个业务线程,一个垃圾回收线程。
-
old 并发标记,重新标记时需要 STW,并发清除
-
Failback Full GC
-
注重响应时间
CMS垃圾回收过程?
CMS收集器是一种老年代区域的垃圾收集器,往往配合ParNew收集器来使用。它适合在注重用户体验的应用上使用,实现了GC线程和用户线程并发进行。 采用了标记清除算法。回收过程大致分为5个步骤:
-
初始标记(STW):暂停其他工作线程(STW),标记GC roots 直接引用的对象。过程很快。
-
并发标记:从GC roots 直接引用的对象出发向下查找所有引用的对象。这个过程最耗时,和用户线程并发执行。【这个过程可能出现的问题:用户线程执行中可能产生新的垃圾(浮动垃圾),无法被标记。】
-
重新标记(STW):为修正 并发标记 中产生变动的对象标识,主要用三色标记中的增量更新算法来进行标记。这个过程会暂停其他进程(STW)。
-
并发清除:将标记为可回收的对象进行回收,和用户线程并发执行。【这个过程也会产生新垃圾对象(浮动垃圾),这些对象将在下次GC时回收】
-
并发重置:将标记为不可回收的对象的标志清除。
三色标记算法
为什么CMS的GC线程可以和用户线程一起工作?----三色标记算法
1.用三种颜色记录对象的标记状态
-
①黑色 – 已标记
-
②灰色 – 标记中
-
③白色 – 还未标记
- 刚开始,所有的对象都是白色,没有被访问。 - 将GC Roots直接关联的对象置为灰色。 - 遍历灰色对象的所有引用,遍历后灰色对象本身置为黑色,引用置为灰色。 - 重复步骤3,直到没有灰色对象为止。 - 结束时,黑色对象存活,白色对象回收。
CSM缺点?
-
并发回收导致CPU资源紧张: 在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低 程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对 用户程序的影响就可能变得很大。
-
无法清理浮动垃圾: 在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但 这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃 圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
-
并发失败(Concurrent Mode Failure): 由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因 此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回 收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可 以通过 -XX: CMSInitiatingOccupancyFraction 参数来设置。 这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发 失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启 用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
-
内存碎片问题: CMS是一款基于标记清除算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续 空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开 启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的 作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理 (默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
为什么CMS要用标记-清除,不用标记-整理算法?
【答】因为CMS作为第一款实现用户线程和收集线程并发执行的收集器!当时的设计理念是减少停顿时间,最好是能并发执行!但是问题来了,如要用户线程也在执行,那么就不能轻易的改变堆中对象的内存地址!不然会导致用户线程无法定位引用对象,从而无法正常运行!而标记整理算法和标记复制算法都会移动存活的对象,这就与上面的策略不符!因此CMS采用的是标记清理算法。
CMS的垃圾回收阶段是并发回收的,如果使用标记整理的话,对象的内存地址会进行移动,因为用户线程还在执行,为了避免因内存地址移动而带来的bug,还需要对用户线程的对象指针进行维护,在这个过程中肯定会STW,这样做就提高了垃圾清理的时长,停顿时间也变长了,不符合CMS以获取最短回收停顿时间为目标的设计初衷。
总结:
CMS的回收周期很长,但是他的STW时间是分开的,比如总的STW要100ms,可能他会在初始标记消耗20ms,重新标记消耗80ms,对于用户来说能感知的到停顿时长可能只有80ms,也就是说CMS的设计初衷是为了提高用户体验,减少停顿时间。这是和Parallel最大的不同。正因为CMS的回收周期很长,所以在垃圾很多的情况下可能出现上次的GC周期还没执行完就又触发了GC,被称为”concurrent mode failure“;对于这种情况会回退到Serial的方式进行回收,全程STW。因为是采用标记清除算法,所以会存在内存碎片的问题,通过参数-XX:+UseCMSCompactAtFullCollection
可以设置清除之后再做一次整理。
-
CMS相关参数
-XX:+UseConcMarkSweepGC
:使用CMS垃圾收集器,当设置这个参数后,年轻代默认会开启ParNew。-XX:ConcGCThreads
:并发的GC线程数,默认是CPU的核数。-XX:+UseCMSCompactAtFullCollection
:相当于标记整理。-XX:CMSFullGCsBeforeCompaction
:多少次FullGC之后压缩一次,默认是0。-XX:CMSInitiatingOccupancyFraction
: 当老年代使用达到该比例时会触发FullGC,默认是92。-XX:+UseCMSInitiatingOccupancyOnly
:这个参数搭配上面那个用,表示是不是要一直使用上面的比例触发FullGC,如果设置则只会在第一次FullGC的时候使用-XX:CMSInitiatingOccupancyFraction的值,之后会进行自动调整。-XX:+CMSScavengeBeforeRemark
:在FullGC前启动一次MinorGC,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段。-XX:+CMSParallellnitialMarkEnabled
:默认情况下初始标记是单线程的,这个参数可以让他多线程执行,可以减少STW。-XX:+CMSParallelRemarkEnabled
:使用多线程进行重新标记,目的也是为了减少STW。
Serial GC、 Parallel GC 、CMS+PN这三个缺点:操作必须扫描整个老年代、不太适合大内存,年轻代老年代都是独立的内存块、大小必须要提前确定。
(4)G1-Garbage-First【JDK1.9默认】
G1 的全称是 Garbage-First【垃圾优先】G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。G1 成为【JDK9的默认垃圾回收器】,取代了 PS+ PO 的组合,而 CMS 被声明为不推荐使用的垃圾回收器。
Region-分区回收---(堆内存的分配)
G1 收集器采用了和此前完全不同的堆内存分配方式,他将堆内存分为2048个相同大小的region(单个大小为1MB~32MB),这些regions在逻辑上被动态的分为4种,每个区域都可以充当 eden,survivor,old, humongous(巨大)
-
Eden
-
Survivor
-
old generation
-
Humongous regions (G1还新增一种新的类型)。如果对象超过所在regions的50%,就会被移入这个类型的region。这种大对象应尽量避免创建。
G1用的哪种垃圾回收算法?
G1收集器采用“标记-复制”和“标记-整理”。
-
从整体看,是基于“标记整理”,
-
从局部看,两个region之间是“标记复制”。
G1垃圾回收过程?
-
初始标记(会STW):
该阶段标记所有从GC根对象可达的存活对象。 这个阶段需要停顿线程,与进行Minor GC的时候同步完成的,因此耗时很短。 -
并发标记:
-
在这个阶段,G1 GC会扫描整个堆,标记出所有存活的对象。这个过程是在程序继续运行的情况下并发执行的,因此不会引起长时间的暂停。
-
G1 GC会跟踪哪些区域的回收收益最大,以便后续优先进行回收。
-
-
最终标记(会STW):
-
这个阶段暂停应用线程,处理并发阶段结束后仍有引用变动的对象。
-
该阶段通常暂停时间较短。
-
-
清理阶段(会STW):
更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。 -
并行回收
-
存活的对象会从旧的区域移动到新的区域中,回收未使用的区域。
-
该过程通常是并行执行的,以减少暂停时间。
-
-
混合回收
-
混合GC阶段既回收年轻代又回收一部分旧年代区域。它在年轻代GC的基础上,混合回收堆中那些收益较高的旧代区域。
-
这个阶段的目的是在控制暂停时间的前提下,逐步减少旧代中的垃圾对象。
-
G1 回收阶段 - 并发标记与混合收集
-
当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程
-
并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。
-
混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制
-
下图显示了老年代和幸存区晋升的存活对象的复制
-
复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
G1 GC的优势和缺点
优点:
-
可预测性:G1 GC的设计目的是控制和减少垃圾回收的长暂停。
-
区域化管理:通过分区来管理内存,使得垃圾回收操作更加灵活,优先回收那些回收价值最高的区域。
-
并发处理:G1 GC的多个阶段是并发执行的,可以有效减少暂停时间,适合大内存应用。
总结来说,G1 GC通过初始标记、并发标记、最终标记、清理、并行回收和混合回收阶段来优化垃圾回收流程,尤其适用于低停顿、可预测的垃圾回收需求的场景。
缺点:
以空间换时间,浪费了很多内存
G1的官方推荐
期望GC-STW时间
G1 新增一个配置(-XX:MaxGCPauseMillis=200),来设置我们期望每次GC-STW的时间。这是一个相对值,不会严格按照这个时间执行,jvm 会评估GC总的时间,如果不能满足这个期望时间,JVM 会通过一定的算法,只回收部分性价比高的内存空间,来达到这个目标。
-
不要设置年轻代大小
-
设置年轻一代的大小会禁用期望GC-STW时间的目标。
-
G1不再能够根据需要扩展和缩小年轻一代的空间。由于尺寸是固定的,因此无法更改尺寸。
-
-
期望GC-STW时间
-
最好不要设置平均GC时间,而是使用 90% 的GC时间.
-
-
Mixed GC 中 Evacuation Failure
-
当没有更多的空闲region被提升到老一代或者复制到幸存空间时,并且由于堆已经达到最大值,堆不能扩展,从而发生Evacuation Failure。此时会触发full GC ,类似与 Serial 收集器的单线程垃圾回收,非常耗时
-
增加(-XX:G1ReservePercent=10)的大小
-
降低(-XX:InitiatingHeapOccupancyPercent=45)的大小
-
增加(-XX:ConcGCThreads=n)并发标记线程数
-
(5)ZGC
CMS和G1如何解决并发漏标问题?
漏标和错标存在两个充要条件:
-
有至少一个黑色对象在自己被标记之后指向了这个白色对象
-
所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
这两个条件,必须全满足,才会造成漏标问题。换言之,我们破坏任何一个条件,这个白色对象。就不会再被漏标。
如此一来这样就产生了两个解决办法:
①CMS采用增量更新:Incremental Update(CMS)
-
只要赋值发生,被赋值的对象就会被记录
-
记录黑色新增的引用(底层用的写屏障)
增量更新破坏的是第一个条件,我们在这个黑色对象增加了对白色对象的引用之后,将它的这个引用,记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描。
可以简单理解为,当一个黑色对象增加了对白色对象的引用,那么这个黑色对象就被变灰。
缺点:就是会重新扫描这个黑色对象的所有引用,比较浪费时间。
②G1采用原始快照:Snapshot At The Beginning,SATB(G1)
-
新加对象会被记录
-
被删除引用关系的对象也被记录
原始快照破坏的是第二个条件,我们在这个灰色对象取消对白色对象的引用之前,将这个引用记录下来,在最后标记的时候,再以这个引用指向的白色对象为根,对它的引用进行扫描。
可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象被变灰。
这样做的缺点就是,这个白色对象有可能并没有黑色对象去引用它,但是它还是被变灰了,就会导致它和它的引用,本来应该被垃圾回收掉,但是此次GC存活了下来,就是所谓的浮动垃圾.
其实这样是比较可以忍受的,只是让它多存活了一次GC而已,浪费一点点空间,但是会比增量更新更省时间。
【面试题】:CMS和G1的异同?
G1 垃圾收集器相比与 CMS 收集器,G1 收集器两个最突出的改进是:
-
基于标记-整理算法,不产生内存碎片。
-
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。 区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。