游戏人生
About Me
  • 你好
  • Math
    • Number
      • Float IEEE754对确定性的影响
      • Pairing Function及其用途
    • Vector and Matrix
      • TRS基础概念
      • LossyScale深入分析
    • Quatenion
      • FromToRotation实现细节
    • Lerp and Curve
      • Slerp球形插值
      • Bezier Curve为什么重要
      • Interpolation和Extrapolation实现细节
  • Programming
    • C#
      • 学习资料
      • C# struct灵魂拷问
      • CIL的世界:call和callvirt
      • .NET装箱拆箱机制
      • .NET垃圾回收机制
    • Go
      • 基础特性
      • 如何正确的判空interface
      • 如何用interface模拟多态
      • 如何定制json序列化
      • 如何安全在循环中删除元素
      • 如何安全关闭channel
      • 如何集成c++库(cgo+swig)
      • 如何性能测试(benchmark, pprof)
    • Lua
      • 基础特性
  • General Game Development
    • Game Engine
      • 学习资料
      • 关于游戏引擎的认知
    • Networking
      • 帧同步
      • 状态同步
      • 物理同步
    • Physics
      • PhysX基本概念
      • PhysX增加Scale支持
      • PhysX场景查询
      • PhysX碰撞检测
      • PhysX刚体动力学
      • PhysX角色控制器
      • PhysX接入项目工程
      • 物理同步
      • 物理破坏
    • Design Pattern
      • 常用设计模式
      • MVP 架构模式
      • ECS 架构模式
  • Unity
    • Runtime
      • Unity拥抱CoreCLR
      • 浅析Mono内存管理
    • UGUI
      • 浅析UGUI渲染机制
      • 浅析UGUI文本优化
      • 介绍若干UGUI实用技巧
    • Resource Management
      • 浅析Unity堆内存的分类和管理方式
      • 深入Unity资源
      • 深入Unity序列化
      • 深入Assetbundle机制
    • Async
      • 深入Unity协程
      • 介绍若干Unity协程实用技巧
      • 异步动作队列
    • Hot Reload
      • Unity+Xlua
      • Xlua Examples学习(一)
      • Xlua Examples学习(二)
    • Editor Extension
    • Performance
      • 浅析Unity Profiler
      • 介绍一个Overdraw分析工具
  • Platform
    • WebGL
  • Real-world Project
    • Souce Engine
    • DOOM3 BFG
Powered by GitBook
On this page
  • Mono是什么
  • GC为什么耗时
  • 何谓Mono内存泄漏
  • 减少GC的常见套路
  • foreach
  • 字符串拼接
  • struct替代class
  • IDisposable与非托管资源
  1. Unity
  2. Runtime

浅析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占用内存的api
[System.Runtime.InteropServices.DllImport("__Internal")]
public static extern long Mono_gc_get_used_size();

[System.Runtime.InteropServices.DllImport("__Internal")]
public static extern long Mono_gc_get_heap_size();

当整个应用被杀死时,Mono占用的内存才能还给OS。

一个有趣的小知识:当在PC模拟器上运行Unity游戏时,点击三角形终止游戏运行,所有内存真的会归还给操作系统吗?答:不是的。如果你用了多线程,那么unity是不对其他线程负责的,点击三角形关闭的只是unity对应的线程。若其他线程访问了unityEngine相关的数据,将是无效的。

减少GC的常见套路

foreach

注:在Unity5.5以前,foreach会产生GC,要使用下面的写法。之后就可以直接用foreach了。

遍历字典的无GC写法:

var tmp = dic.GetEnumerator();
while(tmp.MoveNext())
{
    //tmp.Current.key;
    //tmp.Current.Value
}

字符串拼接

string a = "afadf";
string c = a + i;

当i不是string类型,实际执行的是string.Concat((object)a, (object)i),存在装箱拆箱。 这种字符串拼接常见于打日志操作。为此可以做一些优化。

long uin = 111;

MyDebug.Log("the player uin is: " + uin);//常见写法,有额外GC和装箱拆箱

MyDebug.Log("the player uin is: " + uin.ToString());//无装箱拆箱,有额外GC

StringBuilder sb = new StringBuilder();
MyDebug.Log(sb.Append("the player uin is: ").Append(uin).ToString()); 
//无装箱拆箱,无额外GC,但是这样写麻烦

利用泛型函数可以简化上述无装箱拆箱和额外GC的写法。

StringBuilder sb = new StringBuilder();
public static void Debug(string str)
{
    Debug.Log(str);
}

public static void Debug<T1, T2>(T1 arg1, T2 arg2)
{
    sb.Length=0;//清空sb
    sb.Append(arg1).Append(arg2);
    Debug.Log(sb.ToString());
}

//可以再补充支持3个、4个参数的。。。

MyDebug.Log("the player uin is: " , uin);//现在可以这么写,无额外GC,无装箱拆箱

struct替代class

struct是在栈上分配的,分配速度快,而且不需要GC。

通常我们创建的引用类型总是多于值类型。如果以下问题的回答都为yes,那么我们就应该创建为值类型:

  • 该类型的主要职责是否用于数据存储?

  • 该类型的共有借口是否完全由一些数据成员存取属性定义?

  • 是否确信该类型永远不可能有子类?

  • 是否确信该类型永远不可能具有多态行为?

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++析构函数名字的函数),在这里可以作为释放非托管资源的最后一道屏障。具体怎么用,参考上面的链接。

PreviousUnity拥抱CoreCLRNextUGUI

Last updated 2 years ago

The play mode is just another thread inside the same Mono VM. --

首先可以看看

Dreamora
Unity官方对减少GC的总结
什么是非托管资源(Unmanaged Resources)?
使用IDisposable的正确姿势