【证】:那些可作为GC Roots的对象

一、名词解释

根搜索算法是JVM用来的判断对象是否存活的算法,此算法基本思路为通过一系列的“GC Roots”对象作为起始点,从这些节点往下搜索,当一个对象和GC Roots不可达时,则该对象是无用的,可被回收的。如下图所示:object5、object6、object7虽然互相有关联,但是他们到GC Roots是不可达的,因此他们都可以被回收。

       

在java中,可作为GC Roots的对象有:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象;

2.方法区中的类静态属性引用的对象;

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

4.本地方法栈中JNI(即一般说的Native方法)中引用的对象

二、验证以上可作为GC Roots的对象(此处只做最简单的验证,不涉及很复杂的GCRoots引用链)。

1.验证虚拟机栈(栈帧中的局部变量)中引用的对象 作为GC Roots

/**
 * GCRoots 测试:虚拟机栈(栈帧中的局部变量)中引用的对象作为GCRoots 
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * 
 * 扩展:虚拟机栈中存放了编译器可知的八种基本数据类型,对象引用,returnAddress类型(指向了一条字节码指令的地址)
 * @author ljl
 */
public class TestGCRoots01 {
	private int _10MB = 10 * 1024 * 1024;
	private byte[] memory = new byte[8 * _10MB];

	public static void main(String[] args) {
		method01();
		System.out.println("返回main方法");
		System.gc();
		System.out.println("第二次GC完成");
	}

	public static void method01() {
		TestGCRoots01 t = new TestGCRoots01();
		System.gc();
		System.out.println("第一次GC完成");
	}
}

控制台打印日志:

[GC [PSYoungGen: 105513K->616K(458752K)] 105513K->82536K(983040K), 0.0945986 secs] [Times: user=0.17 sys=0.06, real=0.09 secs] 
[Full GC [PSYoungGen: 616K->0K(458752K)] [ParOldGen: 81920K->82430K(524288K)] 82536K->82430K(983040K) [PSPermGen: 2547K->2546K(21504K)], 0.0273364 secs] [Times: user=0.06 sys=0.01, real=0.03 secs] 
第一次GC完成
返回main方法
[GC [PSYoungGen: 15728K->64K(458752K)] 98159K->82494K(983040K), 0.0014739 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 64K->0K(458752K)] [ParOldGen: 82430K->510K(524288K)] 82494K->510K(983040K) [PSPermGen: 2546K->2546K(21504K)], 0.0118484 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
第二次GC完成

第一次GC:

t为局部变量,引用了new出的对象(80M),作为GC Roots,在Minor GC后被转移到老年代中,且Full GC也不会回收该对象,仍保留在老年代中。

第二次GC:

method01方法执行完后,局部变量t跟随方法消失,不再有引用类型指向该对象,该对象在Full GC后,被完全回收,老年代腾出该对象之前所占的空间。


2.验证方法区中的静态变量引用的对象作为GC Roots

/**
 * 测试方法区中的静态变量引用的对象作为GCRoots
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * 
 * 扩展:方法区存与堆一样,是各个线程共享的内存区域,用于存放已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
 * @author ljl
 * */
public class TestGCRoots02 {
	private static int _10MB = 10 * 1024 * 1024;
	private byte[] memory;

	private static TestGCRoots02 t;

	public TestGCRoots02(int size) {
		memory = new byte[size];
	}

	public static void main(String[] args) {
		TestGCRoots02 t2 = new TestGCRoots02(4 * _10MB);
		t2.t = new TestGCRoots02(8 * _10MB);
		t2 = null;
		System.gc();
	}
}


控制台打印日志:

[GC [PSYoungGen: 138608K->632K(458752K)] 138608K->82552K(983040K), 0.0684508 secs] [Times: user=0.19 sys=0.06, real=0.07 secs] 
[Full GC [PSYoungGen: 632K->0K(458752K)] [ParOldGen: 81920K->82427K(524288K)] 82552K->82427K(983040K) [PSPermGen: 2513K->2512K(21504K)], 0.0162803 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 

t2被置为null,Minor GC后t2之前引用的对象(40M)被完全回收;t为静态变量,存放于方法区中,引用了对象(80M),在Minor GC后,被转移到老年代中,且在Full GC后,也不会被回收,继续保留在老年代中。


3.验证方法区中常量引用对象作为GC Roots

/**
 * 测试常量引用对象作为GCRoots 
 * 注意:t修饰符如果只是final会被回收,static final不会被回收,所以static final 才是常量的正确写法
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * @author ljl
 */
public class TestGCRoots03 {
	private static int _10MB = 10 * 1024 * 1024;
	private static final TestGCRoots03 t = new TestGCRoots03(8 * _10MB);
	private byte[] memory;

	public TestGCRoots03(int size) {
		memory = new byte[size];
	}

	public static void main(String[] args) {
		TestGCRoots03 t3 = new TestGCRoots03(4 * _10MB);
		t3 = null;
		System.gc();
	}
}

控制台打印日志:

[GC [PSYoungGen: 138608K->688K(458752K)] 138608K->82608K(983040K), 0.0514407 secs] [Times: user=0.13 sys=0.02, real=0.05 secs] 
[Full GC [PSYoungGen: 688K->0K(458752K)] [ParOldGen: 81920K->82428K(524288K)] 82608K->82428K(983040K) [PSPermGen: 2515K->2514K(21504K)], 0.0153884 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 

t3被置为null,Minor GC后t3之前引用的对象(40M)被完全回收;t为常量,存放于方法区中,引用了对象(80M),在Minor GC后,被转移到老年代中,且在Full GC后,也不会被回收,继续保留在老年代中。


4.测试成员变量是否可作为GC Roots

/**
 * 测试成员变量引用对象是否可作为GCRoots
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 *
 * @author ljl
 */
public class TestGCRoots04 {
	private static int _10MB = 10 * 1024 * 1024;
	private TestGCRoots04 t;
	private byte[] memory;

	public TestGCRoots04(int size) {
		memory = new byte[size];
	}

	public static void main(String[] args) {
		TestGCRoots04 t4 = new TestGCRoots04(4 * _10MB);
		t4.t = new TestGCRoots04(8 * _10MB);
		t4 = null;
		System.gc();
	}
}

控制台打印日志:

[GC [PSYoungGen: 138608K->600K(458752K)] 138608K->600K(983040K), 0.0015591 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 600K->0K(458752K)] [ParOldGen: 0K->507K(524288K)] 600K->507K(983040K) [PSPermGen: 2513K->2512K(21504K)], 0.0144441 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
t4被置为null,Minor GC后t4之前引用的对象(40M)被完全回收;t为成员变量,也叫实例变量,不同于类变量(静态变量),前面讲到类变量是存储在方法区中,而成员变量是存储在堆内存的对象中的,和对象共存亡,所以是不能作为GC Roots的,从日志中也可看出t在MinorGC后,跟随t4一起被完全回收。不再占用任何空间。


以上为一个非常简单的可作为GC Roots的对象的验证,不涉及较复杂的GC Roots引用链,其实作为使用者来讲,我们只要知道,哪些对象是可作为GC Roots的,在实际开发过程中要特别注意这些对象,不要让无谓的大对象消耗了资源,拖累了性能。



<think>好的,我现在需要详细解释C#垃圾回收(GC)中的标记(Marking)过程,特别是从GC Roots出发遍历可达对象的详细流程。用户之前已经询问过标记阶段的触发时机,现在想深入了解具体的实现细节。 首先,我需要回顾一下用户的问题。用户希望了解标记阶段的具体步骤,包括GC Roots的类型、如何遍历对象图、标记的实现机制、并发或并行处理、以及处理中的一些注意事项。同时,用户提到要详细说明实现流程,可能涉及底层机制,比如位图标记、写屏障等。 接下来,我需要整理已有的知识。标记阶段是GC的核心部分,主要目的是识别所有存活对象,以便后续清理未被标记的对象。C#的GC采用分代回收,标记过程会根据不同代进行调整。标记的起点是GC Roots,这些根包括静态字段、局部变量、寄存器中的引用等。遍历过程需要递归或迭代地访问所有被根引用的对象,并标记它们为存活。未被标记的则视为垃圾。 现在需要详细展开每个步骤。首先,GC Roots的具体类型,比如线程栈中的局部变量、静态变量、GC句柄表等。然后是暂停应用程序线程(STW),以确保堆状态稳定。接下来是根扫描,收集所有根引用。然后是标记阶段本身,可能使用显式栈或队列进行遍历,避免递归导致的栈溢出。标记的实现可能使用位图或对象头中的标记位。同时,需要考虑并发标记,比如后台GC在.NET中的实现,减少暂停时间。此外,处理大对象堆(LOH)时的不同策略,以及写屏障在并发标记中的作用也需要提及。 需要注意用户可能的疑问点。例如,如何高效地遍历对象图,特别是在存在大量对象时;并发标记如何与应用程序线程共存而不导致漏标或错标;标记过程中如何处理新分配的对象;以及不同代(0、1、2代)的标记范围差异。同时,用户可能对术语如“位图标记”、“写屏障”不太熟悉,需要适当解释。 可能还需要比较不同的标记策略,比如深度优先与广度优先遍历,或者不同GC实现(如工作站GC与服务器GC)在标记阶段的差异。此外,解释为什么标记阶段需要暂停线程,以及如何通过并发标记减少暂停时间。 需要确保内容结构清晰,分步骤说明,每个部分有子标题和示例代码(如果有的话),但用户没有要求代码示例,所以可能以文字描述为主。同时,注意避免过于技术化的术语,或在必要时进行解释,以保持易懂性。 现在,需要组织内容。首先介绍标记的总体目标,然后分步骤详细说明流程,包括根枚举、遍历方法、标记数据结构、并发处理、写屏障、处理终结器和大对象堆等。最后总结关键点,强调标记的高效性和对性能的影响。 需要验一些细节是否正确,例如,.NET的GC是否使用位图标记还是对象头中的标记位。根据资料,.NET GC确实在对象头中使用标记位(sync block中的一位)来记录对象是否存活。此外,并发标记在后台GC中是通过写屏障来跟踪应用程序线程对对象引用的修改,确保标记的准确性。 另外,终结器队列的处理,标记完成后,需要将未被标记但具有终结器的对象放入终结器队列,等待终结线程处理。这一点需要提及,但可能属于清理阶段,不过在标记阶段结束时可能需要识别这些对象。 最后,确保语言流畅,逻辑清晰,分点说明,使用适当的例子帮助理解,比如根引用的来源,或遍历过程的示例。避免冗长,但覆盖必要的技术细节以满足用户对“详细实现流程”的需求。</think>在C#的垃圾回收(GC)机制中,**标记(Marking)** 是垃圾回收的核心阶段,其目标是从所有 **GC Roots** 出发,遍历所有可达对象,并标记为“存活”。未被标记的对象则被判定为“垃圾”,后续会被回收。以下是标记阶段的详细实现流程: --- ### **1. 标记阶段的总体目标** - **识别存活对象**:通过遍历对象引用链,确定哪些对象仍被程序使用。 - **区分垃圾对象**:未被标记的对象将被回收,释放内存。 --- ### **2. 详细实现流程** #### **步骤1:确定GC Roots** **GC Roots** 是标记过程的起点,包括以下类型的引用: - **线程栈中的局部变量**:当前执行方法中的局部变量和参数。 - **静态字段(Static Fields)**:所有类型的静态变量。 - **CPU寄存器中的引用**:JIT编译时寄存器中暂存的对象指针。 - **GC句柄表(Handle Table)**:包括弱引用(Weak)、强引用(Strong)、固定对象(Pinned)等。 - **终结器队列(Finalization Queue)**:等待执行终结器(`Finalize`)的对象。 - **其他特殊根**:如异步状态机、跨线程共享对象等。 **示例**: ```csharp void ExampleMethod() { object localObj = new object(); // 局部变量是一个GC Root // 其他代码... } ``` --- #### **步骤2:暂停应用程序线程(Stop-The-World, STW)** 为了确保标记过程中堆的状态稳定,GC会短暂暂停所有托管线程(某些GC模式如“后台GC”允许并发标记,但需特殊处理)。 --- #### **步骤3:根枚举(Root Scanning)** GC遍历所有GC Roots,将直接引用的对象加入 **标记队列(Mark Queue)**,作为遍历的起点。 --- #### **步骤4:递归标记可达对象** 从标记队列中的对象出发,**深度优先或广度优先遍历** 所有引用链: 1. **标记对象头**:在对象头(Object Header)中设置标记位(如 `BIT_SBLK_GC_RESERVE`),表示对象存活。 2. **遍历引用字段**:检查对象的每个引用类型字段,递归标记其指向的对象。 3. **循环处理**:直到标记队列为空,所有可达对象均被标记。 **优化技术**: - **显式栈代替递归**:避免递归调用导致的栈溢出,使用栈或队列数据结构迭代遍历。 - **位图标记(Bitmap Marking)**:某些GC实现用独立位图记录标记状态,而非修改对象头。 - **并行标记**:多线程同时处理不同子树,加快标记速度(适用于多核CPU)。 --- #### **步骤5:处理并发修改(写屏障, Write Barrier)** 在并发标记(如后台GC)中,应用程序线程可能在标记过程中修改对象引用。GC通过 **写屏障** 机制记录这些修改: - 当线程修改对象引用时,写屏障会将被修改的对象标记为“需重新检查”(例如加入 **卡片表,Card Table**)。 - GC在标记完成后,重新扫描这些区域以确保一致性。 --- #### **步骤6:处理终结器(Finalization)** - 若对象实现了 `Finalize` 方法,即使未被标记,GC也会将其加入 **终结器队列**,由独立线程执行清理逻辑。 - 这类对象会延迟回收(需下一次GC才能完全释放)。 --- #### **步骤7:区分代际与大对象堆(LOH)** - **分代GC**:不同代(0/1/2代)的标记范围不同。例如,0代GC仅标记0代对象。 - **大对象堆(LOH)**:>85KB的对象单独存放,标记逻辑与普通堆一致,但回收策略不同(通常延迟回收)。 --- ### **3. 关键数据结构** - **标记队列(Mark Queue)**:存储待遍历的对象引用。 - **卡片表(Card Table)**:记录堆中可能被并发修改的区域。 - **对象头标记位**:通过位掩码标记对象存活状态。 --- ### **4. 性能优化与挑战** - **并发与后台GC**:减少STW暂停时间(如 .NET的“后台GC”允许标记与用户线程并发执行)。 - **分代标记**:仅扫描特定代的存活对象,避免全堆遍历。 - **大对象处理**:LOH的标记与普通堆分离,避免频繁扫描。 --- ### **总结** 标记阶段通过 **GC Roots对象引用链遍历 → 标记存活对象** 的流程,精确识别内存中的垃圾。其实现依赖高效的根枚举、并发控制(如写屏障)、分代优化等机制,以平衡内存回收的准确性与程序性能。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值