参考链接:https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity4-1.html?_ga=2.107957826.1120751560.1556075409-1373067411.1554687911

在Unity中,托管堆扩充其大小比其缩小要容易得多。此外Unity的垃圾回收机制倾向于碎片化内存空间,这种机制导致难以缩小托管堆的大小。

  1. 托管堆的运作和托管堆的扩充

在Unity中托管堆就是把这段内存托管给Mono 和IL2CPP(准确来说应该是Mono VM和IL2CPP VM)。所有的非空引用类型和装箱的值类型都分配在托管堆中。

GC:Unity的GC使用的是Boehm GC algorithm。该算法有两个特点:1.会扫描整个堆,因此当堆变大时,性能会下降。2.已分配的对象不会重新定位,这以为着两个对象中间的空余空间会碎片化,下图。




读取unity游戏内存 unity内存不足_unity hub里面安装空间不足


GC会在两种情况下触发:1.定时触发(不同平台不同)。2.当剩余的空间不足以分配时(如下图)。


读取unity游戏内存 unity内存不足_数组_02


这意味着,即使碎片的空间总和足够大,但没有一个碎片可以装得下,最终也不能分配内存。

这时托管堆就会扩充其大小,不同平台扩充的大小不同,Unity平台一般扩充为原来的两倍。

2.堆扩充带来的问题

  • Unity不会经常释放掉扩充的空间,即使有很多空间都没有使用,以备不时之需。
  • 在大多数平台,Unity最终会返还这部分空间给操作系统,然而返还的耗时不能保证。
  • 托管堆使用的地址空间不会返还给操作系统。

3.减少GC一些简单方法

  • 不要在频繁调用的地方分配内存
List<float> m_NearestNeighbors = new List<float>();//缓存起来

void Update() {

    m_NearestNeighbors.Clear();

    findDistancesToNearestNeighbors(NearestNeighbors);

    m_NearestNeighbors.Sort();

    // … use the sorted list somehow …

}


  • 匿名函数和闭包

(1)在C#中,函数时引用类型,不管是匿名函数还是已定义的函数,作为参数传入时,都会分配堆内存。

(2)当匿名函数中使用外部变量形成闭包时,C#会生成一个匿名函数的类,该类包含外部变量的。匿名函数作为参数传入Sort时,如下代码,会实例化该匿名函数的类,里面包含一个int。因为是对象,所以会分配堆内存。


List<float> listOfNumbers = createListOfRandomNumbers();

int desiredDivisor = getDesiredDivisor();

listOfNumbers.Sort( (x, y) =>

(int)x.CompareTo((int)(y/desiredDivisor)) //调用了外部变量,

);


(3)所有的匿名函数在IL2CPP都会分配堆内存,而Mono不会。

  • 减少装箱——值类型隐式转换为引用类型时
int x = 1;

object y = new object();

y.Equals(x);


  • 字典和枚举

当使用dic.add(key,value),会调用Object.getHashCode(Object),此时Enum是值类型,会导致装箱。


enum MyEnum { a, b, c };

var myDictionary = new Dictionary<MyEnum, object>();

myDictionary.Add(MyEnum.a, new object());


解决:定一个实现IEqualityComparer接口的类,作为Dictionary的比较器。


public class MyEnumComparer : IEqualityComparer<MyEnum> {

    public bool Equals(MyEnum x, MyEnum y) {

        return x == y;

    }

    public int GetHashCode(MyEnum x) {

        return (int)x;

    }

}
static MyEnumComparer myEnumComparer = new MyEnumComparer();
...
Dictionary<MyEnum, string> dict = new Dictionary<MyEnum, string>(myEnumComparer);


  • 不使用foreach循环

(1)foreach循环每次在循环结束的时候,由于Enumerator(值类型)实现了IDisposable接口,所以会导致一次装箱。

(2)通过Enumerator的方法调用遍历要比普通for循环要慢


int accum = 0;

foreach(int x in myList) {

    accum += x;

}


  • UnityAPI 的数组

mesh.vertices会新分配内存,所以应该缓存起来,而不是放到循环里。


var vertices = mesh.vertices;

for(int i = 0; i < vertices.Length; i++)

{

    float x, y, z;

    x = vertices[i].x;

    y = vertices[i].y;

    z = vertices[i].z;

    // ...

    DoSomething(x, y, z);   

}


  • 空数组的重复利用

当函数要返回一个空数组时,应使用一个单例数组长度为0的空数组,而不是每次都重复创建一个空数组。