CoreCLR
在这篇中我将讲述GC Collector内部的实现, 这是CoreCLR中除了JIT以外最复杂部分,下面一些概念目前尚未有公开的文档和书籍讲到。
为了分析这部分我花了一个多月的时间,期间也多次向CoreCLR的开发组提问过,我有信心以下内容都是比较准确的,但如果你发现了错误或者有疑问的地方请指出来,
以下的内容基于CoreCLR 1.1.0的源代码分析,以后可能会有所改变。
因为内容过长,我分成了两篇,这一篇分析代码,下一篇实际使用LLDB跟踪GC收集垃圾的处理。
需要的预备知识
- 看过BOTR中GC设计的文档 原文 译文
- 看过我之前的系列文章, 碰到不明白的可以先跳过但最少需要看一遍
- 对c中的指针有一定了解
- 对常用数据结构有一定了解, 例如链表
- 对基础c++语法有一定了解, 高级语法和STL不需要因为微软只用了低级语法
GC的触发
GC一般在已预留的内存不够用或者已经分配量超过阈值时触发,场景包括:
不能给分配上下文指定新的空间时
当调用try_allocate_more_space不能从segment结尾或自由对象列表获取新的空间时会触发GC, 详细可以看我上一篇中分析的代码。
分配的数据量达到一定阈值时
阈值储存在各个heap的dd_min_gc_size(初始值), dd_desired_allocation(动态调整值), dd_new_allocation(消耗值)中,每次给分配上下文指定空间时会减少dd_new_allocation。
如果dd_new_allocation变为负数或者与dd_desired_allocation的比例小于一定值则触发GC,
触发完GC以后会重新调整dd_new_allocation到dd_desired_allocation。
参考new_allocation_limit, new_allocation_allowed和check_for_full_gc函数。
值得一提的是可以在.Net程序中使用GC.RegisterForFullGCNotification可以设置触发GC需要的dd_new_allocation / dd_desired_allocation的比例(会储存在fgn_maxgen_percent和fgn_loh_percent中), 设置一个大于0的比例可以让GC触发的更加频繁。
StressGC
允许手动设置特殊的GC触发策略, 参考这个文档
作为例子,你可以试着在运行程序前运行export COMPlus_GCStress=1
GCStrees会通过调用GCStress<gc_on_alloc>::MaybeTrigger(acontext)
触发,
如果你设置了COMPlus_GCStressStart环境变量,在调用MaybeTrigger一定次数后会强制触发GC,另外还有COMPlus_GCStressStartAtJit等参数,请参考上面的文档。
默认StressGC不会启用。
手动触发GC
在.Net程序中使用GC.Collect可以触发手动触发GC,我相信你们都知道。
调用.Net中的GC.Collect会调用CoreCLR中的GCHeap::GarbageCollect => GarbageCollectTry => GarbageCollectGeneration。
GC的处理
以下函数大部分都在gc.cpp里,在这个文件里的函数我就不一一标出文件了。
GC的入口点
GC的入口点是GCHeap::GarbageCollectGeneration函数,这个函数的主要作用是停止运行引擎和调用各个gc_heap的gc_heap::garbage_collect函数
因为这一篇重点在于GC做出的处理,我将不对如何停止运行引擎和后台GC做出详细的解释,希望以后可以再写一篇文章讲述
// 第一个参数是回收垃圾的代, 例如等于1时会回收gen 0和gen 1的垃圾
// 第二个参数是触发GC的原因
size_t
GCHeap::GarbageCollectGeneration (unsigned int gen, gc_reason reason)
{
dprintf (2, ("triggered a GC!"));
// 获取gc_heap实例,意义不大
#ifdef MULTIPLE_HEAPS
gc_heap* hpt = gc_heap::g_heaps[0];
#else
gc_heap* hpt = 0;
#endif //MULTIPLE_HEAPS
// 获取当前线程和dd数据
Thread* current_thread = GetThread();
BOOL cooperative_mode = TRUE;
dynamic_data* dd = hpt->dynamic_data_of (gen);
size_t localCount = dd_collection_count (dd);
// 获取GC锁, 防止重复触发GC
enter_spin_lock (&gc_heap::gc_lock);
dprintf (SPINLOCK_LOG, ("GC Egc"));
ASSERT_HOLDING_SPIN_LOCK(&gc_heap::gc_lock);
//don't trigger another GC if one was already in progress
//while waiting for the lock
{
size_t col_count = dd_collection_count (dd);
if (localCount != col_count)
{
#ifdef SYNCHRONIZATION_STATS
gc_lock_contended++;
#endif //SYNCHRONIZATION_STATS
dprintf (SPINLOCK_LOG, ("no need GC Lgc"));
leave_spin_lock (&gc_heap::gc_lock);
// We don't need to release msl here 'cause this means a GC
// has happened and would have release all msl's.
return col_count;
}
}
// 统计GC的开始时间(包括停止运行引擎使用的时间)
#ifdef COUNT_CYCLES
int gc_start = GetCycleCount32();
#endif //COUNT_CYCLES
#ifdef TRACE_GC
#ifdef COUNT_CYCLES
AllocDuration += GetCycleCount32() - AllocStart;
#else
AllocDuration += clock() - AllocStart;
#endif //COUNT_CYCLES
#endif //TRACE_GC
// 设置触发GC的原因
gc_heap::g_low_memory_status = (reason == reason_lowmemory) ||
(reason == reason_lowmemory_blocking) ||
g_bLowMemoryFromHost;
if (g_bLowMemoryFromHost)
reason = reason_lowmemory_host;
gc_trigger_reason = reason;
// 重设GC结束的事件
// 以下说的"事件"的作用和"信号量", .Net中的"Monitor"一样
#ifdef MULTIPLE_HEAPS
for (int i = 0; i < gc_heap::n_heaps; i++)
{
gc_heap::g_heaps[i]->reset_gc_done();
}
#else
gc_heap::reset_gc_done();
#endif //MULTIPLE_HEAPS
// 标记gc已开始, 全局静态变量
gc_heap::gc_started = TRUE;
// 停止运行引擎
{
init_sync_log_stats();
#ifndef MULTIPLE_HEAPS
// 让当前线程进入preemptive模式
// 最终会调用Thread::EnablePreemptiveGC
// 设置线程的m_fPreemptiveGCDisabled等于0
cooperative_mode = gc_heap::enable_preemptive (current_thread);
dprintf (2, ("Suspending EE"));
BEGIN_TIMING(suspend_ee_during_log);
// 停止运行引擎,这里我只做简单解释
// - 调用ThreadSuspend::SuspendEE
// - 调用LockThreadStore锁住线程集合直到RestartEE
// - 设置GCHeap中全局事件WaitForGCEvent
// - 调用ThreadStore::TrapReturingThreads
// - 设置全局变量g_TrapReturningThreads,jit会生成检查这个全局变量的代码
// - 调用SuspendRuntime, 停止除了当前线程以外的线程,如果线程在cooperative模式则劫持并停止,如果线程在preemptive模式则阻止进入cooperative模式
GCToEEInterface::SuspendEE(GCToEEInterface::SUSPEND_FOR_GC);
END_TIMING(suspend_ee_during_log);
// 再次判断是否应该执行gc
// 目前如果设置了NoGCRegion(gc_heap::settings.pause_mode == pause_no_gc)则会进一步检查
// https://msdn.microsoft.com/en-us/library/system.runtime.gclatencymode(v=vs.110).aspx
gc_heap::proceed_with_gc_p = gc_heap::should_proceed_with_gc();
// 设置当前线程离开preemptive模式
gc_heap::disable_preemptive (current_thread, cooperative_mode);
if (gc_heap::proceed_with_gc_p)
pGenGCHeap->settings.init_mechanisms();
else
gc_heap::update_collection_counts_for_no_gc();
#endif //!MULTIPLE_HEAPS
}
// MAP_EVENT_MONITORS(EE_MONITOR_GARBAGE_COLLECTIONS, NotifyEvent(EE_EVENT_TYPE_GC_STARTED, 0));
// 统计GC的开始时间
#ifdef TRACE_GC
#ifdef COUNT_CYCLES
unsigned start;
unsigned finish;
start = GetCycleCount32();
#else
clock_t start;
clock_t finish;
start = clock();
#endif //COUNT_CYCLES
PromotedObjectCount = 0;
#endif //TRACE_GC
// 当前收集代的序号
// 后面看到condemned generation时都表示"当前收集代"
unsigned int condemned_generation_number = gen;
// We want to get a stack from the user thread that triggered the GC
// instead of on the GC thread which is the case for Server GC.
// But we are doing it for Workstation GC as well to be uniform.
FireEtwGCTriggered((int) reason, GetClrInstanceId());
// 进入GC处理
// 如果有多个heap(服务器GC),可以使用各个heap的线程并行处理
// 如果只有一个heap(工作站GC),直接在当前线程处理
#ifdef MULTIPLE_HEAPS
GcCondemnedGeneration = condemned_generation_number;
// 当前线程进入preemptive模式
cooperative_mode = gc_heap::enable_preemptive (current_thread);
BEGIN_TIMING(gc_during_log);
// gc_heap::gc_thread_function在收到这个信号以后会进入GC处理
// 在里面也会判断proceed_with_gc_p
gc_heap::ee_suspend_event.Set();
// 等待所有线程处理完毕
gc_heap::wait_for_gc_done();
END_TIMING(gc_during_log);
// 当前线程离开preemptive模式
gc_heap::disable_preemptive (current_thread, cooperative_mode);
condemned_generation_number = GcCondemnedGeneration;
#else
// 在当前线程中进入GC处理
if (gc_heap::proceed_with_gc_p)
{
BEGIN_TIMING(gc_during_log);
pGenGCHeap->garbage_collect (condemned_generation_number);
END_TIMING(gc_during_log);
}
#endif