在上一篇中我分析了CoreCLR中GC的内部处理,

在这一篇我将使用LLDB实际跟踪CoreCLR中GC,关于如何使用LLDB调试CoreCLR的介绍可以看:

  • 微软官方的文档,地址
  • 我在第3篇中的介绍,地址
  • LLDB官方的入门文档,地址

源代码

本篇跟踪程序的源代码如下:

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内存收集器的内部实现 调试篇的更多相关文章

  1. CoreCLR源码探索(四) GC内存收集器的内部实现 分析篇

    在这篇中我将讲述GC Collector内部的实现, 这是CoreCLR中除了JIT以外最复杂部分,下面一些概念目前尚未有公开的文档和书籍讲到. 为了分析这部分我花了一个多月的时间,期间也多次向Cor ...

  2. CoreCLR源码探索(三) GC内存分配器的内部实现

    在前一篇中我讲解了new是怎么工作的, 但是却一笔跳过了内存分配相关的部分. 在这一篇中我将详细讲解GC内存分配器的内部实现. 在看这一篇之前请必须先看完微软BOTR文档中的"Garbage ...

  3. Golang源码探索(三) GC的实现原理(转)

    Golang从1.5开始引入了三色GC, 经过多次改进, 当前的1.9版本的GC停顿时间已经可以做到极短.停顿时间的减少意味着"最大响应时间"的缩短, 这也让go更适合编写网络服务 ...

  4. Golang源码探索(三) GC的实现原理

    Golang从1.5开始引入了三色GC, 经过多次改进, 当前的1.9版本的GC停顿时间已经可以做到极短. 停顿时间的减少意味着"最大响应时间"的缩短, 这也让go更适合编写网络服 ...

  5. CoreCLR源码探索(一) Object是什么

    .Net程序员们每天都在和Object在打交道 如果你问一个.Net程序员什么是Object,他可能会信誓旦旦的告诉你"Object还不简单吗,就是所有类型的基类" 这个答案是对的 ...

  6. CoreCLR源码探索(二) new是什么

    前一篇我们看到了CoreCLR中对Object的定义,这一篇我们将会看CoreCLR中对new的定义和处理 new对于.Net程序员们来说同样是耳熟能详的关键词,我们每天都会用到new,然而new究竟 ...

  7. CoreCLR源码探索(六) NullReferenceException是如何发生的

    NullReferenceException可能是.Net程序员遇到最多的例外了, 这个例外发生的如此频繁, 以至于人们付出了巨大的努力来使用各种特性和约束试图防止它发生, 但时至今日它仍然让很多程序 ...

  8. CoreCLR源码探索(七) JIT的工作原理(入门篇)

    很多C#的初学者都会有这么一个疑问, .Net程序代码是如何被机器加载执行的? 最简单的解答是, C#会通过编译器(CodeDom, Roslyn)编译成IL代码, 然后CLR(.Net Framew ...

  9. CoreCLR源码探索(八) JIT的工作原理(详解篇)

    在上一篇我们对CoreCLR中的JIT有了一个基础的了解, 这一篇我们将更详细分析JIT的实现. JIT的实现代码主要在https://github.com/dotnet/coreclr/tree/m ...

随机推荐

  1. redis安装-单机版

    环境准备 因为redis使用c语言开发的,如果要运行在linux上,需要gcc-c++的环境.那么我们就要先看一下是否安装了gcc-c++如果没有的话,需要在虚拟机上安装gcc-c++环境(友情提示, ...

  2. java 继承的学习(转)

    转自:http://www.cnblogs.com/happyframework/p/3332243.html,非常感谢啊 public class test { /** * @param args ...

  3. Dirty Flag 模式及其应用

    之前在开发中就发现"dirty"是一种不错的解决方案:可以用来延缓计算或者避免不必要的计算.后来在想,这应该也算一种设计模式吧,于是搜索"Dirty设计模式", ...

  4. 动态样式语言—LESS

    博客原文地址:Claiyre的个人博客 https://claiyre.github.io/ 博客园地址:http://www.cnblogs.com/nuannuan7362/ 如需转载,请在文章开 ...

  5. SpringMVC简版教程、部分功能

    注:本文只用注解来实现 前言 SpringMVC各种流程图流程图(其他的各种流程图) jsp.xml.action彼此之间的关系,都如何使用 spring-mvc.xml如何配置,放在哪里? acti ...

  6. Spring之IOC讲解

    一.SpringIOC的好处: ioc的思想最核心的地方在于,资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处. 1.资源集中管理,实现资源的可配置和易管理. 2.降低了使用 ...

  7. 【前端】:jQuery上

    前言: 今天写一篇jQuery,发现内容太多了,那就分成两篇写吧--写完jQuery基础知识后会再写一些jQuery实例~~ jQuery下载.jQuery是一个兼容多浏览器的javascript库, ...

  8. 网站Web业务架构从小到大演变

    有一天,我突发奇想创建了一个站点,基于 LNMP 架构,起初只有我自己访问,后来因为我点儿正,访问量越来越大,所以最终导致下面的架构演变. 1.单台机器 单台机器因为只是一个小站,访问量一天也没有多少 ...

  9. 智能打印SDK-源码剖析

    开源地址:http://www.dnnode.com/ 软件下载地址:http://www.dnnode.com/在线展示:http://www.dnnode.com/help.html 前面的文章, ...

  10. 2017-2-28 C#基础 数组

    1.什么是数组? 数组就是具有相同数据类型变量的集合. 2.数组的作用:操作大量数据. 3.数组的定义要求:(1)数组里面的内容必须是同一类型.(2)数组必须有长度限制. 4.数组分为一维数组,二维数 ...