当创建对象、字符串或数组时,存储它所需的内存将从称为堆的中央池中分配。当项目不再使用时,它曾经占用的内存可以被回收并用于别的东西。在过去,通常由程序员通过适当的函数调用明确地分配和释放这些堆内存块。如今,像Unity的Mono引擎这样的运行时系统会自动为您管理内存。自动内存管理需要比显式分配/释放更少的编码工作,并大大降低内存泄漏(内存被分配但从未随后释放的情况)的可能性。

值类型和引用类型

当调用一个函数时,它的参数值将被复制到一个保留特定调用的内存区域。只占用几个字节的数据类型可以非常快速方便地复制。然而,对象、字符串和数组要大得多,如果这些类型的数据被定期复制,那将是非常低效的。幸运的是,这是不必要的;大项目的实际存储空间是从堆中分配的,一个小的“指针”值用来记住它的位置。从那时起,只有指针在参数传递过程中需要被复制。只要运行时系统能够定位指针标识的项,就可以经常使用数据的一个副本。

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

内存分配和垃圾收集

内存管理器跟踪它知道未被使用的堆中的区域。当请求一个新的内存块时(例如当一个对象被实例化时),管理器选择一个未使用的区域,从中分配该块,然后从已知的未使用的空间中移除分配的内存。后续请求以相同的方式处理,直到没有足够大的空闲区域分配所需的块大小。在这一点上,从堆中分配的所有内存仍然在使用中是非常不可能的。只要还存在可以找到它的引用变量,就只能访问堆上的引用项。如果对内存块的所有引用都消失了(即,引用变量已被重新分配,或者它们是现在超出范围的局部变量),则它占用的内存可以安全地重新分配。

为了确定哪些堆块不再被使用,内存管理器会搜索所有当前活动的引用变量,并将它们所指的块标记为live。在搜索结束时,内存管理器认为这些live块之间的任何空间都是空的,并且可用于后续分配。由于显而易见的原因,定位和释放未使用的内存的过程被称为垃圾回收(或简称GC)。

优化

垃圾收集对程序员来说是自动的、不可见的,但是收集过程实际上需要大量的CPU时间。如果正确使用,自动内存管理通常会等于或击败手动分配的整体性能。但是,对于程序员来说,重要的是要避免那些比实际需要触发更多次收集器和在执行中引入暂停的错误。有一些臭名昭著的算法,可能是GC噩梦,尽管他们乍一看是无辜的。重复字符串连接是一个典型的例子:

//C# script example
using UnityEngine;
using System.Collections; public class ExampleScript : MonoBehaviour {
void ConcatExample(int[] intArray) {
string line = intArray[0].ToString(); for (i = 1; i < intArray.Length; i++) {
line += ", " + intArray[i].ToString();
} return line;
}
} //JS script example
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中通常意味着帧更新。就像是:

//C# script example
using UnityEngine;
using System.Collections; public class ExampleScript : MonoBehaviour {
public GUIText scoreBoard;
public int score; void Update() {
string scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}
} //JS script example
var scoreBoard: GUIText;
var score: int; function Update() {
var scoreText: String = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}

...每次调用Update将分配新字符串,并不断生成的新垃圾。大多数情况下,只有当分数变化时才更新文本:

//C# script example
using UnityEngine;
using System.Collections; public class ExampleScript : MonoBehaviour {
public GUIText scoreBoard;
public string scoreText;
public int score;
public int oldScore; void Update() {
if (score != oldScore) {
scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
oldScore = score;
}
}
} //JS script example
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;
}
}

当函数返回数组值时,会发生另一个潜在的问题:

//C# script example
using UnityEngine;
using System.Collections; public class ExampleScript : MonoBehaviour {
float[] RandomList(int numElements) {
var result = new float[numElements]; for (int i = 0; i < numElements; i++) {
result[i] = Random.value;
} return result;
}
} //JS script example
function RandomList(numElements: int) {
var result = new float[numElements]; for (i = 0; i < numElements; i++) {
result[i] = Random.value;
} return result;
}

当创建一个充满值的新数组时,这种函数非常优雅和方便。但是,如果反复调用,那么每次都会分配新的内存。由于数组可能非常大,可用空间可能会迅速消耗,从而导致垃圾收集频繁。避免这个问题的一个方法是利用数组是引用类型的事实。作为参数传递给函数的数组可以在该函数内修改,结果将在函数返回后保留。

像上面这样的功能通常可以被替换成:

//C# script example
using UnityEngine;
using System.Collections; public class ExampleScript : MonoBehaviour {
void RandomList(float[] arrayToFill) {
for (int i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
} //JS script example
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();
}

但是,您应该谨慎使用此技术,并检查profiler统计信息,以确保它真正减少了游戏的收集时间。

大堆垃圾收集缓慢且不可频繁收集

这个策略对于分配(和因此收集)相对不频繁并可以在游戏暂停期间处理的游戏最适用。对于堆来说,尽可能大,而不是因为系统内存太少而导致操作系统杀死你的应用程序。但是,如果可能,Mono运行时会自动避免扩展堆。您可以通过在启动期间预先分配一些占位符空间来手动扩展堆(即,您实例化一个纯粹用于对内存管理器产生影响的“无用”对象):

//C# script example
using UnityEngine;
using System.Collections; public class ExampleScript : MonoBehaviour {
void 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 (int i = 0; i < 1024; i++)
tmp[i] = new byte[1024]; // release reference
tmp = null;
}
} //JS script example
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();

同样,在使用此策略时应该小心,并注意Profiler统计数据,而不是仅仅假定它具有所期望的效果。

可重复使用的对象池

很多情况下,只要减少创建和销毁对象的数量,就可以避免生成垃圾。游戏中存在着某些类型的物体,如抛射体,尽管一次只会有少量的物体在游戏中,但它们可能会被反复地遇到。在这种情况下,常常可以重用对象,而不是破坏旧对象,并用新的对象替换它们。

更多信息

内存管理是一个微妙而复杂的课题,它已经投入了大量的学术研究。如果您有兴趣了解更多信息,那么memorymanagement.org是一个很好的资源,列出了许多出版物和在线文章。有关对象池的更多信息可以在维基百科页面Sourcemaking.com上找到。

原文链接:Understanding Automatic Memory Management


本文作者: Sheh伟伟

本文链接: http://davidsheh.github.io/2017/07/13/「翻译」理解Unity的自动内存管理/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!

[翻译]理解Unity的自动内存管理的更多相关文章

  1. [深入理解Java虚拟机]<自动内存管理>

    Overview 走近Java:介绍Java发展史 第二部分:自动内存管理机制 程序员把内存控制的权利交给了Java虚拟机,从而可以在编码时享受自动内存管理.但另一方面一旦出现内存泄漏和溢出等问题,就 ...

  2. 深入理解JAVA虚拟机 自动内存管理机制

    运行时数据区域 其中右侧三个一起的部分是每个线程一份,左侧两个是所有线程共享的. 程序计数器(Program Counter Register) 英文名称叫Program Counter Regist ...

  3. 深入理解JVM(一) -- 自动内存管理机制

    Java运行时数据区域分为:程序计数器,虚拟机栈,本地方法栈,Java堆,方法区,运行时常量池,直接内存,结构如下: 1.程序计数器: 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示 ...

  4. 【深入理解Java虚拟机】自动内存管理机制——垃圾回收机制

      Java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来.C/C++程序员既拥有每一个对象的所有权,同时也担负着每一个对象生 ...

  5. 【深入理解Java虚拟机】自动内存管理机制——内存区域划分

      Java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来.C/C++程序员既拥有每一个对象的所有权,同时也担负着每一个对象生 ...

  6. 深入理解Java虚拟机(自动内存管理机制)

    文章首发于公众号:BaronTalk 书籍真的是常读常新,古人说「书读百遍其义自见」还是很有道理的.周志明老师的这本<深入理解 Java 虚拟机>我细读了不下三遍,每一次阅读都有新的收获, ...

  7. JVM | 第1部分:自动内存管理与性能调优《深入理解 Java 虚拟机》

    目录 前言 1. 自动内存管理 1.1 JVM运行时数据区 1.2 Java 内存结构 1.3 HotSpot 虚拟机创建对象 1.4 HotSpot 虚拟机的对象内存布局 1.5 访问对象 2. 垃 ...

  8. iOS----ARC(自动内存管理)

    1.ARC是什么呢,有什么用? ARC是苹果官方推出的帮助我们苹果开发工程师管理内存的一种自动内存管理机制,它的前身是MRC,也就是手动内存管理: 2.ARC的基本原理是什么? ARC是编译器(时)特 ...

  9. JVM自动内存管理-Java内存区域与内存溢出异常

    摘要: JVM内存的划分,导致内存溢出异常的可能区域. 1. JVM运行时内存区域 JVM在执行Java程序的过程中会把它所管理的内存划分为以下几个区域: 1.1 程序计数器 程序计数器是一块较小的内 ...

随机推荐

  1. 用docker弹性部署自己的服务

    很久不看docker的东西了,之前了解的一些基本命令都忘得差不多了,适逢工作需要,再来复习巩固下.今天想完成的是:借助docker不部署下自己的服务. 环境准备 都说“巧妇难为无米之炊”,所以还是需要 ...

  2. BZOJ 2242 [SDOI2011]计算器 ——EXGCD/快速幂/BSGS

    三合一的题目. exgcd不解释,快速幂不解释. BSGS采用了一种不用写EXGCD的方法,写起来感觉好了很多. 比较坑,没给BSGS的样例(LAJI) #include <map> #i ...

  3. POJ 2888 Magic Bracelet ——Burnside引理

    [题目分析] 同样是Burnside引理.但是有几种颜色是不能放在一起的. 所以DP就好了. 然后T掉 所以矩阵乘法就好了. 然后T掉 所以取模取的少一些,矩阵乘法里的取模尤其要注意,就可以了. A掉 ...

  4. 【2018.10.20】noip模拟赛Day3 二阶和

    今年BJ省选某题的弱化版…… 这看起来就没那么难了,有几种方法维护,这里提两种. 第一种(傻逼的我写的) 维护 一维&二维前缀和. 对于一个长度为$m$的序列$b_1,b_2,...,b_m$ ...

  5. c++ 多线程:线程句柄可以提前关闭,但是线程并没有关闭

    很多程序在创建线程都这样写的:ThreadHandle = CreateThread(NULL,0,.....);CloseHandel(ThreadHandle );1,线程和线程句柄(Handle ...

  6. 标准C程序设计七---14

    Linux应用             编程深入            语言编程 标准C程序设计七---经典C11程序设计    以下内容为阅读:    <标准C程序设计>(第7版) 作者 ...

  7. POJ 2125 最小点权覆盖集(输出方案)

    题意:给一个图(有自回路,重边),要去掉所有边,规则:对某个点,可以有2种操作:去掉进入该点 的所有边,也可以去掉出该点所有边,(第一种代价为w+,第二种代价为w-).求最小代价去除所有边. 己思:点 ...

  8. iOS数据持久化存储

    本文中的代码托管在github上:https://github.com/WindyShade/DataSaveMethods 相对复杂的App仅靠内存的数据肯定无法满足,数据写磁盘作持久化存储是几乎每 ...

  9. pycharm索引index时间很长的原因

    pycharm进行索引index的目的时代码自动补全,当引入新的插件时,就会增加索引时间,插件越多,索引时间越长 没有好的解决办法,除非增加硬件:或者不使用代码自动补全功能

  10. 大话大前端时代(一) —— Vue 与 iOS 的组件化

    序 今年大前端的概念一而再再而三的被提及,那么大前端时代究竟是什么呢?大前端这个词最早是因为在阿里内部有很多前端开发人员既写前端又写 Java 的 Velocity 模板而得来,不过现在大前端的范围已 ...