各语言内存操作对比:
语言 |
申请内存 |
释放内存 |
C |
malloc |
free |
C++ |
new |
delete |
Java |
new |
自动释放 |
Java语言的自动内存管理设计最终可以归结为自动化地解决了两个问题:
即对象内存的分配和回收。
了解JVM是垃圾回收机制,如何有效防止内存泄露、保证内存的有效使用,需要思考三个方向的问题:
进行垃圾回收的第一步:什么是垃圾? 没有引用指向的一个对象或多个对象(循环引用)。
定位垃圾的方法有两种:引用计数法和可达性分析。
Java中将数据类型分为两大类:基本类型和引用类型。如果reference类型数据中存储的数值为另一个块内存的起始地址,就称这块内存代表一个引用。在JDK1.2开始对引用的概念进行了扩充,分为强、软、弱、虚四种引用,且强度依次逐渐降低。详细见Java-四种引用类型。
Person p = new Person();
// 等号后面的 new Person(); 是真正的实例对象,其内容存储在Java堆内存中
// 等号前面的 p 只是一个引用标识符,保存在虚拟机栈内存中,它存储的只是一个地址,是 new Person() 在堆内存中的起始位置,因此 p 就是一个引用。
// 等号是一种强引用方式
引用计数法是垃圾收集器中的早期策略,通过判断对象的引用数量来决定对象是否可以被回收。该方法为堆中每个对象添加一个计数器,有地方引用了此对象则该对象的计数器加1,如果引用失效了则计数器减1,引用计数为0的对象实例则可以被当作垃圾收集。
优点:实现简单,判定效率也很高;
缺点:很难解决对象之间循环引用的问题;互相引用导致他们的引用计数都不为0,最终不能回收他们。
通过判断对象的引用链是否可达GCRoot来决定对象是否可以被回收。从离散数学中的图论引入,把所有的引用关系当作一张关系图谱,从被称为"GCRoot"的对象作为起始点,沿着引用链(Reference Chain,即引用路径)向下搜索,如果一个对象没有任何引用链连接到GCRoot节点,则证明此对象是不可用的/不可达,则此对象可被回收。
JAVA中可以作为GCRoot对象/根对象包括以下几种:
如图,引用链连接上GCroot的Object1、2、3、4都是被使用的对象,但是Object5、6、7却不能通过任何方式连接上根节点,因此判定Object5、6、7为可回收的节点。
可达性分析之后,不可达的对象一定会被垃圾收集器回收吗?不一定。
在可达性分析后发现不可达的对象会被进行一次标记,然后进行筛选,筛选的条件是判断该对象有没有必要执行finalize()方法;
1、对象无finalize方法:直接去除引用链,一次GC时便直接回收实例对象。
// JVM执行参数 -Xmx20m -XX:+PrintGCDetails
public class FinalizeTest {
static class M {
// 10M大小
private byte[] bytes = new byte[10 * 1024 * 1024];
}
public static void main(String[] args) throws Exception {
M m = new M();
m = null;
// new M()被去掉引用,且M中没有自救方法finalize,所以在GC时会直接回收
System.gc();
// GC相关线程优先级低,主线程等待一下
Thread.sleep(1000);
}
}
// 日志
/* 年轻代PSYoungGen的总内存大小为6144k=6M,所以10M的new M()对象会直接被分配在老年代ParOldGen*/
[GC (System.gc()) [PSYoungGen: 1749K->480K(6144K)] 11989K->10765K(19968K), 0.0018203 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// FGC情况下,ParOldGen老年代从10285K->471K
[Full GC (System.gc()) [PSYoungGen: 480K->0K(6144K)] [ParOldGen: 10285K->471K(13824K)] 10765K->471K(19968K), [Metaspace: 3087K->3087K(1056768K)], 0.0046541 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
/* 可知new M()有10M大小,从整个堆上各代的使用内存情况,可知new M()已经不在堆上面,也可以通过jmap来查看实例对象存活*/
/* 后面的三个内存地址值指的是:起始地址,已使用空间的结束地址,对应区整个内存空间的结束地址 */
Heap
PSYoungGen total 6144K, used 1255K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
eden space 5632K, 22% used [0x00000007bf980000,0x00000007bfab9f10,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 13824K, used 471K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
object space 13824K, 3% used [0x00000007bec00000,0x00000007bec75e70,0x00000007bf980000)
Metaspace used 3613K, capacity 4536K, committed 4864K, reserved 1056768K
class space used 399K, capacity 428K, committed 512K, reserved 1048576K
Process finished with exit code 0
2、对象有finalize方法,但不进行自我拯救:直接去除引用链,第一次GC时对象放入F-Queue的队列且进行finalize方法的拯救,不会被回收,但finalize中并未拯救,所以第二次GC时对象会被回收。
public class FinalizeTest {
static class M {
// 10M大小
private byte[] bytes = new byte[10 * 1024 * 1024];
@Override
protected void finalize() {
System.out.println("m的finalize执行了,但并不拯救");
}
}
public static void main(String[] args) throws Exception {
M m = new M();
m = null;
System.out.println("第一次GC-----------");
// 因为M的存在finalize,所以被放入F-Queue的队列中,拯救线程执行finalize方法,所以10M的new M()对象本次不被回收
System.gc();
// GC相关线程优先级低,主线程等待一下
Thread.sleep(1000);
// 第二次GC,
System.out.println("第二次GC-----------");
// 因为M的finalize已经执行了,但并未拯救,所以10M的new M()对象被回收了
System.gc();
Thread.sleep(1000);
}
}
// 日志
第一次GC-----------
[GC (System.gc()) [PSYoungGen: 1862K->512K(6144K)] 12102K->10797K(19968K), 0.0022455 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
/* 第一次FGC情况下,ParOldGen老年代从10285K->10712K,说明并未回收10M的new M()对象*/
[Full GC (System.gc()) [PSYoungGen: 512K->0K(6144K)] [ParOldGen: 10285K->10712K(13824K)] 10797K->10712K(19968K), [Metaspace: 3101K->3101K(1056768K)], 0.0040101 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
m的finalize执行了,但并不拯救
第二次GC-----------
[GC (System.gc()) [PSYoungGen: 1422K->448K(6144K)] 12134K->11168K(19968K), 0.0008161 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
/* 第二次FGC情况下,ParOldGen老年代从10720K->773K,因为M的finalize已经执行了,但并未拯救,所以10M的new M()对象被回收了*/
[Full GC (System.gc()) [PSYoungGen: 448K->0K(6144K)] [ParOldGen: 10720K->773K(13824K)] 11168K->773K(19968K), [Metaspace: 3614K->3614K(1056768K)], 0.0054856 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 6144K, used 150K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
eden space 5632K, 2% used [0x00000007bf980000,0x00000007bf9a5a40,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
ParOldGen total 13824K, used 773K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
object space 13824K, 5% used [0x00000007bec00000,0x00000007becc1520,0x00000007bf980000)
Metaspace used 3620K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 399K, capacity 428K, committed 512K, reserved 1048576K
Process finished with exit code 0
3、对象有finalize,且会自我拯救:直接去除引用链,第一次GC时对象放入F-Queue的队列且进行finalize方法的拯救,不会被回收;第二次GC时,被拯救的对象保持着新的引用链,所以不会被回收。
public class FinalizeTest {
public static M saveMen = null;
static class M {
// 10M大小
private byte[] bytes = new byte[10 * 1024 * 1024];
@Override
protected void finalize() {
FinalizeTest.saveMen = this;
System.out.println("m的finalize执行了,且自我拯救");
}
}
public static void main(String[] args) throws Exception {
M m = new M();
m = null;
System.out.println("第一次GC-----------");
System.gc();
// GC相关线程优先级低,主线程等待一下
Thread.sleep(1000);
System.out.print("saveMen: " + saveMen);
// 第二次GC
System.out.println("第二次GC-----------");
System.gc();
Thread.sleep(1000);
System.out.print("saveMen: " + saveMen);
}
}
// 日志
第一次GC-----------
[GC (System.gc()) [PSYoungGen: 1862K->496K(6144K)] 12102K->10781K(19968K), 0.0019234 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
/* 第一次FGC情况下,ParOldGen老年代从10285K->10712K,说明并未回收10M的new M()对象*/
[Full GC (System.gc()) [PSYoungGen: 496K->0K(6144K)] [ParOldGen: 10285K->10712K(13824K)] 10781K->10712K(19968K), [Metaspace: 3103K->3103K(1056768K)], 0.0043965 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
m的finalize执行了,且自我拯救
/* 打印拯救的变量saveMen的引用,说明拯救成功 */
saveMen: edward.com.FinalizeTest$M@3fee733d第二次GC-----------
[GC (System.gc()) [PSYoungGen: 1423K->384K(6144K)] 12136K->11104K(19968K), 0.0018592 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
/* 第二次FGC情况下,ParOldGen老年代从10720K->11013K,因为M的finalize已经执行且已拯救,所以10M的new M()对象被saveMen强引用着*/
[Full GC (System.gc()) [PSYoungGen: 384K->0K(6144K)] [ParOldGen: 10720K->11013K(13824K)] 11104K->11013K(19968K), [Metaspace: 3618K->3618K(1056768K)], 0.0074772 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
/* 第二次GC后打印拯救的变量saveMen的引用,说明对象还在 */
saveMen: edward.com.FinalizeTest$M@3fee733dHeap
PSYoungGen total 6144K, used 263K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
eden space 5632K, 4% used [0x00000007bf980000,0x00000007bf9c1ce0,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
ParOldGen total 13824K, used 11013K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
object space 13824K, 79% used [0x00000007bec00000,0x00000007bf6c16f0,0x00000007bf980000)
Metaspace used 3625K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 399K, capacity 428K, committed 512K, reserved 1048576K
Process finished with exit code 0
4、对象有finalize,且会自我拯救,但两次去除引用链:直接去除引用链,第一次GC时对象放入F-Queue的队列且进行finalize方法的拯救,不会被回收;但第二次GC时,被拯救的对象的新引用链又去除,这次对象直接被回收。
public class FinalizeTest {
public static M saveMen = null;
static class M {
// 10M大小
private byte[] bytes = new byte[10 * 1024 * 1024];
@Override
protected void finalize() {
FinalizeTest.saveMen = this;
System.out.println("m的finalize执行了,且自我拯救");
}
}
public static void main(String[] args) throws Exception {
M m = new M();
m = null;
System.out.println("第一次GC-----------");
System.gc();
// GC相关线程优先级低,主线程等待一下
Thread.sleep(1000);
System.out.print("saveMen: " + saveMen);
// 第二次GC,且再次去除次拯救new M()对象的引用链
saveMen = null;
System.out.println("第二次GC-----------");
System.gc();
Thread.sleep(1000);
System.out.print("saveMen: " + saveMen);
}
}
// 日志
第一次GC-----------
[GC (System.gc()) [PSYoungGen: 1862K->496K(6144K)] 12102K->10781K(19968K), 0.0008208 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
/* 第一次FGC情况下,ParOldGen老年代从10285K->10712K,说明并未回收10M的new M()对象*/
[Full GC (System.gc()) [PSYoungGen: 496K->0K(6144K)] [ParOldGen: 10285K->10712K(13824K)] 10781K->10712K(19968K), [Metaspace: 3111K->3111K(1056768K)], 0.0044683 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
m的finalize执行了,且自我拯救
/* 打印拯救的变量saveMen的引用,说明拯救成功 */
saveMen: edward.com.FinalizeTest$M@3fee733d第二次GC-----------
[GC (System.gc()) [PSYoungGen: 1423K->416K(6144K)] 12136K->11136K(19968K), 0.0018508 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
/* 第二次FGC情况下,ParOldGen老年代从10720K->774K,因为M的finalize已经执行且已拯救,所以再次去掉引用后GC,10M的new M()对象直接被回收*/
[Full GC (System.gc()) [PSYoungGen: 416K->0K(6144K)] [ParOldGen: 10720K->774K(13824K)] 11136K->774K(19968K), [Metaspace: 3624K->3624K(1056768K)], 0.0078671 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
/* 第二次GC后打印拯救的变量saveMen的引用,对象不在 */
saveMen: nullHeap
PSYoungGen total 6144K, used 263K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
eden space 5632K, 4% used [0x00000007bf980000,0x00000007bf9c1ce0,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
ParOldGen total 13824K, used 774K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
object space 13824K, 5% used [0x00000007bec00000,0x00000007becc1938,0x00000007bf980000)
Metaspace used 3631K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 399K, capacity 428K, committed 512K, reserved 1048576K
Process finished with exit code 0
算法分为"标记"和"清除"两个阶段。
优点:逻辑清晰,易于操作;
缺点:
将可用内存划分为大小相等的两块,每次只使用其中的一块,当一块的内存用完,将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
适用于对象存活率低的场景,研究发现新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率不错。
优点:内存区域连续性,不会产生内存碎片,实现简单,运行高效。
缺点:
和标记清除算法类似,但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程。
适用于对象存活率高的场景(老年代)。
优点:
HotSpot虚拟机的堆内存分代设计:
一般情况下,所有新生成的对象首先都是放在新生代的,目标就是尽可能快的收集掉那些生命周期短的对象。
YGC条件:Eden区满
一般情况下,存放的都是一些生命周期较长的对象,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。
Full GC的原因:
对象进入老年代的情况:
在JVM运行稳定后,以FGC后老年代存活对象空间大小为参考:
老年代的空间分配担保原则:
空间担保原则触发条件:Minor GC前
在发生Minor GC之前,虚拟机会计算老年代剩余可用空间 > 年轻代所有对象总空间(包括垃圾对象)?
以上说的有风险,是因为取历次晋升到老年代对象的平均值,并不能保证每次都能担保成功;如果担保失败的话,依然需要进行Full GC。所以我们最好还是打开HandlePromotionFailure开关,避免过多频繁的Full GC(因为Full GC的执行速度比Minor GC慢的多)。
GC情况:
类对象的回收条件:
JVM堆内存为什么分代 ?
不同对象的生命周期(存活情况)是不一样的,分代可以根据不同对象的情况进行不同的管理和回收。
新生代:存活率低,每次垃圾收集都有大批对象死去,选择复制算法快速且只需要付出少量存活对象的复制成本即可;
老年代:存活率高,没有额外空间对它进行分配担保,因此使用标记清除或者标记整理算法;
gc频率和耗时也不一样,使性能最高效。
新生代各区内存比例为什么是8:1:1?
研究发现新生代中的对象每次回收都基本上只有10%左右的对象存活。这样划分内存的空间利用率达到了90%,只有10%的空间是浪费掉;
JVM的出现过的十种垃圾收集器,及组合方式:
Serial(年轻代)/Serial Old(老年代)适用于单CPU的环境,单线程、简单高效、没有线程切换的开销,垃圾收集时会暂停其他用户进程。
Serial Old在JDK1.5中配合PS使用;也作为CMS的后备选择,在concurrent mode failure时使用。
ParNew其实就是 Serial 收集器的多线程并行版本(参数控制、收集算法、STW、对象分配规则、回收策略等),在多核CPU环境下有着比Serial更好的表现。可以使用-XX:ParallelGCThreads参数来控制垃圾收集线程数,默认和CPU数量一样。也是为了和CMS组合的。
PS是吞吐优先的回收器,GC自适应调节策略。
https://hllvm-group.iteye.com/group/topic/37095#post-242695
ParNew为什么不能和Parallel Old组合?
PNew和PS是不同人在同一时期开发的,PS的开发者更注重高吞吐(自适应策略),和PNew低停顿是对立的,PS开发之后效果不错就合上去了,正好同一期PO的开发(也有PS的开发)就是为了兼容PS,所以就组合一起了,且PO只能和PS组合。
查看JVM默认使用的垃圾回收器命令:
// PrintCommandLineFlags打印JVM启动的命令行参数
java -XX:+PrintCommandLineFlags -version
JVM垃圾回收器使用配置:
JDK1.8server模式下,默认使用PS+PO收集器。
年轻代的单线程收集器,采用复制算法,GC时会暂停所有用户进程(Stop The Wold);
老年代的单线程收集器,采用标记-整理算法,Serial收集器的老年代版本,也会STW。
年轻代的并行收集器,采用复制算法,ParNew收集器实质上是 Serial 收集器的多线程并行版本,在多核CPU环境下有着比Serial更好的表现,和CMS收集器配合使用。
也是年轻代收集器,采用复制算法的多线程并行收集器 ,但Parall Scavenger 收集追求高吞吐量,高效利用CPU;所以也叫吞吐量优先收集器。
与Parallel New的区别:
老年代的多线程并行收集器,采用标记-整理算法,吞吐量优先,为配合PS收集器的老年代版本。
老年代并发收集器,采用标记 - 清除算法,以最短回收低停顿时间为目标的收集器,HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。STW时间能降低到200ms内。
CMS收集器使用的是标记 - 清除算法,所以会产生内存碎片,对于内存碎片的处理需要通过 标记-整理算法来进行压缩整理,所以CMS收集器的老年代也会搭配Serial Old收集器使用!!!
基于标记 - 清除算法的实现四个步骤 :
缺点:
CMS的并发预处理和并发可中断预处理?
1、首先,CMS是一个关注停顿时间,以回收停顿时间最短为目标的垃圾回收器。并发预处理阶段做的工作是标记,重标记需要STW(Stop The World),因此重标记的工作尽可能多的在并发阶段完成来减少STW的时间。此阶段标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
2、并发可中断预清理(Concurrent precleaning)是标记在并发标记阶段引用发生变化的对象,如果发现对象的引用发生变化,则JVM会标记堆的这个区域为Dirty Card。那些能够从Dirty Card到达的对象也被标记(标记为存活),当标记做完后,这个Dirty Card区域就会消失。CMS有两个参数:CMSScheduleRemarkEdenSizeThreshold、CMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。两个参数组合起来的意思是预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入重新标记阶段。
为什么CMS后有G1?
因为现代服务器可用内存发展越来越大,CMS清理之后会老年代会产生内存碎片,内存不够时还是会FGC,可能老年代的内存碎片空间并不大,但是内存压缩整理时需要移动很多对象的内存,导致FGC停顿时间很长(如128G的堆,停顿10~20分钟)。所以CMS不适用于大内存的JVM虚拟机。CMS起着承上启下的作用。
G1 跳出固定大小以及固定分代区域划分的限制,而是把连续的 Java 堆分成了多个大小相等的不需要连续的独立区域 ( region ),每一个 region 都可以根据需要扮演新生代的 Eden Survivor空间 或者 老年代空间。分而治之的思想,STW时间能降低到10ms内,主要适用于多处理器、大容量内存的、追求低停顿同时具备高吞吐的垃圾回收器。
内存结构:
G1特点:
G1老年代收集器的运作可分四个步骤:
G1垃圾收集的种类:
为什么G1能保证-XX:MaxGCPauseMills配置的最大停顿时间?
G1垃圾回收器在垃圾遍历的时候就会计算对应回收需要的时间,当接近所配置的时间时就去进行部分收集,远小于则会继续遍历更多的垃圾来回收。
G1从整体来看是基于标记整理算法实现的收集器,但从局部上看又是基于标记复制算法实现。最终这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
STW时间能降低到10ms内(比拟C++回收效率),JDK11可以使用。ZGC
什么是颜色指针?什么是mmap?不同指针间是怎么映射到同一个位置上的?什么是重定位?什么是转发表?什么是线程自愈?
实验状态。
实验状态。
对象标记使用的是三色标记算法,将对象标记状态分为三种颜色,每次寻找都是基于上一次标记状态的结果来进行下一步遍历。
三种状态:
颜色状态的存储:CMS垃圾回收器是在对象头部的Mark Word拿出两个二进制位来存储对象的标记颜色。
问题1:多标 - 浮动垃圾Floating Garbage
并发标记阶段,用户线程和垃圾线程并行运行。
1、当前垃圾线程标记及引用链的状态:GCRoot -> A(黑) -> B(黑)
2、线程切换导致垃圾线程停止,用户线程运行,将A(黑) -> B(黑)引用去掉。则会导致B成为浮动垃圾。
问题2:漏标 - 误删(已解决)
并发标记阶段,用户线程和垃圾线程并行运行。
1、当前垃圾线程标记及引用链的状态:A(黑) -> B(灰) -> D(白)
2、线程切换导致垃圾线程停止,用户线程运行,正好将B(灰) -> D(白)引用去掉,增加A(黑) ->D(白)引用。
3、垃圾线程再次运行时,A(黑)不会再标记其fields节点,而遍历B(灰)时,D(白)已不是B的fields节点,所以则会导致D(白)的漏标。可能会被误删。
CMS解决方案:Incremental Update。
增量更新指当黑色对象新增引用为白色的对象时,会将这个引用记录下来,等并发标记结束后,再将这些引用关系的黑色对象作为根重新扫描一次。也可简单理解为JVM通过写屏障,在A.xField = D的时候,如果A是黑色,D是白色,则将A置为灰色,这样垃圾线程再次遍历会重写标记A的fields节点。
问题3:但是CMS的Incremental Update解决方案依然存在漏标。
1、当前垃圾线程标记及引用链的状态:A(灰) -> B(白) -> D(白),且垃圾线程1已扫描完A对象的field1属性。
2、线程切换导致垃圾线程停止,用户线程运行,正好将B(白) -> D(白)引用去掉,增加A.field1 ->D(白)引用。
3、垃圾线程1再次运行时,A.field1不会再标记其fields节点的D,当扫描完A.field2和B时会变为黑,已完成标记,则会导致D的漏标。
所以CMS在remark阶段,必须从GCRoot对已标记树再扫描一遍。
G1实现三色标记算法的方案是SATB(Snapshot At The Begining,原始快照)。
如有引用A(黑) -> B(灰) -> D(白)引用链,在B -> D引用消失的时候,将B->D的引用推到堆栈中记录,并发标记结束后,然后从堆栈中取出该引用(即指向的D对象)来扫描,来看D对象是否还被引用。避免本次误删。再加上G1的分区的Remember Set设计,可以找到当前分区对象被其他哪些分区所引用。