所有内容最终都被JIT化为本机机器代码,因此最终,我们在.NET中有一个本机堆栈,只要它进行垃圾收集,GC就需要扫描对象指针。
现在,问题是:.NET垃圾收集器如何确定指向GC堆内对象的指针实际上是一个托管指针还是一个随机整数发生以获得一个值对应有效地址?
显然,如果它无法区分这两者,那么就会出现内存泄漏,所以我想知道它是如何工作的。或者 - 我敢说 - .NET有可能泄漏内存吗? :o
答案 0 :(得分:12)
正如其他人所指出的那样,GC确切地知道堆栈和堆上每个块的哪些字段是托管引用,因为GC和抖动知道所有的类型。
然而,你的观点很好。想象一个完全假设的世界,在同一个过程中有两种内存管理。例如,假设你有一个用C ++编写的名为“InterMothra Chro-Nagava-Sploranator”的假设程序,该程序使用传统的COM样式引用计数内存管理,其中一切只是指向进程内存的指针,并通过调用一个对象来释放对象。释放方法正确次数。假设Sploranator假设有一种脚本语言JabbaScript,它维护一个垃圾收集的对象池。
当JabbaScript对象具有对非托管Sploranator对象的引用,并且同一Sploranator对象具有右引用时,会出现问题。这是一个循环引用,不能被JabbaScript垃圾收集器破坏,因为它不知道Sploranator对象的内存布局。因此,存在内存泄漏的可能性。
解决此问题的一种方法是重写Sploranator内存管理器,以便将其对象分配到托管GC池之外。
另一种方法是使用启发式方法; GC可以专门用一个处理器的线程来扫描所有内存,寻找恰好是指向其对象的指针的整数。这听起来很多,但它可以省略未提交的页面,自己的托管堆中的页面,已知仅包含代码的页面等等。 GC可以猜测,如果它认为一个对象可能已经死了,并且它无法在其控制之外的任何内存中找到任何指向该对象的指针,那么该对象几乎肯定已经死了。
这种启发式的缺点当然是错误的。您可能有一个意外匹配指针的整数(虽然这在64位域中不太可能)。这将延长对象的生命周期。但谁在乎?在循环引用可以延长对象的生命周期的情况下,我们已经。我们试图让这种情况更好,而这种启发式方法也是如此。它不完美是无关紧要的;它总比没有好。
另一种可能错误的方式是Sploranator可以编码指针,比如说,在存储值时翻转所有位,并且只在调用之前将其翻转回来。如果Sploranator对此GC启发式策略主动充满敌意,那么它就不起作用。
此处概述的垃圾收集策略与任何产品的实际GC策略之间的相似性几乎完全是巧合。 Eric关于假设不存在的产品的垃圾收集器的实施细节的思考仅用于娱乐目的。
答案 1 :(得分:3)
垃圾收集器不需要推断特定字节模式(无论是4字节还是8字节)是否是指针 - 已经 知道强>
在CLR中,所有内容都是强类型的,因此垃圾收集器知道字节是int
,long
,对象引用,无类型指针等等。
内存中对象的布局是在编译类型中定义的 - 存储在程序集中的元数据给出了实例的每个成员的类型和位置。
堆栈帧的布局类似 - 编译方法时JITter布局堆栈帧,并跟踪存储在哪里的数据类型。 (它由JITter完成,允许根据处理器的功能进行不同的优化)。
当垃圾收集器运行时,它可以访问所有这些元数据,因此它永远不需要猜测特定的位模式是否可能是引用。
Eric Lippert's blog是了解更多信息的好地方 - References are not addresses将是一个值得一去的地方。
答案 2 :(得分:1)
请记住,所有托管内存都由CLR管理。任何实际的托管引用都是由CLR创建的。它知道它创造了什么以及它没有创造什么。
如果您确实认为必须了解实施的详细信息,那么您应该CLR via C#阅读Jeffrey Richter。答案并不简单 - 它的引用比SO上的答案要多。
答案 3 :(得分:1)
当JITing代码时,编译器知道它在哪些位置放置对象的引用。每当你在一个包含引用的方法中使用一个字段时,它就知道在那个地方有一个引用。 JIT代码时也可以保留此信息。
现在引用指向该对象。每个对象都有一个指向其类的指针(.GetType() - 方法)。基本上GC现在可以使用指针,跟随它,读取对象的类型。该类型告诉您是否有其他字段包含对其他对象的引用。这样GC就可以遍历整个堆栈和堆栈。
当然这有点过于简化,但基本原则。最后是一个实现细节。当然,还有其他方法和各种技巧可以有效地实现这一目标。
注释后更新:堆栈上的指针指向堆上的对象。每个对象都有一个标题,它还包含指向其type-info的指针。所以你可以取消引用堆栈上的指针,取消引用指向object-info的指针,找出它是什么类型的对象。
答案 4 :(得分:1)
看看Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework
(某些技术细节可能有点过时,但所描述的结构是有效的。)
文章中的一些简要说明......
初始化进程时,运行时保留连续的 最初的地址空间区域 没有分配存储空间。这个 地址空间区域是管理的 堆。堆也维护着 指针,我称之为 NextObjPtr。这个指针指示 下一个对象的位置 在堆内分配。原来, NextObjPtr设置为基数 保留地址空间的地址 区域。
...
每个应用程序都有一组根。根识别存储 位置,指的是对象 托管堆或对象 设置为null。例如,所有的 全局和静态对象指针 申请被视为一部分 应用程序的根源。此外, 任何局部变量/参数对象 线程堆栈上的指针是 被视为申请的一部分 根。最后,任何CPU注册 包含指向对象的指针 托管堆也被视为一部分 应用程序的根源。 列表 活性根的维持 实时(JIT)编译器和通用 语言运行时,并且已经完成 垃圾收集器可以访问 算法强>
...
当垃圾收集器开始运行时,它会生成 假设所有对象都在 堆是垃圾。换句话说,它 假定没有应用程序 根指的是。中的任何对象 堆。现在,垃圾收集器 开始走根和建设 可以从中获取的所有对象的图形 根。例如,垃圾 collector可以找到一个全局变量 指向堆中的对象。
关于这个问题......
...或碰巧的随机整数 有一个对应于a的值 有效地址?....内存泄漏?
如果无法访问该对象,GC无论如何都会销毁它。
答案 5 :(得分:0)
在.NET中创建新的引用类型对象时,您将自动使用CLR及其GC“注册”它。无法将随机值类型注入此过程。换句话说:
CLR不会维护一些与值类型混合的大型,无组织的指针堆。它只跟踪CLR创建的对象(无论如何都是为了垃圾收集目的。)任何值类型都将在堆栈上短暂存在或者是类实例的成员。 GC没有混淆的可能性。
答案 6 :(得分:0)
根据“CLR via C#”一书,运行时通过检查“方法的内部表”确切地知道在哪里找到引用/指针。 这个内部表在微软实现中的含义是未知的,但它可以合理地识别堆栈上的调用帧,局部变量,甚至寄存器为每个EIP地址保存的值。
单声道实现使用保守扫描,这意味着将堆栈上的每个值都视为潜在指针。这不仅会转换为内存泄漏,而且(因为它无法更新这些值)由此标识的对象被视为固定(由GC压缩程序无法移动)并导致内存碎片。
现在,mono可以选择使用GCMaps的“精确堆栈标记”。您可以在此处阅读更多内容http://www.mono-project.com/Generational_GC#Precise_Stack_Marking
请注意,此实现不准确,因为它是MS,因为它会继续保守地处理当前帧。
答案 7 :(得分:-1)
引用有标题,所以它不仅仅是一个随机整数。