文章目录

  • Unity内存
  • 内存域
    • - 托管域
    • - 本地域
    • - 外部库
    • - 跨桥操作
  • 堆和栈
    • - 栈
    • - 堆
    • - 堆栈的使用
  • 垃圾回收
    • - Mono内存分配过程
    • - 内存泄漏
    • - 内存碎片
    • - 运行时垃圾回收
    • - 多线程垃圾回收

Unity内存

在游戏开发的世界中,程序的优劣与内存的使用有着密不可分的关系。从很多方面来说Unity对于内存的使用并不是最优解,正因为如此,Unity的内存管理与优化就显得尤为重要。

内存域

Unity引擎中的内存空间本质上可以分为3个不同的内存域,分别为托管域本地域以及外部库,每个域存储不同的数据类型,关注不同的任务集。

- 托管域

在使用Mono编译的情况下,托管域就是Mono VM使用的运行时内存。“托管”的本意是Mono可以自动地改变堆的大小来适应所需要的内存,并且适时地调用垃圾回收(Garbage Collection)操作来释放已经不需要的内存。我们编写的任何MonoBehaviour脚本以及自定义C#类都会在此域中实例化对象,因此我们编写的任何C#代码都会很明确的与此域交互。目前绝大部分Unity游戏逻辑代码所使用的语言为C#,C#代码所占用的内存又称为mono内存,这是因为Unity是通过mono来跨平台解析并运行C#代码的,在Android系统上,游戏的lib目录下存在的libmono.so文件,就是mono在Android系统上的实现。

- 本地域

Unity有一些底层的本地代码功能是由C++编写的,并根据目标平台编译到不同的应用程序中。该域关心内部内存空间的分配,比如为各种子系统(渲染管线、物理系统、用户输入系统等)分配资源数据(纹理、音频、网格等)和内存空间。它还包括GameObject、Component等重要游戏对象的本地描述,以便和这些内部系统交互。这也是大多数内建Unity类(如Transform、Rigidbody组件)保存其数据的地方。

- 外部库

外部库是DirectXOpenGL以及项目中包含的自定义库和插件所使用的内存域,在C#代码中引用这些库将导致类似的内存上下文切换和后续成本

- 跨桥操作

托管域也包含存储在本地域中的对象描述包装器,当和Transform等组件交互时,大多数指令会请求Unity进入本地代码,在本地域生成结果,然后将结果复制回托管域。这就是托管域和本地域之间的本地-托管桥。当两个域对相同实体有自己的描述时,跨域他们之间的桥需要进行上下文切换,而这会为游戏带来很多相当严重的潜在性能问题。所以,我们在开发的过程中要尽量避免跨桥操作。具体如何避免跨桥操作请参考另一篇文章:【Unity】Unity开发进阶(一)减少跨桥(上下文切换)

堆和栈

在大多数现代操作系统中,运行时的内存空间分为两种类型:栈和堆。

- 栈

栈是内存中预留的特殊空间,专门用于存储小的、短期的数据值,这些值一旦超出作用就会自动释放,因此成为栈。与数据结构中的栈一样,内存栈也有入栈和出栈。
所有声明过的本地变量(比如int a;)都会放入栈内,当调用函数时处理他们的加载和卸载。也就是说在程序运行前变量已经定义到栈中了。这些函数调用通过所谓的调用栈进行拓展与收缩,当对当前函数完成调用栈处理时,它跳回调用栈之前的调用点,并从之前离开的位置继续执行剩余内容。之前内存分配的位置总是已知的,不需要执行内存清理操作,因为新的内存分配指挥覆盖旧数据,因此栈是相对高效的
栈的总大小通常很小,大约为兆字节(MB)。当分配超过栈可支持的空间时,可能会导致栈溢出,这会出现在执行大量调用栈时(例如无限循环)或有大量本地变量时,但大多数时候,尽管栈的大小相对较小,但很少会引起栈溢出

- 堆

堆表示所有其他的内存空间,并用于大多数内存分配**。由于我们想让大多数内存分配的持有时间比当前函数调用更长,因此不能栈上分配他们,因为栈会在方法执行结束后覆盖执行前后产生的结果。因为数据类型有时往往过大,或者需要保留到函数之外,这时就有了堆。在物理上堆和栈并没有什么区别,它们都只是内存空间,包含存在于RAM中的数据字节。操作系统会请求并保存这些数据字节。不同之处在于使用它们的时机、场合和方式。
在本地代码中,例如用C++编写的语言,这些内存分配通过手动处理,我们有责任确保正确地分配所有内存块,并在不需要时显式地释放内存。不然容易造成内存泄漏,直到内存不够,程序崩溃为止。
在托管语言中,内存释放通过垃圾回收器自动处理,在Unity程序的初始化期间,Mono平台向操作系统申请一串内存,用于生成堆内存空间(通常称为托管堆),供C#代码使用。这个堆空间开始相对较小,不到1MB,但是随着脚本代码需要新的内存块而增长。如果Unity不再需要它,那么该空间可以通过释放回操作系统来缩小。

- 堆栈的使用

下面通过图的方式展现在一个方法执行时堆栈的大致情况

Mono VM根据数据类型的不同决定哪些需要存在于栈中,哪些要存在于堆中。通常值类型的数据都直接存在栈中,而引用型数据则会在堆内开辟一段连续的空间,并将该段空间的堆内存地址放到栈中。具体在堆中开辟多大的空间,取决于实例化变量的实际类型,而不是取决于变量的声明类型。

垃圾回收

垃圾回收器(Garbage Collector,GC)有一个重要的工作,该工作确保不使用比所需要的更多的托管内存,而不再需要的内存会自动回收。也就是说,对象的创建以及销毁,GC都会参与其中。例如:如果创建一个GameObject,接着销毁它,那么GC将标记该对象使用的内存空间,以便在后续的时间回收这段内存。注意,内存回收不是实时的,而是只在不要的时候才会回收内存。

Unity使用的Mono版本中的GC是一种追踪式GC,它使用标记与清除策略。该算法分为两个阶段:

  1. 每个分配的对象通过一个额外的数据位追踪。该数据位表示对象是否被标记。这些标记设置为false,标识它尚未被标记。当收集过程开始时,它通过设置对象的标识位Alive为true,标记所有依然对程序可访问的对象。可访问对象要么是直接引用(例如栈上的静态或本地变量),要么是通过其他直接或间接可访问对象的字段(成员变量)来间接引用。理论上所有没有被引用的对象都应该被回收。
  2. 第二阶段涉及迭代这类引用(GC将在程序的整个生命周期中跟踪这些引用),并基于它的标记状态决定是否应该回收。如果对象未被标记,就会被视为回收的候选者。这个阶段会直接跳过已被标记过的对象,但在下次垃圾回收扫描之前会将它们重新设置为false,以完成新一轮的标记。

当第二个阶段执行结束,所有没被标记的对象会被正式回收以释放空间,然后重新访问创建对象的出事请求,如果GC已经释放了足够的空间,就在新释放的空间内分配内存并返回给调用者。如果释放后的空间不够,就只能再向系统申请更多的托管堆。

事实上,GC在内存中维护所有对象的列表,而应用程序维护了另一个独立的列表,其中仅包含它们中的一部分。只要程序用完对象,就简单的忘记它的存在,将其从列表中移除,而不去考虑对象是否需要被回收。也就是说,垃圾回收的工作是GC自己独立完成的,程序只需要维护自己的对象列表即可。

游戏对于性能的要求较高,为提高效率,可在场景切换时或资源使用不频繁时调用GC。

- Mono内存分配过程

Mono内存分为两部分:已用内存(Used)和堆内存(Heap),已用内存指的是Mono实际使用的内存,堆内存是Mono向系统申请的内存。当程序需要申请内存时,垃圾回收器(GC)会先在堆中查找是否有连续的且空间足够的内存位置,如果有这样的空位,则直接分配内存,如果没有则会先执行垃圾回收再尝试分配,如果仍然不够则会向系统申请更多的内存空间以供使用。

- 内存泄漏

通常情况下,GC所标记的对象都是可访问对象,但在某些特定情况下(互相引用、静态引用等),GC会产生错误的标记,将一些永远无法访问的对象标记为Alive,这将导致这部分内存一直未被回收,这部分内容就被称为内存泄漏。如果程序中定期的产生此类代码,程序会在运行一段时间后耗尽系统内存,导致程序强退、闪退,甚至系统重启。

腾讯的WeTest平台提供了一系列测试工具,其中就有测试内存泄漏的工具,详见文章:Unity游戏Mono内存管理及泄漏

- 内存碎片

在理想状态下,持续的分配和回收对象能够让堆保持恒定大小。然而,程序中的所有对象很少以他们分配的顺序被回收,而且他们占用的内存大小也很少一样,这就导致了内存碎片。

因为对象在堆中的存储方式必须是连续的,那么大的对象就无法完美的分段安插在已经被回收过的小空间内,这些回收后没有被重新利用起来的小空间就被称为内存碎片。这样的碎片多了以后,GC就经常需要申请新的堆,而新的分配也将花费更多的时间来寻找空间和申请空间。

- 运行时垃圾回收

在最坏的情况下,当游戏请求新的内存分配时,CPU在完成分配之前需要花费CPU周期来完成下面的任务:

  1. 验证是否有足够的连续空间用于分配新对象。
  2. 如果没有足够空间,迭代所有已知的直接和间接引用,标记他们是否可达。
  3. 再次迭代所有这些引用,标识未标记的对象用于回收。
  4. 迭代所有标识对象,以检查回收一些对象是否能为新对象创建足够大的连续空间。
  5. 如果没有,从操作系统请求新的内存块,以便拓展堆。
  6. 在新分配的块前面分配新对象,并返回给调用者。

此时CPU需要处理很多工作,特别当该新内存分配用于重要的对象,如粒子特效,新进入场景的角色,火切换场景过渡等。用户极有可能注意到此时GC冻结了游戏以处理极端情况。更糟的是,GC工作负载随着已分配的堆空间的增长而变差,因为擦除几兆字节的空间比扫描几千兆字节快得多。这也是很多游戏越玩越卡的原因所在,轻则掉帧,重则卡顿,甚至闪退。

- 多线程垃圾回收

GC运行在两个独立线程上:主线程和所谓的Finalizer Thread。当调用GC时,它运行在主线程上,并标志堆内存块为后续回收。这不会立刻发生,由Mono控制的Finalizer Thread在内存最终释放并可用于重新分配之前,可能会延迟几秒。

可以通过Projiler窗口中Memory Area的Total Allocated块观察此行为(绿线)。垃圾回收后可能需要几秒钟总分配值才会下降。由于这种延迟,不应该依赖内存一旦回收就可以使用这一观念,而且因此不应该浪费时间尝试消耗可用内存的最后一个字节,必须确保有某种类型的缓冲区用于未来的分配

GC释放的块有时会在一段时间后返回到操作系统,这将减少堆消耗的保留空间,并允许内存分配给其他应用程序。然而这是不可预测的,它取决于目标平台,因此不应该依赖它。唯一的安全假设是一旦内存分配给Mono,它就会被保留,不再可用于本地域或相同系统上运行的任何其他程序。


更多内容请查看总目录【Unity】Unity学习笔记目录整理

【Unity】Unity内存管理与优化(一)内存域、堆栈、垃圾回收、内存泄漏、内存碎片相关推荐

  1. 关于mysql内存管理_MYSQL内存管理及优化

    MYSQL内存管理及优化 内存是影响数据库性能的主要资源,也是mysql性能优化的一个重要方面: 内存优化的原则 将尽量多的内存分配给mysql做缓存,但是要给操作系统和其他程序的运行预留足够的内存, ...

  2. 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )

    文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...

  3. gperftools mysql_利用 gperftools 对nginx mysql 内存管理 性能优化

    利用 gperftools 对nginx 与 mysql  进行 内存管理  性能优化 降低负载. Gperftools 是由谷歌开发.官方对gperftools 的介绍为: These tools ...

  4. C语言 --- 动态内存管理(上)+优化版通讯录+笔试题

    文章目录 前言 一.为什么存在动态内存分配 二.动态内存函数的介绍 2.1.malloc函数+free函数 2.2.calloc函数+free函数 2.3.realloc函数 三.常见的动态内存错误 ...

  5. Mysql 内存管理及优化

    Mysql 内存管理及优化 1)内存优化原则 1) 将尽量多的内存分配给 MySQL 做缓存,但要给操作系统和其他程序预留足够内存. 2) MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 I ...

  6. 视频教程-iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化-iOS

    iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化 小码哥教育CEO,曾开发了2个iOS的流行开源框架(MJRefresh.MJExtension),目前在国内的使用率非常高. 李 ...

  7. iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化-李明杰-专题视频课程...

    iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化-236人已学习 课程介绍         得遇名师,突飞猛进!iOS培训王者MJ(李明杰)老师精心研发,iOS进阶课程,实用技术 ...

  8. linux内存管理的主要概念是虚拟内存,你知道linux内存管理基础及方法?

    描述 一.基本概念 (1)物理内存和虚拟内存 物理内存:系统硬件提供的真实物理内存 虚拟内存:利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为swap,swap类似于windows的虚拟 ...

  9. Unity 3D中的内存管理与优化游戏运行性能的经验

    Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极差的体验.类似这种情况并不少见,但是绝大部分都是可以避免的.虽 ...

最新文章

  1. 【PHPExcel】生成Excel2007文件并下载
  2. Readline-select
  3. Java 整数型的进制间的互相转换
  4. SLS机器学习最佳实战:日志聚类+异常告警
  5. 跟着百度学PHP[4]OOP面对对象编程-16-switch逻辑就语句
  6. 为什么大家都说 SELECT * 效率低
  7. /etc/rc.local开启自启不执行问题
  8. NSArray去除重复元素
  9. 【信号与系统实验】实验四 傅里叶变换、系统的频域分析
  10. Windows如何清理C盘的缓存文件和删除恶意软件
  11. 服务器怎么使自己的文件夹加密,NAS中如何创建和使用加密文件夹
  12. 微信小程序检测版本是否有更新
  13. 洛谷blog传送门qwq
  14. NVIDIA GPU 常用操作
  15. 古诗三百首(html)
  16. 线上虚拟展馆增强观众沉浸式体验感
  17. 左倾红黑树的go语言实现
  18. 电子防盗标签 之一:射频软标签 《转》
  19. linux发送免费短信
  20. Android小闹钟程序【安卓进化十三】

热门文章

  1. 实验十五 Java Swing 图形用户界面程序设计应用
  2. php正则匹配preg_match,php正则表达式中preg_match函数的详解
  3. 【通州建设】地铁S6线更名为21号线!途径通州多站!
  4. SketchUp模型组件【iMod · 精选242 —— 现代客厅SU模型】
  5. GitHub新手用法详解【适合新手入门-建议收藏!!!】
  6. python中for in语句有什么用法
  7. 大数据报告|70%的中国人过劳死危险,其中有你吗?
  8. 鸟什么羊什么的成语(鸟什么羊什么四字成语大全)
  9. 华为手机怎么语音服务器,原来华为手机实现文字转语音这么简单!今天才知道,真是绝了...
  10. Transformer-XL