返回介绍

理解自动内存管理

发布于 2021-06-19 18:03:26 字数 4750 浏览 879 评论 0 收藏 0

在创建对象、字符串或数组时,其存储所需的内存将从一个被称为的中央池分配。若项目不再使用,它之前占用的内存可以释放回收,用来存储其他数据。过去通常由程序设计员使用相应的函数调用,明确分配并释放这些堆内存块。现在,像 Unity Mono 引擎之类的运行时间系统可以自动管理内存。自动内存管理比明确分配/释放需要的编码工作更少,并大大降低了潜在的内存泄漏(内存分配,但从未被释放的情况)。

值和引用类型

在调用函数时,其参数值将复制到专为此次调用保留的内存区域。数据类型只会占用几个字节,可以快速简单地复制。但在通常情况下,对象、字符串和数组要大得多,如果这些类型的数据定期复制的话将会变得相当低效。幸运的是,我们没必要这样做;大型项目的实际存储空间从堆中分配并且只使用一个小小的“指针”值来记住它的位置。自此,在参数传递期间只需复制指针。只要运行时间系统可以找到指针识别的项目,就可以在必要时经常使用单个数据副本。

直接存储并在参数传递过程中复制的类型被称为值类型。它包括整型、浮点型、布尔型和 Unity 的结构类型(例如,Color 和 Vector3)。被分配在堆然后通过指针访问的类型被称为引用类型,因为存储在变量中的值只是“指向”真实的数据。引用类型包括对象、字符串和数组等。

分配和垃圾收集

内存管理会跟踪堆中已确定未使用的区域。在请求新的内存块时(例如当一个对象被实例化),管理器将选择未使用的区域分配给内存块,然后将分配的内存从已知未使用空间中删除。后续请求都以同样的方式处理,直到有没有足够大的未使用空间来分配所需的块大小。此时,堆中分配的所有内存几乎不可能都在使用中。只有在参考变量仍可找到引用时,才可以访问堆上的引用项目。如果内存块的所有引用都已经消失(例如,引用变量已经重新分配,或者它们成为超出范围的局部变量),那么,它所占用的内存可以安全地重新分配。

若要决定哪些堆块不再被使用,内存管理器将在所有当前活动的引用变量中搜索,并将其引用的块标记为“活动”块。完成搜索之后,活动块之间的所有空间都将被内存管理器视为未使用,可以用于后续分配。显然,定位和释放未使用内存的过程被称为垃圾收集(简称为 GC)。

优化

垃圾收集为自动执行,且程序设计员不可见,但实际上收集过程需要在后台占用大量 CPU 时间。若使用恰当,自动内存管理的整体性能一般会等于或高于手动分配。但是,程序设计员应避免失误引发不必要的回收器触发和执行停顿。

现在也存在一些非主流的算法,虽然第一眼看上去并无恶意,但却能成为的 GC 的噩梦。重复字符串串联便是一个典型的例子:-

 function ConcatExample(intArray: int[]) {
 	var line = intArray[0].ToString();
 
 	for (i = 1; i < intArray.Length; i++) {
 		line += ", " + intArray[i].ToString();
 	}
 
 	return line;
 }

此处的关键细节是新的块没有一个接一个恰当地添加到字符串。实际情况是,每次循环的时候,之前的 line 变量的内容成为死链 — 每次分配的都是一个由原块和结尾处新块组成的全新的字符串。随着 i 值的增加,字符串也将不断变长,消耗的堆空间量也随之增加,因此,每次调用此函数,都极有可能使用数百个字节的空闲堆空间。如需串联大量字符串,那么最好使用 Mono 库的 System.Text.StringBuilder 类。

但是,如果不频繁调用,字符串联不会引发过多的问题,并且 Unity 通常采用帧更新。如下所示:-

 var scoreBoard: GUIText;
 var score: int;
 
 function Update() {
 	var scoreText: String = "Score: " + score.ToString();
 	scoreBoard.text = scoreText;
 }

… 每次更新时都将分配新的字符串,并且持续产生新的垃圾。大多数字符串可以通过更新文本保存,除非 score 发生变化:-

 var scoreBoard: GUIText;
 var scoreText: String;
 var score: int;
 var oldScore: int;
 
 function Update() {
 	if (score != oldScore) {
 		scoreText = "Score: " + score.ToString();
 		scoreBoard.text = scoreText;
 		oldScore = score;
 	}
 }

另一个潜在问题发生在函数返回数组值时:-

 function RandomList(numElements: int) {
 	var result = new float[numElements];
 
 	for (i = 0; i < numElements; i++) {
 		result[i] = Random.value;
 	}
 
 	return result;
 }

在创建有填充值的新数组时,此类函数非常美观实用。但是,如果反复调用,每次都将分配新的内存。由于数组可能非常大,空闲的堆空间可能快速消耗完,导致频繁的垃圾收集。避免这个问题的一种方式是利用数组是一种引用类型。作为参数传递到函数的数组可以在函数内修改,在函数返回之后结果将保留。上述函数通常可作如下替换:-

 function RandomList(arrayToFill: float[]) {
 	for (i = 0; i < arrayToFill.Length; i++) {
 		arrayToFill[i] = Random.value;
 	}
 }

这项操作会简单地用新值取代现有的数组内容。虽然这需要在调用的代码中完成数组初始分配(这似乎不够美观),但是此函数在调用时不会产生任何新的垃圾。

请求收集

如上所述,最好尽量避免分配。但考虑到它们无法被完全消除,可以使用两种主要策略将其对游戏设置的入侵降至最低:-

快速、频繁垃圾收集的小堆

此策略最好用于拥有长期游戏设置的游戏,此类游戏考虑的主要问题是平稳的帧速率。这些游戏的主要特点是频繁分配小堆,但是这些堆只会短暂使用。在 iOS 上使用这一策略时,一般堆大小为 200KB,在 iPhone 3G 上,垃圾收集将花费大约 5ms 时间。如果堆增加至 1MB,垃圾收集将花费 7ms。因此,在某些需要以规定帧间隔进行垃圾收集的情况下,这将是非常有用的功能。通常,垃圾收集的频率将高于必要的次数;但它可以快速处理,几乎不会对游戏造成影响。

 if (Time.frameCount % 30 == 0)
 {
    System.GC.Collect();
 }
 

但请应该谨慎使用该技巧,并检查分析器统计信息,以确保它可以真正减少游戏的垃圾收集时间。

缓慢但很少进行垃圾收集的大堆

此策略适用于很少进行分配(收集也相应减少)的游戏,可以在游戏暂停时处理。如果堆尽可能大,但不会大到系统内存低而导致 OS 无法运行游戏时,它非常有用。如果可能的话,Mono 运行时间会避免自动扩展堆。您可以在启动时预先分配某些占位符空间(例如,对于仅因影响内存管理器的而被分配的“无用”对象,可对其实例化),手动扩展堆:-

 function Start() {
 	var tmp = new System.Object[1024];
 
 	// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
         for (var i : int = 0; i < 1024; i++)
 		tmp[i] = new byte[1024];
 
 	// release reference
         tmp = null;
 }
 
 

在游戏设置中可以容纳垃圾收集的的停顿之间,不应完全填满足够大的堆。若发生此类停顿,可以明确地请求垃圾收集:-

 System.GC.Collect();
 

同样,也应该谨慎使用该策略,并注意分析器的统计信息,而是不是假设它已经取得预期效果。

可重复使用的对象池

在很多情况下,可以通过减少创建和销毁的对象数目来避免产生垃圾。您可能反复遇到游戏中的某些对象,比如爆弹,尽管只有少数对象会立即爆炸。这种情况通常可以重复使用对象而无需摧毁旧对象,然后使用新对象替换。

请参阅此处,了解更多有关对象池 (Object Pool) 及其实现的详细信息。

更多信息

内存管理是一个微妙复杂的话题,已经投入了大量的学术精力。如果有兴趣了解更多相关知识,那么 memorymanagement.org 是不错的资源网站,它刊登了大量出版物和在线文章。更多有关对象池的信息,请访问 Wikipedia 页面Sourcemaking.com

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文