CoreCLR源码探索(五) GC内存收集器的内部实现 调试篇
在上一篇中我分析了CoreCLR中GC的内部处理,
在这一篇我将使用LLDB实际跟踪CoreCLR中GC,关于如何使用LLDB调试CoreCLR的介绍可以看:
源代码
本篇跟踪程序的源代码如下:
using System;
using System.Runtime.InteropServices;
namespace ConsoleApplication
{
public class Program
{
public class ClassA { }
public class ClassB { }
public class ClassC { }
public static void Main(string[] args)
{
var a = new ClassA();
{ var b = new ClassB(); }
var c = new ClassC();
GCHandle handle = GCHandle.Alloc(c, GCHandleType.Pinned);
IntPtr address = handle.AddrOfPinnedObject();
Console.WriteLine((long)address);
GC.Collect();
Console.WriteLine("first collect completed");
c = null;
GC.Collect();
Console.WriteLine("second collect completed");
GC.Collect();
Console.WriteLine("third collect completed");
}
}
}
准备调试
环境和我的第三篇文章一样,都是ubuntu 16.04 LTS,首先需要发布程序:
dotnet publish
发布程序后,把自己编译的coreclr文件覆盖到发布目录中:
复制coreclr/bin/Product/Linux.x64.Debug下的文件到程序目录/bin/Debug/netcoreapp1.1/ubuntu.16.04-x64/publish下。
请不要设置开启服务器GC,一来是这篇文章分析的是工作站GC的处理,二来开启服务器GC很容易导致调试时死锁。
进入调试
准备工作完成以后就可以进入调试了
cd 程序目录/bin/Debug/netcoreapp1.1/ubuntu.16.04-x64/publish
lldb-3.6 程序名称

首先设置gc主函数的断点,然后运行程序
b gc1
r

我们停在了gc1函数,现在可以用bt来看调用来源

这次是手动触发GC,调用来源中包含了GCInterface::Collect和JIT生成的函数
需要显示当前的本地变量可以用fr v,需要打印变量或者表达式可以用p

现在用n来步过,用s来步进继续跟踪代码

进入标记阶段
在上图的位置中用s命令即可进入mark_phase,继续步过到下图的位置

这时先让我们看下堆中的对象,加载CoreCLR提供的LLDB插件
plugin load libsosplugin.so
插件提供的命令可以查看这里的文档
执行dumpheap查看堆中的状态


执行dso查看堆和寄存器中引用的对象

执行dumpobj查看对象的信息

在这一轮gc中对象a b c都会存活下来,
可能你会对为什么b能存活下来感到惊讶,对象b的引用分配在栈上,即时生命周期过了也不一定会失效(rsp不会移回去)
br s -n Promote -c "(long)*ppObject == 0x00007fff5c01a2b8" # -n 名称 -c 条件
c # 继续执行

接下来步进mark_object_simple函数,然后步进gc_mark1函数

me re -s8 -c3 -fx o # 显示地址中的内存,8个字节一组,3组,hex格式,地址是o
p ((CObjectHeader*)o)->IsMarked() # 显示对象是否标记存活
我们可以清楚的看到标记对象存活设置了MethodTable的指针|= 1
现在给PinObject下断点
br s -n PinObject -c "(long)*pObjRef == 0x00007fff5c01a1a0"
c

可以看到只是调用Promote然后传入GC_CALL_PINNED
继续步进到if (flags & GC_CALL_PINNED)下的pin_object

可以看到pinned标记设置在同步索引块中
进入计划阶段
进入计划阶段后首先打印一下各个代的状态
p generation_table
使用这个命令可以看到gen 0 ~ gen 3的状态,最后一个元素是空元素不用在意

继续步过下去到下图的这一段

在这里我们找到了一个plug的开始,然后枚举已标记的对象,下图是擦除marked和pinned标记的代码

在这里我们找到了一个plug的结束

如果是Full GC或者不升代,在处理第一个plug之前就会设置gen 2的计划代边界

模拟压缩的地址

如果x越过原来的gen 0的边界,设置gen 1的计划代边界(原gen 1的对象变gen 2),
如果不升代这里也会设置gen 0的计划代边界

模拟压缩后把原地址与压缩到的地址的偏移值存到plug信息(plug前的一块内存)中

构建plug树

设置brick表,这个plug树跨了6个brick


如果升代,模拟压缩全部完成后设置gen 0的计划代边界

接下来如果不动里面的变量,将会进入清扫阶段(不满足进入压缩阶段的条件)
进入清扫阶段
这次为了观察对象c如何被清扫,我们进入第二次gc的make_free_lists
b make_free_lists
c
处理当前brick中的plug树

前面看到的对象c的地址是0x00007fff5c01a2e8,这里我们就看对象c后面的plug是如何处理的
br s -f gc.cpp -l 23070 -c "(long)tree > 0x00007fff5c01a2e8"
c
我们可以看到plug 0x00007fff5c01a300前面的空余空间中包含了对象c,空余空间的开始地址就是对象c

接下来就是在这片空余空间中创建free object和加到free list了,
这里的大小不足(< min_free_list)所以只会创建free object不会加到free list中

设置代边界,之前计划阶段模拟的计划代边界不会被使用

清扫阶段完成后这次的gc的主要工作就完成了,接下来让我们看重定位阶段和压缩阶段
进入重定位阶段
使用上面的程序让计划阶段选择压缩,需要修改变量,这里重新运行程序并使用以下命令
b gc.cpp:22489
c
expr should_compact = true

n步过到下图的位置,s步进到relocate_phase函数

到这个位置可以看到用了和标记阶段一样的GcScanRoots函数,但是传入的不是Promote而是Relocate函数

接下来下断点进入Relocate函数
b Relocate
c
GCHeap::Relocate函数不会重定位子对象,只是用来重定位来源于根对象的引用

一直走到这个位置然后进入gc_heap::relocate_address函数

根据原地址和brick table找到对应的plug树

搜索plug树中old_address所属的plug

根据plug中的reloc修改指针地址

现在再来看relocate_survivors函数,这个函数用于重定位存活下来的对象中的引用
b relocate_survivors
c

接下来会枚举并处理brick,走到这里进入relocate_survivors_in_brick函数,这个函数处理单个brick中的plug树

递归处理plug树种的各个节点

走到这里进入relocate_survivors_in_plug函数,这个函数处理单个plug中的对象

图中的这个plug结尾被下一个plug覆盖过,需要特殊处理,这里继续进入relocate_shortened_survivor_helper函数

当前是unpinned plug,下一个plug是pinned plug

枚举处理plug中的各个对象

如果这个对象结尾未被覆盖,则调用relocate_obj_helper重定位对象中的各个成员


如果对象结尾被覆盖了,则调用relocate_shortened_obj_helper重定位对象中的各个成员
在这里成员如果被覆盖会调用reloc_ref_in_shortened_obj修改备份数据中的成员,但是因为go_through_object_nostart是一个macro这里无法调试内部的代码

接下来我们观察对象a的地址是否改变了
重新运行并修改should_compact变量
b gc.cpp:22489
r
expr should_compact = true
plugin load libsosplugin.so
dso
我们可以看到对象a的地址在0x00007fff5c01a2b8,接下来给relocate_address函数下断点

br s -n relocate_address -c "(long)(*pold_address) == 0x00007fff5c01a2b8"
c

我们可以看到地址由0x00007fff5c01a2b8变成了0x00007fff5c0091b8

接下来一直跳回plan_phase,下图可以看到重定位阶段完成以后新的地址上仍无对象,重定位阶段只是修改了地址并未复制内存,直到压缩阶段完成以后对象才会在新的地址

接下来看压缩阶段
进入压缩阶段
在重定位阶段完成以后走到下图的位置,步进即可进入压缩阶段

枚举brick table

处理单个brick table中的plug树

根据下一个tree的gap计算last_plug的大小

处理单个plug中的对象

上面的last_plug是pinned plug所以不移动,这里找了另外一个会移动的plug

下图可以看到整个plug都被复制到新的地址

这里再找一个结尾被覆盖过的plug看看是怎么处理的

首先把被覆盖的结尾大小加回去

然后把被覆盖的内容临时恢复回去


复制完再把覆盖的内容交换回来,因为下一个plug还需要用

最终在recover_saved_pinned_info会全部恢复回去
参考链接
https://github.com/dotnet/coreclr/blob/master/Documentation/botr/garbage-collection.md
https://github.com/dotnet/coreclr/blob/release/1.1.0/Documentation/building/linux-instructions.md
https://github.com/dotnet/coreclr/blob/release/1.1.0/Documentation/building/debugging-instructions.md
http://lldb.llvm.org/tutorial.html
http://lldb.llvm.org/lldb-gdb.html
写在最后
这一篇中我列出了几个gc中比较关键的部分,但是还有成千上百处可以探讨的部分,
如果你有兴趣可以自己试着用lldb调试CoreCLR,可以学到很多文档和书籍之外的知识,
特别是对于CoreCLR这种文档少注释也少的项目,掌握调试工具可以大幅减少理解代码所需的时间
写完这一篇我将暂停研究GC,下一篇开始会介绍JIT相关的内容,敬请期待
CoreCLR源码探索(五) GC内存收集器的内部实现 调试篇的更多相关文章
- CoreCLR源码探索(四) GC内存收集器的内部实现 分析篇
在这篇中我将讲述GC Collector内部的实现, 这是CoreCLR中除了JIT以外最复杂部分,下面一些概念目前尚未有公开的文档和书籍讲到. 为了分析这部分我花了一个多月的时间,期间也多次向Cor ...
- CoreCLR源码探索(三) GC内存分配器的内部实现
在前一篇中我讲解了new是怎么工作的, 但是却一笔跳过了内存分配相关的部分. 在这一篇中我将详细讲解GC内存分配器的内部实现. 在看这一篇之前请必须先看完微软BOTR文档中的"Garbage ...
- Golang源码探索(三) GC的实现原理(转)
Golang从1.5开始引入了三色GC, 经过多次改进, 当前的1.9版本的GC停顿时间已经可以做到极短.停顿时间的减少意味着"最大响应时间"的缩短, 这也让go更适合编写网络服务 ...
- Golang源码探索(三) GC的实现原理
Golang从1.5开始引入了三色GC, 经过多次改进, 当前的1.9版本的GC停顿时间已经可以做到极短. 停顿时间的减少意味着"最大响应时间"的缩短, 这也让go更适合编写网络服 ...
- CoreCLR源码探索(一) Object是什么
.Net程序员们每天都在和Object在打交道 如果你问一个.Net程序员什么是Object,他可能会信誓旦旦的告诉你"Object还不简单吗,就是所有类型的基类" 这个答案是对的 ...
- CoreCLR源码探索(二) new是什么
前一篇我们看到了CoreCLR中对Object的定义,这一篇我们将会看CoreCLR中对new的定义和处理 new对于.Net程序员们来说同样是耳熟能详的关键词,我们每天都会用到new,然而new究竟 ...
- CoreCLR源码探索(六) NullReferenceException是如何发生的
NullReferenceException可能是.Net程序员遇到最多的例外了, 这个例外发生的如此频繁, 以至于人们付出了巨大的努力来使用各种特性和约束试图防止它发生, 但时至今日它仍然让很多程序 ...
- CoreCLR源码探索(七) JIT的工作原理(入门篇)
很多C#的初学者都会有这么一个疑问, .Net程序代码是如何被机器加载执行的? 最简单的解答是, C#会通过编译器(CodeDom, Roslyn)编译成IL代码, 然后CLR(.Net Framew ...
- CoreCLR源码探索(八) JIT的工作原理(详解篇)
在上一篇我们对CoreCLR中的JIT有了一个基础的了解, 这一篇我们将更详细分析JIT的实现. JIT的实现代码主要在https://github.com/dotnet/coreclr/tree/m ...
随机推荐
- C#下控制台程序窗口下启用快速编辑模式运行线程会阻止线程运行
最近做一个小的功能,使用C#控制台程序开启一个线程进行无限循环没5秒处理一次程序,发现控制台窗口在开启快速编辑模式情况下,进行选择程序打印 出来的文字后发现线程不走了,将快速编辑模式去除后,线程就不会 ...
- 前端基本知识(二):JS的原始链的理解
之前一直对于前端的基本知识不是了解很详细,基本功不扎实,但是前端开发中的基本知识才是以后职业发展的根基,虽然自己总是以一种实践是检验真理的唯一标准,写代码实践项目才是唯一,但是经常遇到知道怎么去解决这 ...
- esri-leaflet部分瓦片缺失问题及解决办法
esri-leaflet加载TileLayer的时候,有时候由于数据的原因,造成部分瓦片缺失的问题,网页加载TileLayer的时候,当地图范围正好拖动到缺失的范围的时候,会一直请求 http://d ...
- MongoDB学习总结(一) —— Windows平台下安装
> 基本概念 MongoDB是一个基于分布式文件存储的开源数据库系统,皆在为WEB应用提供可扩展的高性能数据存储解决方案.MongoDB将数据存储为一个文档,数据结构由键值key=>val ...
- web前端简介
Web标准: 结构(硬件):xhtml html 表现(软件):css 行为(插件):dom js html:超文本标记语言 (Hyper Text Markup Language) xhtml:可 ...
- JavaScript中的this关键字的用法和注意点
JavaScript中的this关键字的用法和注意点 一.this关键字的用法 this一般用于指向对象(绑定对象); 01.在普通函数调用中,其内部的this指向全局对象(window); func ...
- 【java基础之jdk源码】Object
最新在整体回归下java基础薄弱环节,以下为自己整理笔记,若有理解错误,请批评指正,谢谢. java.lang.Object为java所有类的基类,所以一般的类都可用重写或直接使用Object下方法, ...
- android学习18——对PathMeasure中getPosTan的理解
考虑这样的场景:要实现物体沿直接或曲线运动的效果.这就要算出某个时刻t,物体的坐标.getPosTan就是用来求坐标的.看下面的代码: float step = 0.0001f; Path path ...
- UWP: ListView 中与滚动有关的两个需求的实现
在 App 的开发过程中,ListView 控件是比较常用的控件之一.掌握它的用法,能帮助我们在一定程度上提高开发效率.本文将会介绍 ListView 的一种用法--获取并设置 ListView 的滚 ...
- Error: Cannot find module 'gulp-clone'问题的解决
安装完gulp环境,并且配置好gulpfile.js,执行静态文件压缩和代码混淆时,出现如下错误: Error: Cannot find module 'gulp-clone' Error: Cann ...