?内存和垃圾回收原理
内存分为栈内存和堆内存,栈用来存储短期的和小块的数据,堆用来存储长期的和大块的数据。
Unity自动内存管理:
1.当一个变量被创建时,会在栈或堆内存池上申请一块内存空间。(值类型存在栈上,引用类型存在堆上。)
2.只要这个变量在作用域内,可以被代码访问。分配给它的内存在使用中,则称这款内存已被分配。根据内存空间位置,被称为栈上对象或者堆上对象。
3.如果这个变量不在作用域内,在代码中被释放了。内存不再被需要了,会返回到当初申请的内存池中,这个过程叫内存释放。当变量不在作用域时,栈上内存会立刻被释放。但堆上内存不会立刻释放,只是会把这块内存标记为等待释放,内存还是已分配状态。
4.调用GC垃圾回收时,会遍历已分配的堆内存,找到标记为“等待释放”的内存块,将其释放回内存池中。
栈内存
栈内存分配快速和简单,是因为栈元素都是小且存在很短时间,分配和释放内存总是按照预期的时间和大小。
栈工作像栈数据类型一样,元素只能按照严格的顺序添加或者移除。因为这种简洁和严格,所以很快。当一个变量存储在栈上时,内存简单的在栈的“末尾”被分配,当栈上的变量不在作用域时,存储它的内存马上被返还回栈以便重用。
堆内存
堆内存会存储长期和短期的数据,而且数据类型很多,而且大小不固定。 分配内存过程: 1.先判断堆上是否有足够大的连续空闲内存。 2.若内存不足,则进行GC垃圾回收。(过程较慢) 3.GC后,若还内存不足,则需要增加堆内存空间。(过程较慢)
垃圾回收过程
1.检查堆内存所有对象,和对象的引用,看对象是否在作用域内。
2.对不在作用域的对象进行标记。
3.删除被标记的对象,将其内存还给内存池。
对象多,对象的引用多,都会增加GC的工作。
触发垃圾回收的条件
1.当准备分配堆内存,但堆内存不足时。 2.Unity定时触发。 3.手动触发。
垃圾回收引起的性能问题
低帧率,间歇性卡死,性能不稳定。
在Unity Profiler的CPU分类中,以Time ms排序,如果GC.Collect()在上方,或者GC.Collect()的Time ms较大,可以考虑优化GC垃圾回收问题。
🏆优化
优化原理
1.降低GC执行时间。 较少的分配堆内存,和对对象的引用。
2.降低GC执行频率。降低对堆内存的分配和释放。
3.再合适的时机手动调用。较困难和不常用。
🌟优化方案
铺垫了这么多,终于到了优化的方法。
(1)缓存数据
如果代码重复调用堆内存,再抛弃结果,造成不必要的垃圾。那么我们应该保存结果的引用,并进行服用。这就是缓存技术。
实现: 不在方法中创建临时对象,而是创建全局对象。在方法中使用。
//下面例子中,函数每次被调用时都会造成堆内存分配,因为有新的数组创建。
void OnTriggerEnter(Collider other)
{
Renderer[] allRenderers = FindObjectsOfType<Renderer>();
ExampleFunction(allRenderers);
}
//下面代码只有一次堆内存分配,因为数组创建和填充一次,然后被缓存了。
//缓存数组可以复用而不用生成更多垃圾。
private Renderer[] allRenderers;
void Start()
{
allRenderers = FindObjectsOfType<Renderer>();
}
void OnTriggerEnter(Collider other)
{
ExampleFunction(allRenderers);
}
(2)不在频繁调用的函数中分配堆内存
把频繁在Update或LateUpdate中要调用声明的对象,改为在Start或Awake中进行声明。不在Update中new对象。
在Update中进行操作前进行判断,如判断是否需要修改坐标。或增加计时器。 降低代码执行频率。
(3)清除集合,而不是每次都创建新的
使用临时存储变量的List时,在使用前Clear一下即可。不要每次都New它。
(4)对象池
对于频繁创建和销毁的对象,如子弹,敌人等。使用对象池机制。
使用两个List,分别存储已显示的对象,和隐藏(表示被销毁)的对象。
需要创建对象时,判断隐藏对象池中是否有隐藏的对象,若有,则直接显示并使用此对象;若没有,则创建一个对象并使用,并将此对象放入显示对象池中。
需要销毁对象时,将此对象隐藏,放入隐藏对象池中。
(5)string字符串
string字符串是引用类型,而且每次修改都会创建一个新的引用。
1.不要频繁的拼接字符串,如时间的数字变量+单位名称。将其修改为两个UI,一个显示数字,另一个显示单位。
2.可以使用StringBuilder来拼接字符串使用。
(6)Unity函数调用的优化
有些Unity函数会造成堆内存分配,可以将其缓存起来,而不是每次都用UnityAPI进行查找使用。
使用GameObject.CompareTag()不会产生垃圾。使用GameObject.tag进行判断会产生垃圾。
(7)装箱和拆箱
要小心某些API底层调用了拆箱和装箱操作。
(8)协程的优化
yield 不会产生垃圾,但后面的参数会产生垃圾。
yield return 0;//这会产生垃圾是因为发生了装箱.
//如果我们只是想要等待一帧,而不产生垃圾
//最好是使用下面的代码:
yield return null;
缓存WaitForSeconds,而不是每次都new。
WaitForSeconds delay = new WaitForSeconds(1f);
while (!isComplete)
{
yield return delay;
}
如果因为协程产生了很多垃圾,则要考虑自己写一个同功能的代码,看看是否可以优化性能。
(9)foreach循环
在Unity5.5之前,foreach循环数组以外的集合时,每次循环都会产生垃圾。这是因为幕后的装箱操作。每次循环开始System.Object都会在堆上被创建,在循环结束时被销毁。这个问题已经在Unity5.5版本中修复了。
(10)函数的引用
函数引用,不论是匿名方法还是命名的方法,在Uniyt中都是引用类型的变量。他们会引起堆内存分配。把匿名方法转换为闭包会显著的增加内存占用和堆内存分配的大小。
函数引用和闭包具体怎么明确的分配内存,取决于不同的平台和编译设置,但是考虑到垃圾回收,我们最好少使用函数引用和闭包。
匿名函数的两种形式:delegate开头,或者lambda表达式
Action lawer;
lawer=delegate() //使用匿名方法进行委托
{
Console.WriteLine("I have no money!!!");
};
lawer = () => //使用lambda表达式进行委托
{
Console.WriteLine("I have no money!!!");
};
(11)少用Linq和正则表达式
他们都会产生垃圾,因为需要装箱操作,如果需要考虑性能问题,那么最好不要使用他们。
(12)结构体struct的优化
结构体虽然是值类型,但如果内部定义了string引用类型。那么GC时,会查找整个结构体。
如果结构体数组中,结构体定义包含了string。我们可以将string类型单独存放为一个数组。结构体中只定义值类型。
(13)减少对对象的引用
删除没必要的对象引用,堆内存上对象不变的情况下,引用越少,GC越快。
如:返回值是对象 如果返回值是对象,可以优化为对象的编号。
如果对你有帮助,可以点个赞鼓励我哈。
参考资料 https://www.cnblogs.com/alan777/p/6155501.html
|