.NET 内存管理

4

能否提供一份.NET内存管理的高层次概述?

更具体地说,我想了解以下内容:

  • 是否存在一个全局的.NET堆?
  • 堆是基于应用程序的吗?
  • 运行我的应用程序时,是创建新堆/分配内存还是从总的.NET堆中分配内存?
  • .NET堆的原始内存用尽时会发生什么?它是否向操作系统请求更多内存?
  • 了解这些基本知识可以为我进一步阅读打下良好的基础。

1
垃圾回收:Microsoft .NET Framework中的自动内存管理 - http://msdn.microsoft.com/zh-cn/magazine/bb985010.aspx - CD..
3个回答

2

每个进程都有自己的堆 - 如果在GC清理了所有可以清理的东西后需要更多的内存,进程会向操作系统请求更多信息。

我知道的关于这类信息的最好资源是Jeffrey Richter的CLR via C#书。


那么,.net本身没有默认或总体的内存分配?只有当我的应用程序(进程)运行时才会有,如果在GC完成其工作后需要更多内存,则会从操作系统请求更多内存?我是否正确地思考了这一点,即如果此时操作系统无法为我的进程提供更多内存,则会发送(OOM)内存不足异常? - Developr
@user799372:我不认为有任何默认限制(与Java不同)。我相信CLR托管环境可以在需要时强制执行限制。如果内存不足以满足您的应用程序,您将遇到OOM。 - Jon Skeet
@user799372:不确定你的意思 - 你是指GC在请求更多内存之前清除现有内存的难度吗?我相信这可能会根据不同的CLR版本而改变,但我认为它是动态的 - 它将根据先前的行为进行自我调整。 - Jon Skeet
我只是想知道当一个进程被创建时,它的伴随堆是否有一个总大小限制(这反过来会在阈值被触发时触发GC)。 - Developr
@user799372:这远远超出了在评论中可以合理完成的范围。我建议您阅读Eric Lippert关于堆栈的博客系列文章。请参见http://blogs.msdn.com/b/ericlippert/archive/2009/04/27/the-stack-is-an-implementation-detail.aspx - Jon Skeet
显示剩余6条评论

1
有一个总的.NET堆吗?
有很多。您通常关心的是0、1和2代垃圾收集堆、大对象堆和加载器堆。代帮助垃圾收集器更有效。LOH用于太大而无法移动的对象。加载器堆存储静态变量值。
堆是基于应用程序的吗?
不,它们是基于AppDomain的。AppDomains提供了一种廉价的进程替代方案。
当我运行我的应用程序时,会创建/分配新的堆或者内存来自总的.NET堆?
默认CLR在您的代码开始运行之前创建主AppDomain及其相关堆。
当.NET堆的原始内存用完时会发生什么?它会向操作系统请求更多内存吗?
是的。

应用程序域是自动创建的还是我们必须在应用程序中定义/构建的内容? - Developr
在第三个答案中指出:默认的CLR会自动创建主AppDomain。其他情况则需要编写代码来实现。 - Hans Passant
啊,你是这么说的 - 抱歉我看错了 :) 所以...当一个进程被创建时,主应用程序域也会被创建。如果我们想要更多的应用程序域,就需要编写代码。我们如何知道要将后续的应用程序域添加到哪个进程中?这只是我们应该知道的事情吗?例如,我们可能已经创建了app1:process1并且已经知道它的存在,因此可以通过编程方式向该特定进程添加第二个、第三个等应用程序域吗? - Developr
我不禁注意到这些后续问题可能有点错位。为什么不问一下你接受答案的那个人呢? - Hans Passant
我是 Stack Overflow 的新手,所以仍在逐步掌握如何适当地进行讨论 :) 我想我已经在你回复之前接受了上一个答案。我很想把答案都给你两个,因为你们都非常帮助我更好地了解了这个问题,但我不认为系统允许我这样做。 - Developr

0

本文涉及 .net 内存管理的以下问题。我希望这能让读者对 .net 内存管理有一个良好的了解:

  1. 值类型如何分配内存?
  2. 引用类型如何分配内存?
  3. 静态字段和全局变量如何分配内存?
  4. 多线程环境下如何分配内存?
  5. 继承中如何分配内存?
  6. 垃圾回收器是如何工作的?
  7. 内存管理的最佳实践是什么?

让我们开始吧。 .net 类型的内存分配发生在堆栈或堆中。我们需要了解类型何时分配在堆栈或堆中。

堆栈

方法中的所有局部变量都在堆栈中分配内存。 当方法 A 调用另一个方法 B 时,调用方法 A 的返回地址存储在堆栈中,并将控制传递给方法 B。 一旦方法 B 执行完成,它(以及其本地数据)就会从堆栈中删除,并从方法 A 停止的位置恢复执行。

在检查堆中存储了什么之前,我们需要了解根引用。 让我们通过一个例子来理解:

假设有一个员工类,其中包含员工技能列表。如果我们需要访问员工的技能,我们需要创建一个员工类对象,然后访问该列表。

因此,类型为员工的对象引用是存储在堆栈上的根引用,实际对象存储在堆中。

所有全局变量、静态全局变量和类型(使用 new 初始化时)都存储在堆中。

多线程环境下的内存分配 每个线程都有自己的堆栈,但堆是所有线程共享的。因此,在堆的情况下确保线程安全变得很重要,因为不同的线程可以访问/修改相同的资源。

本文档不涉及线程安全。

继承中的内存分配 当我们创建子类的对象时,在堆中创建单个对象。 这个对象将保存与类相关的所有状态数据,包括父类。

垃圾回收(GC) .net 使用垃圾回收来自动清理分配的对象。

垃圾收集器检查堆上未被任何东西引用或者说没有根引用的分配对象。

静态变量分配在堆上,永远不会被垃圾回收,因为这些变量从来没有根引用。

GC在单独的线程上运行,用于收集未使用的对象并释放内存。它会定期自动运行或在应用程序开始耗尽内存时运行。

开发人员可以通过执行GC.Collect()来启动GC,但不建议这样做,因为它可能会影响应用程序性能。

GC代

根据对象的生命周期,它可以分为3个类别 -

第0代 - 当对象刚创建时,它被放置在第0代。这意味着此对象是新的,并且尚未由GC检查。

第1代 - 经过一次GC检查但由于具有根引用而幸存下来的对象被放置在第1代中。

第2代 - 经过两次或更多次检查并由于具有根引用而未被GC杀死的对象位于第2代中。

GC不会收集文件、网络、数据库、UI元素等非托管资源的对象。我们需要显式地使用Dispose(IDisposable)或在using语句中使用相关类对象(确保IDiposable继承了我们要为其使用dispose的类型)。

内存管理最佳实践

  1. 使用IDisposable或using语句释放非托管资源。

  2. 如果在创建类对象时不需要这些成员,请尝试推迟类成员的初始化。

  3. 集合(如List)在列表创建时未定义大小时,将初始大小设置为4个元素。如果我们在4个元素之后添加任何元素,则集合大小会在内存中加倍。 因此,为了避免在列表较小时占用额外的内存,建议指定集合的初始大小。

  4. 尽量保持数据模型简单和结构良好。因为如果太复杂,GC需要更多时间来分析整个图以检查哪些对象可以被收集。

  5. 尽可能使用yield语句。 C#中的yield关键字用于生成IEnumerator实现的迭代器模式。使用yield语句的好处是不需要将整个集合保存在内存中,而是逐个处理。

  6. 避免在LINQ查询中过度分组或聚合函数。

谢谢!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接