首页 > 代码库 > 了解自动内存管理

了解自动内存管理



了解自动内存管理


当创建对象、 字符串或数组时,从中央池中称为分配存储它所需的内存。该项目时不再使用,它一次占用的内存可以回收,并用于别的东西。在过去,它通常是由程序员来分配和释放这些块堆内存使用适当的函数调用显式。如今,像统一的单引擎的运行时系统会自动为您管理内存。自动内存管理需要少比显式分配/释放的编码工作,极大地减少了潜在的内存泄漏 (情况在哪里内存分配,但永远不会随后释放)。

值和引用类型

当一个函数被调用时,其参数的值复制到为这一具体要求保留的内存区域。可以复制占用只有几个字节的数据类型,非常迅速和容易。然而,这是常见的对象、字符串和数组要大得多,如果这些类型的数据被复制在定期的基础上,它将会非常低效。幸运的是,这不是必要的 ;从堆分配一个大的项目的实际存储空间和一个小的"指针"值,用来记住它的位置。从那时起,只有指针需要复制期间传递的参数。只要运行时系统可以找到由指针标识的项,可以作为必要时经常使用数据的单个副本。

直接存储和复制期间参数传递的类型称为值类型。这些包括整数、 浮点数、 布尔值和统一的结构类型(例如,颜色和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;
}
 
 

这里关键的细节是新片不会添加到地方中的字符串、 一个接一个。到底发生了什么是周围循环的每次行变量上以前的内容变得死寂了 — —一个全新的字符串分配包含原片加末尾的新部分。因为字符串获取与增加值的我更长的时间,所用的堆空间正在消耗也增加,所以它是容易使用了数百个字节的可用堆空间的每次调用此函数。如果您需要将许多字符串连接在一起更好的选择是单声道库System.Text.StringBuilder类。

然而,即使重复的串联不会造成太多的麻烦,除非它叫做频繁,并在通常意味着该框架的统一更新。就像:-

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

你何时分配新的字符串调用 Update时每次和生成新的垃圾不断淌出。大多数是可以通过更新文本,仅当比分更改时保存:-

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上使用这种策略时的典型堆大小是大约 200 KB,垃圾回收会约5ms iPhone 3g。如果堆增加到 1 MB时,该集合将约 7ms。因此,它可以有利于有时要求普通帧间隔的垃圾回收。这通常会使集合比严格必需更经常发生,但他们将处理速度快、 影响最小的游戏:-

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

然而,你应该谨慎使用这种技术,检查事件探查器统计信息,以确保它真的减少收集时间为你的游戏。

大堆与缓慢但很少发生垃圾回收

这一战略适合的游戏拨款 (和集合) 是相对较少,可以在游戏暂停期间处理。它是用于堆是一样大的而不是如此之大,让您的应用程序由于系统内存不足 OS被杀害。然而,单声道的运行时避免扩大堆自动如果可能的话。您可以通过在启动过程中预一些占位符空间手动扩展堆 (即您实例化一个纯粹的影响,内存管理器分配的"无用"对象):-

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();
 
 

再次,你应该照顾使用此策略时,注意到探查器统计信息而不只在假设它有预期的效果。

可重用的对象池

有很多情况下,在那里您可以避免生成垃圾仅通过减少创建和销毁的对象的数目。有某些类型的对象在游戏中,如子弹头,可能会遇到几遍,即使只有一小部分曾经将播放一次。在这种情况下,很有可能要重用的对象,而不是摧毁旧的并替换为新的。

进一步的信息

内存管理是微妙和复杂须大量的学术努力一直致力。如果你有兴趣学习更多有关它memorymanagement.org是一种优秀的资源,列出了许多出版物和在线文章。在Sourcemaking.com维基百科的页面,可以找到有关对象池的进一步信息.

 

了解自动内存管理