我在这篇关于 .net 内存管理的文章中讨论了以下问题。我希望这能让读者了解 .net 内存管理。
- 如何为值类型分配内存?
- 如何为引用类型分配内存?
- 内存是如何分配给静态字段和全局变量的?
- 多线程环境下如何分配内存?
- 如何在继承中分配内存?
- GC 是如何工作的?
- 什么是内存管理最佳实践?
让我们开始吧
.net 类型的内存分配发生在堆栈或堆中。我们需要了解什么时候在堆栈或堆中分配类型。
堆叠
方法中的所有局部变量都在堆栈中分配内存。
当一个方法 A 调用另一个方法 B 时,调用方法 A 的返回地址存储在 STACK 中,并将控制权传递给方法 B。
方法 B 执行完成后,它(连同其本地数据)从 STACK 中删除,并从方法 A 中停止的位置继续执行。
堆
在检查堆中存储的内容之前,我们需要了解根引用。
让我们用一个例子来理解——
假设有一个包含员工技能列表的员工类。如果我们需要访问员工的技能,我们需要创建一个员工类的对象,然后访问该列表。
所以这里员工类型的对象引用是存储在堆栈上的根引用,实际对象存储在堆上
所有全局变量、静态全局变量和类型(使用 new 初始化时)都存储在 HEAP 中。
多线程环境中的内存分配
每个线程都有自己的 STACK,但 HEAP 在所有线程之间共享。因此,在 HEAP 的情况下确保线程安全变得很重要,因为不同的线程可以访问/修改相同的资源。
线程安全超出了本文档的范围。
继承中的内存分配
当我们创建子类的对象时,会在堆中创建单个对象。
该对象将保存类的所有状态相关数据,包括父类。
垃圾收集 (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 查询中过度分组或聚合函数。
谢谢!