找回密码
 注册帐号

扫一扫,访问微社区

理解自动内存管理

2015-1-8 18:26| 发布者: 杨炎| 查看: 1651| 评论: 0|原作者: 蛮牛|来自: unity3d脚本manual

摘要: 理解自动内存管理在创建对象、字符串或数组时,其存储所需的内存将从一个被称为堆的中央池分配。若项目不再使用,它之前占用的内存可以释放回收,用来存储其他数据。过去通常由程序设计员使用相应的函数调用,明确分 ...

理解自动内存管理

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

值和引用类型

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

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

分配和垃圾收集

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

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

上一篇:教程下一篇:了解视锥体

相关阅读

文章点评
相关文章