虽然看了一些书,还网络上的一些博文,不过对CLR托管内存细节依然比较模糊。而且因为工作原因总会有很多质疑,想要亲眼看到内存里二进制数据的变化。

所以借助winhex直接查看内存以证实书上的描述或更进一步揣摩CLR托管内存的运作方式,这里写下来跟大家一起分享(由于自己这方面知识储备不太充足,下面的好多内容也是猜测,肯定有很对错误,希望了解的网友可以帮忙指正)

测试环境: windowsXP win10 win7 (dotnet4.0 Releases编译 ,下文截图为win7上的运行结果)

内存查看工具: winhex 7.5

虽然重点是监测二进制的内存,不过基本的测试代码还是要有的(测试是直接运行编译好的exe,没有使用调试模式,编译时要使用Releases,因为debug跟Releases在GC回收时对象是否可达的判断是不一样的)

下面对内存的查找部分看起来可能有点跳跃,因为是借助了反复测试得到的规律,很多过程没有赘述

进行之前需要先简单了解CLR对象分配(类型对象指针要知道),GC的基本过程(G0,G1,G2需要简单了解),二进制数据的存储(主要是大小端)

下图是测试中用到的引用对象的结构

下图为测试的主要步骤,会分8步进行,每一步也都标注出来了

 第1监测点

 
 
通过TestForGC_3对象里的值类型87564023523 (hex 146338F6E3 ,windows为小端存储,所以最后搜索E3 F6 38 63 14)
前面的78 41 9B 92 为32位类型对象指针 后面接着的是同步块索引 (如果是64位程序这2个数据则都将是64位)
 
 
 
根据前面TestForGC_3的地址70354802,我们在内存里搜索到了唯一的一个匹配项,说明这里一定是张表,这个指针指向了TestForGC_3的地址
 
 
 
这个就是 NextObjPtr 了  (这个也只是推测,根据后面的操作发现新增对象末尾地址总是002BFB
48里面的数据,以及之前的反复测试前面的类型指针也是匹配的,但测试结果并不是每次这个类型指针都是这个值,并且在不同系统版本下差距非常大)
后面的操作大家可以看到它的确就是NextObjPtr ,整个内存块里存着这个地址的位置也只有这里)【在托管堆中维护着一个NextObjPtr指针,指向下一个新建对象分配时在托管堆中所处的位置】
 

第2监测点
 
 
按照顺序我们通过内存搜索先找到了a1的地址
这里顺便解析下对象(引用类型)在内存里的存储
最前面8字节为类型对象指针及同步块索引(每个32位,如果是64位应用则每个64位)
类型对象指针不是一成不变的,就是dotnet内置的类型也不能保证,这次运行是一个值(地址),另外一个实例运行起来可能是另外一个(地址) (这里的地址全部使用偏移地址)
后面接着的3个8字节数据发布是TypeA里3个引用类型变量的地址,可以看到第2个地址就紧接在下面(因为是一起分配的)
顺便看下string类型在内存里的存储
 
 
 
根据a1里面的存储的地址02483588,轻松定位到了“testtypea”字符串
同样与其他对象一样拥有类型对象指针,同步索引块,后面有4个字节的数据长度,然后后面跟数据
这也的确说明了string千真万确引用类型,毫无争议
最后TypeA里面还有一个引用对象TypeB,是一样的就不重复说了,不过TypeB的指针只存在a1里面(即他的回收确实也只能靠根搜索)
 
 
 
现在我们通过a1的位置,查找内存中含有其地址的内存,居然搜索到了5块内存,而且都靠的非常近
 
 
 
 
同样的方法搜索到bytesStart在内存里的地址
同样的结构类型指针,同步索引,后面跟8字节的长度,再后面就是数据
 
 
 
根据地址搜索bytesStart在内存里的指针,也只有1个(这种结果在同样环境下运行每次的表现都是已有的,不过在更换运行环境后就会有明显差异),而且也紧靠着a1的指针(可以推断他们确实是在一张“表”上)
 
 
 
现在看下刚刚找到的NextObjPtr里面的数据是多少(02486988),下面我们看下这个地址里是什么
 
 
 
可以看到就指向了最后分配的bytesStart地址的后面
每个引用对象后面都有8个全0的字节(多次测试,反复分析数据都是这样)
 

监测点3 (为了验证刚刚的NextObjPtr的确是那块内存)
 
 
到第3步,可以直观的看到bytesThen就直接使用了刚刚NextObjPtr后面指向的内存,
 
 
 
 
同时也看到NextObjPtr指示下的一片内存(这个时候对A0 6D 48 02 的搜索也证明内存里只有这么一块存的是这个数据)
而且可以看到这个地址确实就是bytesThen后面的内存地址
 

第4监测点 (重复创建10分份的typeA)
 
 
这次直接使用类型对象指针搜索新创建的10份TypeA (可能会搜索出其他数据,因为内存里有其他程序及测试前几次运行残留的数据)
可以看到这些TypeA直接分配在了bytesThen的后面(测试中尽量少使用终端打印。终端打印虽然1行代码,不过clr会创建很多对象去完成打印,不方便观察)
 
 
 
 
现在想知道这些TypeA的指针,却发现内存里根本没有这个地址(后面9个的结果一样)
甚至连里面的TypeB的指针也搜索不到
 
其实这些TypeA从一创建即为不可达,因为后面再也没有用到它们的地方,即一开始就没有任何对象引用过他们,在引用跟踪里一直被作为垃圾
 
 
 
这个时候NextObjPtr 已经指向了02487520
 

监测点5 (出RunCreat)
 
 
 
出RunCreat()这个方法回到RunTest()里,NextObjPtr指向依然没有变化(没有新的对象创建)。
 

监测点6 (回收G0)
 
 
执行完G0回收后 ,NextObjPtr直接变为了全0 (其实后面还有跟的8个字节的数据也变为了0,这8个字节可能为G0阀值)
 
 
 这里有个疏忽,本来先要监测a1的回收,现在发现后面的代码残留上一次的测试代码错误的把a1引用了, 所以要到这一行结束a1是垃圾
 
 
 
经过G0回收后,bytesStart  bytesThen,应该移动到G1,不过看他们在内存里的位置并没有发生任何变化(内存里也只有这一份)
那10个在RunCreat创建的TypeA也似乎没有什么变化
 
关于书上的描述跟图例,似乎在GC完成后,G0向G1的代提升会移动内存,不过现在看来并没有移动内存(目前GC把85000字节的数据当作大对象,所以这里的bytesStart  也不是大对象)
 
 
那如果bytesStart  bytesThen是存活的,不能回收,那下一个NextObjPtr也一定紧接着在bytesThen后面
整个内存符合条件的也就这么一处(即使是搜索?? 6D 48 02 也只有这一块内存符合条件),虽然这块内存看起来没有什么特别的格式
 

监测点7 (下一个地址从什么地方开始分配)
 
 
 
可以看到bytes这个全a的数据真的是从刚刚推测的地址开始分配内存的,在RunCreat创建的TypeA也直接被覆盖了(确实被当作了垃圾)
 
 
 
NextObjPtr现在也正常的指向了bytes的后面
 

监测点8
 
 
 
 
数据确实在里面被改动了,而且bytesStart  bytesThen也的确处于G1
 

监测点9
 这次回收除了a1 其他的bytesStart  bytesThen bytes ,应该都会被回收
 
 
之前放着bytesStart  bytesThen bytes 指针的内存 数据已经被覆盖了,现在他们都被移动到另外一块内存
而a1 现在应该由g1提升到了g2
 
 
 
之前存放a1指针的地址也全部被覆盖了,在内存里搜索到4块新内存,其中一块还与前面的bytesStart  bytesThen 新指针放在一起
 
 
 
虽然现在NextObjPtr 现在不为0 ,但也明显被重置了 (因为打印的缘故,GC后马上创建了新的对象)
也很明显,并没有覆盖前面的内存,而是直接指向了后面的内存
 
 
现在来看刚刚被认为是标记GC后下一次分配内存的地址的的内存块,现在的地址02488A88,这个地址也十分合理,正好在两次NextObjPtr 的中间
标识这个地址的确是标记GC后 新NextObjPtr的初始值
 

监测结束

跳出RunTest,马上就执行了一次完全的GC

上面写的比较杂乱,虽然很对东西还是没有弄明白也没有发现什么规律,不过至少可以得到下面的一些结果

1:证明了NextObjPtr 的存在,也了解他的基本行为(其结构后面数据可能还包含G0阈值等其他数据)

2:GC回收使用的标记方法的确是根搜索

3:被回收的内存不会被擦除,只是通过移动NextObjPtr标记下一个内存能被分配的位置

4:对象从G0移动到G1,内存本身不会移动(可能记录对象的指针的表会有相应更新)

5:不是每次回收都会压缩内存,大部分时间都维持原有结构

6:对象在内存中的存储细节

最后上面写了那么多,其实不单单就是为了看CLR物理内存,同样也是表达一种方法,用同样的方法也可以查看包括jvm在内的几乎所有进程的物理内存,同时winhex不仅可以查看,还拥有在运行时直接修改物理内存的能力。

在物理内存中观察CLR托管内存及GC行为的更多相关文章

  1. 在内存中观察CRL托管内存及GC行为

    虽然看了一些书,还网络上的一些博文,不过对CRL托管内存的介绍都不是十分清楚,大部分都是一样的,如果再要了解细节就十分困难了. 所以借助winhex直接查看内存以证实书上的描述或更进一步揣摩CRL托管 ...

  2. CLR托管内存

    在物理内存中观察CLR托管内存及GC行为   虽然看了一些书,还网络上的一些博文,不过对CLR托管内存细节依然比较模糊.而且因为工作原因总会有很多质疑,想要亲眼看到内存里二进制数据的变化. 所以借助w ...

  3. VC++中的类的内存分布(上)(通过强制转换,观察地址,以及地址里的值来判断)

    0.序 目前正在学习C++中,对于C++的类及其类的实现原理也挺感兴趣.于是打算通过观察类在内存中的分布更好地理解类的实现.因为其实类的分布是由编译器决定的,而本次试验使用的编译器为VS2015 RC ...

  4. C# 中托管内存与非托管内存之间的转换

    c#有自己的内存回收机制,所以在c#中我们可以只new,不用关心怎样delete,c#使用gc来清理内存,这部分内存就是managed memory,大部分时候我们工作于c#环境中,都是在使用托管内存 ...

  5. 浅入 .NET Core 中的内存和GC知识

    目录 托管代码 自动内存管理 参考资料: [1]https://docs.microsoft.com/zh-cn/dotnet/standard/managed-code [2]:https://do ...

  6. 深入了解C#系列:谈谈C#中垃圾回收与内存管理机制

    今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...

  7. C#中垃圾回收与内存管理机制

    今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...

  8. 记一次 .NET 某桌面奇侠游戏 非托管内存泄漏分析

    一:背景 1. 讲故事 说实话,这篇dump我本来是不准备上一篇文章来解读的,但它有两点深深的感动了我. 无数次的听说用 Unity 可做游戏开发,但百闻不如一见. 游戏中有很多金庸武侠小说才有的名字 ...

  9. PerfView专题 (第二篇):如何寻找 C# 中的 Heap堆内存泄漏

    一:背景 上一篇我们聊到了如何去找 热点函数,这一篇我们来看下当你的程序出现了 非托管内存泄漏 时如何去寻找可疑的代码源头,其实思路很简单,就是在 HeapAlloc 或者 VirtualAlloc ...

随机推荐

  1. Django学习笔记(5)——cookie和session

    一,前言 1.1,什么是会话跟踪技术 在JavaWeb中,客户向某一服务器发出第一个请求开始,会话就开始了,直到客户关闭了浏览器会话结束.在一个会话的多个请求中共享数据,这就是会话跟踪技术. 例如在一 ...

  2. 【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析

    当当当当当当当,好久不见,最近又是换工作,又是换房子,忙的不可开交,断更了一小段时间,最重要的一篇迟迟出不来,每次都犹抱琵琶半遮面,想要把它用通俗易懂的方式进行说明,确实有一定的难度,可愁煞我也,但自 ...

  3. 如何正确使用Java泛型

    前言 Java 1.5之前是没有泛型的,以前从集合中读取每个对象都必须先进行转换,如果不小心存入集合中对象类型是错的,运行过程中转换处理会报错.有了泛型之后编译器会自动帮助转换,使程序更加安全,但是要 ...

  4. Django 系列博客(十六)

    Django 系列博客(十六) 前言 本篇博客介绍 Django 的 forms 组件. 基本属性介绍 创建 forms 类时,主要涉及到字段和插件,字段用于对用户请求数据的验证,插件用于自动生成 h ...

  5. .Net语言 APP开发平台——Smobiler学习日志:在手机应用开发中如何快速调用电话拨打功能

    样式一 一.目标样式 我们要实现上图中的效果,需要如下的操作: 1.从工具栏上的”Smobiler Components”拖动一个PhoneButton控件到窗体界面上 2.修改PhoneButton ...

  6. 第24章 退出 - Identity Server 4 中文文档(v1.0.0)

    注销IdentityServer就像删除身份验证cookie一样简单,但是为了完成联合注销,我们必须考虑将用户从客户端应用程序(甚至可能是上游身份提供者)中签名. 24.1 删除认证 要删除身份验证c ...

  7. C#工具:CSV文件转换帮助类

    CSV是逗号分隔值格式的文件,其文件以纯文本形式存储表格数据(数字和文本).CSV文件由任意数目的记录组成,记录间以某种换行符分隔:每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号 ...

  8. 研发团队如何写好API接口文档

    导读 背景 痛点在哪? 为什么要写接口文档? API规范 接口工具 总结 背景        随着业务的发展,支撑组的项目也是越来越多.同时,从整个支撑组项目架构体系(含运维和运营体系),我们对系统业 ...

  9. [PHP] try catch在日常中的使用

    1.try catch可以捕获上一层throw的异常 2.finally是不管try或者catch任何一块有return, 最终都会执行的块 3.try也是可以捕获到call_user_func_ar ...

  10. TS学习随笔(五)->函数

    这篇文章我们来看一下TS里面的函数 函数声明 在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expre ...