内存管理

当一个对象,字符串或者数组被创建时,和它所需大小相符的存储空间会从一个中央内存池中被分配出来,这个内存池我们一般称之为堆(heap)。

原理可以类比于C中的malloc()函数,只不过我们无需手动指定所需的内存大小。

当上述物体创建后不再被使用时,它所占用的内存会被回收以便重复利用。在早些时候,这个分配和释放堆中内存的功能都是由程序猿控制的,秘诀就是使用适宜的函数调用。而到了如今,很多运行时系统(例如Unity的Mono引擎)已经可以自动地帮你管理内存了。自动内存管理(即Automatic Memory Management)相对而言减少了代码量,还可以极大地避免内存泄露。

运行时系统(Runtime System), 指由编程语言自身实现的核心代码,它们包含了一些底层的处理策略(例如自动内存管理),虽然我们没有直观地看到,但是我们写的每个程序都包含了运行时系统。
内存泄露(Memory Leakage), 指在内存被分配后从未被销毁,在你编写的项目体量越来越大时内存泄露的弊端会更容易出现,自动内存管理简化了内存管理的工作,所以某种程度上避开了这个问题。

值类型和引用类型

当函数被调用时,它的所有参数值都会被复制到一块为该函数专门开辟的保留内存区(该过程被称为参数传递, parameter passing)。仅占用少量字节的数据类型可以被快速简单地完成复制。然而,对于那些对象,字符串和数组来说,它们占用的内存空间通常非常大,采用常规的方法进行拷贝往往效率低下。幸运的是,这种复制操作并不是必要的,大块数据的存储空间往往直接从堆中分配,它们出现的位置都会被一个占用字节数极少的指针所替代。指针存储的值用于记录大块数据的实际存放位置。采用这种方式,在参数传递时大块数据仅仅需要复制一个指向它们实际存放位置的指针即可。只要运行时系统可以成功定位到指针记录地址所在的数据,一个单独的数据副本就足矣应付所有必要的函数调用了。

在参数传递过程中可以直接存储和复制的类型被称为值类型(value types)。这种类型数据的代表有整数,浮点数,布尔值以及Unity中定义的结构体类型(比如Color和Vector3)。在堆中分配,并通过指针存取的数据类型则被统称为引用类型(Reference types)。这种叫法是源于存放在变量中的值仅仅用于“引用”真实的数据。引用类型的例子包括对象,字符串以及数组。

值类型和引用类型分别有一种类型时常被错误地归类。一个是Vector3,它是货真价实的结构体类型,所以尝试修改函数的Vector3参数的xyz值不会影响输入的数据。另一个就是字符串类型,它是“饱受误解”的引用类型,直接在堆中分配内存。

内存分配和垃圾回收

内存管理器可以实时追踪堆中未被使用的内存区域,你也可以认为管理器保存有一张未使用区域的列表。当有新的内存块被请求时(换句话说就是有对象被初始化),管理器会选择堆中未使用区域的内存进行分配并将该部分内存移出未使用区域的列表。随后的请求都会按照相同的套路处理,直到没有足够大的内存块可以被分配位置。实际上此时堆中被分配出去的内存并不是都处于使用状态。堆中一个引用类型的数据只有在仍有引用类型变量指向它时才可能被访问和修改。所以当一个内存块的所有引用都消失时(例如引用变量被赋予新的内存地址或者作为局部变量超出作用范围),这块被占用的内存空间就可以被安全地回收了。

为了确保堆中某块内存不再被使用,内存管理器会搜索当前所有活跃的引用变量并且将他们引用的数据块标记为“存活”状态。在搜索结束后,所有位于“存活”区块之间的内存区域都会被当做未使用区域处理并且可以在后续的内存分配过程中使用。事实上,这种定位和释放未使用内存区域的过程就是大名鼎鼎的垃圾回收(Garbage Collection, 简写为GC)。

性能优化

垃圾回收对于程序猿来说是自动化且不可见的,但这并不表明我们不需要关注它。事实上,垃圾回收的过程需要在后台占用大量的CPU时间。如果使用得当,自动内部分配通常在总体性能上可以媲美甚至打败手动内存分配。然而对于程序猿来说,如何避免由于GC频繁触发而导致的游戏卡顿是一个很重要的课题。

有一些声名狼藉的代码写法被称为”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;}
}

这段代码的关键细节就在于字符串组不能直接采用累加的方式直接拼在一起。在每次循环中实际进行的操作其实是:之前的引用变量指向的字符串“当场去世”,一个崭新的字符串会被创建并包含旧字符串以及新的字符串。由于字符串会随着for循环变得越来越长,被消耗的堆内存空间也会越来越多。所以每次该函数被调用时轻轻松松就会消耗掉数以百计字节的空闲内存空间。如果你需要把很多字符串连接在一起,更好的选择是使用Mono库中的System.Text.StringBuilder类。

然而,即使是重复的字符串拼接,在非频繁调用的情况下也不会带来太大的麻烦。一个错误的示范就是在Update函数中使用:

//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;}
}

由于Update()每一帧都会被调用,所以上述写法会在每一帧产生固定大小的垃圾。大多数类似情况都可以通过条件语句判断score是否改变,并仅在改变时修改text来节约内存:

//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;}}
}

另一个潜在的问题会在函数返回一个数组类型的值时产生:

//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;}
}

这种类型的函数在创建带有指定数值的数组时非常高效优雅。然而如果它被频繁地调用,每次都会分配一个崭新的内存区域。由于数组可以变得很大,空闲的堆内存空间就会因此被迅速消耗,从而导致频繁的GC。一种避免该问题的方案是利用数组类型也属于引用类型的事实。由于引用类型可以被作为参数传递,在函数中被修改后即使离开作用范围仍不会立刻回收,所以上述代码经常可以被替换为:

//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;}}
}

上述方法只是简单地使用全新的内容替换数组中已有的内容。虽然在使用之前需要进行额外的初始化操作(看起来一点也不清真),但是这个函数被调用时不再会产生新的垃圾了。

请求垃圾回收

像之前提到过的,最好尽可能地避免垃圾回收。但是鉴于GC并不能被完全地消除,一般都有两种主要的策略可以用于最小化GC对游戏过程产生的冲击。

小型堆内存,快速频繁GC

该策略常常适用于拥有较长游戏周期的游戏。在该类游戏中平稳的帧率往往是主要的考量。在这类游戏中小块的内存会被频繁地分配,但仅仅只会使用很短的时间。在iOS上使用该策略时,典型的堆大小大约为200KB。在iPone 3G上GC大概会花费5ms。如果堆大小增加到1MB,GC花费的时间就会增长到7ms。这时以固定的间隔请求GC有时会变得比较有用。尽管这么做通常会让垃圾回收比常规情况更加频繁,但是每次GC都可以处理的很快,对游戏的影响可以降到很低:

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

大型堆内存,缓慢低频GC

该策略适用于内存分配相对频率较低并且可以在暂停时(例如加载页面)进行内存处理的游戏。该情况下堆内存尽可以大会比较好,但要保证堆内存不会过大而导致应用被操作系统干掉。不幸的是,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 blocksfor (int i = 0; i < 1024; i++)tmp[i] = new byte[1024];// release referencetmp = null;}
}

一个足够大的堆内存在两次游戏暂停的间隙不太可能被填满。而在游戏暂停的过程中你完全可以通过显式调用GC进行内存回收。

System.GC.Collect();

再次提醒,为了达到理想的效果,你应该分析性能统计数据来调整堆内存的大小,而不是想当然的认为可以满足预期。

可重用的对象池(Object Pools)

很多情况下你可以简单地通过减少创建和销毁的对象数量来避免产生垃圾。在游戏里有很多种类的对象会被频繁地使用但仅有一小部分会同时显示。在这种情况下,通常选择复用对象要比直接销毁+创建高效的多。

结语

该文章参考了Unity Manual中的Understanding Automatic Memory Management,并根据个人理解进行了整理,有疑问的欢迎留言探讨。

理解自动内存管理(Automatic Memory Management)相关推荐

  1. HALCON:内存管理(Memory Management)

    HALCON:内存管理(Memory Management)

  2. Oracle PGA内存管理 PGA Memory Management

    一.简介         Program Global Area(PGA)是一个和服务器进程关联的包含数据和控制信息的私有内存区域. 对于一个复杂的查询来说,需要在PAG中的SQL工作区(work a ...

  3. 异构内存管理 Heterogeneous Memory Management (HMM)

    https://www.kernel.org/doc/html/latest/vm/hmm.html 目录 异构内存管理 (HMM) 使用特定于设备的内存分配器的问题 I/O 总线.设备内存特性 共享 ...

  4. Linux内存管理Linux Memory Management Notes

    Linux 内存基础 地址类型 linux内核中有许多种不同的地址类型 用户虚拟地址 用户空间看到的常规地址,通过页表可以将虚拟地址和物理地址映射起来 物理地址 用在cpu和内存之间的地址叫做物理地址 ...

  5. oracle启用amm,【内存管理】Oracle AMM自动内存管理详解

    一. Oracle 的三种内存管理方式 oracle 内存管理有三种方式,每一个 instance 只能够选择一种.这三种管理方式分别是 AMM 自动内存管理( Automatic Memory Ma ...

  6. oracle自动内存管理要不要开,Oracle 11g的自动内存管理

    Oracle 的 9i/10g 中已经对内存管理逐步做了很大的简化,11g 则更进一步,引入了一个新的概念自动化内存管理(Automatic Memory Management,AMM) . 如果 D ...

  7. [翻译]理解Unity的自动内存管理

    当创建对象.字符串或数组时,存储它所需的内存将从称为堆的中央池中分配.当项目不再使用时,它曾经占用的内存可以被回收并用于别的东西.在过去,通常由程序员通过适当的函数调用明确地分配和释放这些堆内存块.如 ...

  8. 【Java书笔记】:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》第2部分-自动内存管理,第3部分-虚拟机执行子系统,第5部分-高效并发

    作者:周志明 整理者GitHub:https://github.com/starjuly/UnderstandingTheJVM 第2部分-自动内存管理 第2章 Java内存区域与内存溢出异常 2.2 ...

  9. 深入理解java虚拟机-1.自动内存管理

    文章目录 1.自动内存管理 1.1 Java内存区域与内存溢出异常 1.1.1 运行时数据区域 程序计数器 程序计数器为什么是私有的? java虚拟机栈 本地方法栈 虚拟机栈和本地方法栈为什么是私有的 ...

最新文章

  1. ecshop根目录调用_ecshop列表页 调用二级分类教程
  2. Java类和对象初始化
  3. Order asynchronous mode
  4. ubuntu离线安装依赖
  5. python pip安装及出现的问题
  6. 20191019:(leetcode习题)第K个语法符号
  7. 哎,老了之display-box
  8. LINUX/MAC的rpath,搜索依赖库时从哪里开始
  9. 批处理bat命令快速截图
  10. 一天入门51单片机教程
  11. 心电信号质量评估——ecg_qc工具包介绍(二)
  12. [ XJTUSE ]JAVA语言基础知识——7.11 JTree、TreeModel实现树
  13. ORB-SLAM2的源码阅读(九):Initializer类
  14. 知识普及:KB=Kb?
  15. oracle错误号提示ORA-
  16. 大麦票夹:从工具到服务的技术演进之路
  17. Batch Normalization (BN层)-----批归一化
  18. ROS入门:运行小海龟
  19. 三维坐标点绕任意轴旋转的新坐标计算
  20. 物理大地测量学笔记(一)

热门文章

  1. hrrn算法java_整理一些计算机基础知识!(不定期更新)
  2. java产生随机数的方法
  3. 数据库的本质、概念及其应用实践(一)
  4. fopen多次打开同一个文件
  5. LongAdder和AtomicLong
  6. js怎么给下拉框默认选中
  7. oppo手机彩蛋android 9.0,OPPO手机安卓8.1系统彩蛋怎么打开
  8. 【编程实践】Golang 实现中文分词
  9. Spring继续学习: IOC、Bean、拓展点.....
  10. 安装正版微软官方win10简单方法