能否提供一份.NET内存管理的高层次概述?
更具体地说,我想了解以下内容:
- 是否存在一个全局的.NET堆?
- 堆是基于应用程序的吗?
- 运行我的应用程序时,是创建新堆/分配内存还是从总的.NET堆中分配内存?
- .NET堆的原始内存用尽时会发生什么?它是否向操作系统请求更多内存?
- 了解这些基本知识可以为我进一步阅读打下良好的基础。
能否提供一份.NET内存管理的高层次概述?
更具体地说,我想了解以下内容:
每个进程都有自己的堆 - 如果在GC清理了所有可以清理的东西后需要更多的内存,进程会向操作系统请求更多信息。
我知道的关于这类信息的最好资源是Jeffrey Richter的CLR via C#书。
本文涉及 .net 内存管理的以下问题。我希望这能让读者对 .net 内存管理有一个良好的了解:
让我们开始吧。 .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的类型)。
内存管理最佳实践
使用IDisposable或using语句释放非托管资源。
如果在创建类对象时不需要这些成员,请尝试推迟类成员的初始化。
集合(如List)在列表创建时未定义大小时,将初始大小设置为4个元素。如果我们在4个元素之后添加任何元素,则集合大小会在内存中加倍。 因此,为了避免在列表较小时占用额外的内存,建议指定集合的初始大小。
尽量保持数据模型简单和结构良好。因为如果太复杂,GC需要更多时间来分析整个图以检查哪些对象可以被收集。
尽可能使用yield语句。 C#中的yield关键字用于生成IEnumerator实现的迭代器模式。使用yield语句的好处是不需要将整个集合保存在内存中,而是逐个处理。
避免在LINQ查询中过度分组或聚合函数。
谢谢!