浅析Mono内存管理
本文写于早期,故停止对其内容加以扩充,仅对当年理解不足之处加以修正。
本文讨论的C#对象指的是Unity.Object以外的类型对象,是由Mono VM管理的。
Mono是什么
我们总在说的“Mono”,到底指的是什么?直白点说,Mono是.NET的一套免费公开的实现。其特性要落后于.NET。而Unity目前使用的Mono更是老版的Mono(2.6.5版) Unity2017以后默认的Mono环境兼容了.NET4.6。
GC为什么耗时
具体是怎样判断哪些需要被回收呢?我们的直观想法是引用计数
,很快很简单。但这样会有循环引用而不得释放的问题(iOS用的是自动引用计数,为了解决这个问题,需要开发者使用弱引用)。Unity携带的Mono,采用的是Boehm GC
,一种没有分代的、没有整理的、不精确的GC方案。Boehm策略是rootObj
,不是引用计数。最新的Mono已经默认采用分代的SGen GC
。
概括一下GC耗时原因:
Boehm GC
以全局数据区和当前寄存器中的对象为根节点进行遍历,这本身就耗时;启动GC前,会暂停所有些需要Mono内存分配的线程(也叫做
stop the world
),完成GC后才恢复,因此造成卡顿。
所以,一方面我们要少制造垃圾,另一方面要控制垃圾回收调用的时机。
何谓Mono内存泄漏
有了GC机制,为什么还有Mono内存泄漏?
答:Mono内存泄漏,并不是真的泄漏,而是Mono虚拟机向操作系统(os)申请到的内存不会还给os,即使GC后,那些不用的内存(unused size
)也是自己保存起来备用。不够的时候会再向OS申请。因此,Mono占用的内存(heap size
)是只增不减的。当有些资源实际上已经不用了,却占着空间无法被GC回收,就称为Mono内存泄漏。
当整个应用被杀死时,Mono占用的内存才能还给OS。
一个有趣的小知识:当在PC模拟器上运行Unity游戏时,点击三角形终止游戏运行,所有内存真的会归还给操作系统吗?答:不是的。如果你用了多线程,那么unity是不对其他线程负责的,点击三角形关闭的只是unity对应的线程。若其他线程访问了unityEngine相关的数据,将是无效的。
The play mode is just another thread inside the same Mono VM. --Dreamora
减少GC的常见套路
首先可以看看Unity官方对减少GC的总结
foreach
注:在Unity5.5
以前,foreach会产生GC,要使用下面的写法。之后就可以直接用foreach了。
遍历字典的无GC写法:
字符串拼接
当i
不是string类型,实际执行的是string.Concat((object)a, (object)i)
,存在装箱拆箱。
这种字符串拼接常见于打日志操作。为此可以做一些优化。
利用泛型函数可以简化上述无装箱拆箱和额外GC的写法。
struct替代class
struct是在栈上分配的,分配速度快,而且不需要GC。
通常我们创建的引用类型总是多于值类型。如果以下问题的回答都为yes,那么我们就应该创建为值类型:
该类型的主要职责是否用于数据存储?
该类型的共有借口是否完全由一些数据成员存取属性定义?
是否确信该类型永远不可能有子类?
是否确信该类型永远不可能具有多态行为?
IDisposable与非托管资源
什么是非托管资源(Unmanaged Resources)? 使用IDisposable的正确姿势
非托管资源,顾名思义,占用的是非托管堆上的内存。常见的非托管资源如:
Open files
Open network connections
Unmanaged memory
In XNA: vertex buffers, index buffers, textures, etc.
但要注意,并不是所有文件读写操作都是在与非托管资源直接打交道,很多时候.net封装了一层wrapper类供我们使用,暗地里完成了很多“dirty work”。
When you open file in .NET, you probably use some built-in .NET class System.IO.File, FileStream or something else. Because it is a normal .NET class, it is managed.
对于托管资源,其由GC管理;对于非托管资源,是要手动调用自定义的卸载方法的。为了统一命名这个卸载方法,要求实现IDisposable
接口。如果没有手动调用,那么,由于这个非托管资源是挂在c#的object底下的,当这个object被GC最终处理时,会调用一个Finalize
方法(c#中则是去实现一个类似c++析构函数名字的函数),在这里可以作为释放非托管资源的最后一道屏障。具体怎么用,参考上面的链接。
Last updated