当前位置: 首页 > news >正文

Unity学习之垃圾回收GC

        理解 GC 是 Unity 性能优化的基石,因为不恰当的内存管理会导致游戏在关键时刻卡顿(GC Spike),严重影响玩家体验。

        C#内部有两个内存管理池:堆内存和栈内存。栈内存(stack)主要用来存储较小的和短暂的数据,堆内存(heap)主要用来存储较大的和存储时间较长的数据。C#中的变量只会在堆栈或者堆内存上进行内存分配,变量要么存储在栈内存上,要么处于堆内存上。

        垃圾回收主要是指堆上的内存分配和回收,C#中会定时对堆内存进行GC操作。

1. 什么是垃圾回收GC

        在 C# 中,我们使用 new 关键字来创建对象。这些对象被分配在 ** 托管堆(Managed Heap)** 上。

        简单来说,GC 就是一个后台 “清洁工”。它的工作是自动找出那些不再被任何变量引用的对象,并回收它们占用的内存,以便为新对象腾出空间。

        为什么需要自动 GC? 手动管理内存(如 C++ 中的 new 和 delete)非常容易出错。忘记释放内存会导致内存泄漏(Memory Leak),而重复释放或释放正在使用的内存会导致程序崩溃。GC 的出现就是为了让开发者从繁琐且危险的内存管理中解放出来。

2. GC工作流程(简化版描述)

(1) 标记(Mark):GC 会暂停应用程序的所有线程(这就是导致卡顿的原因),然后从 “根” 对象(如静态变量、当前栈上的局部变量)开始遍历,标记所有仍然存活的对象。

(2)清除(Sweep):GC 遍历整个堆,将所有未被标记的对象(即垃圾)占用的内存空间标记为 “空闲”,并将这些空闲空间加入一个 “空闲列表”。

(3)压缩(Compact)(可选):为了减少内存碎片,GC 可能会移动所有存活的对象,将它们紧凑地排列在堆的一端,从而形成一整块连续的空闲内存。这个过程开销很大。

关键概念:GC 暂停(GC Pause)

GC 在执行 “标记” 和 “压缩” 阶段时,必须暂停主线程。这个暂停的时间就是我们常说的GC 卡顿(GC Spike)。游戏逻辑越复杂,堆上的对象越多,GC 暂停的时间就越长。

3. Unity 中的 GC:你需要知道的关键点

Unity 使用的是 Mono 或 IL2CPP 后端,它们的 GC 有一些特定的行为和注意事项。

分代回收(Generational GC)

nity 的 GC 是分代的,它将对象分为三代:

(1)第 0 代(Generation 0):新创建的对象。GC 最频繁地检查这一代。回收成本最低。

(2)第 1 代(Generation 1):在第 0 代 GC 中存活下来的对象。GC 检查频率较低。

(3)第 2 代(Generation 2):在第 1 代 GC 中存活下来的对象。GC 检查频率最低,但回收成本最高。

工作原理假设:新创建的对象很快会变成垃圾。因此,GC 专注于频繁清理年轻的对象(第 0 代),而很少去打扰那些长期存活的对象(第 1、2 代)。一次完整的 GC(回收所有代)会产生非常长的停顿。

4. Unity GC触发条件

(1)周期性触发,平台不同周期时间不同

(2)当你 new 一个对象时,如果托管堆的剩余空间不足,GC 会被触发来尝试回收内存。

(3)通过调用 System.GC.Collect()在游戏的正常逻辑中,你几乎永远不应该手动调用它,除非在一些特定的、可控的场景(如场景切换的加载界面)。

5. Unity的GC执行的问题

(1)每次执行GC,都会暂停Unity主线程,因此游戏画面会停止渲染,造成卡顿与掉帧

(2)执行GC若内存仍然不足,会申请更大内存空间,造成更长时间卡顿

(3)Unity的GC采取不压缩(内存块不会从离散变连续)的机制,可能导致内存碎片化

6. 优化如何尽量避免不必要的GC

(1) 尽量减少不必要的new对象的次数。谨慎使用new(),并且避免在Update等周期函数中使用,array,class等分配在堆上

(2) 减少临时变量的使用,多使用公共对象,多利用缓存机制。(将容器定义到函数外,用到容器的时候进行修改即可)。

(3) 对于大量字符串拼接时,将StringBuilder代替String。(string不可修改性,修改即创建一个新的string对象,旧的直接抛弃等待GC,但少量字符串拼接用string,性能优于stringbuilder)

// Bad
string message = "";
for (int i = 0; i < 10; i++)
{message += "Item " + i + ", "; // 循环10次,产生10个垃圾字符串!
}// Good
private StringBuilder _stringBuilder = new StringBuilder();
void BuildMessage()
{_stringBuilder.Clear();for (int i = 0; i < 10; i++){_stringBuilder.Append("Item ").Append(i).Append(", "); // 无GC Alloc}string finalMessage = _stringBuilder.ToString(); // 只在最后产生一次分配
}

(4) 使用扩容的容器时,例如:List,StringBuilder等,定义时尽量根据存储变量的内存大小定义储存空间,减少扩容的操作。(扩容后,旧的容器直接抛弃等待GC)

(5) 闭包:通过匿名函数实现,可以捕获外部变量,不会因为函数执行完就销毁。

    void Start(){int externalVar = 10; // 外部变量(Start函数的局部变量)// 定义一个匿名函数(闭包),它捕获了externalVarSystem.Action closureFunc = () => {externalVar += 5;Debug.Log("闭包内的变量值:" + externalVar); // 输出:15};closureFunc();Debug.Log("外部的变量值:" + externalVar); // 输出:15}

(6) 使用struct代替class,管理轻量数据,因为struct分配在栈上

(7) 多使用对象池与Cache(缓存),实例化对象产生大量垃圾 Instantiate(new GameObject());解决手段是 使用对象池技术来解决这个问题

(8) 在Update中使用射线检测时,会疯狂创建数组,会产生GC

void Update() {Physics.RaycastAll(new Ray());
}

可以使用 Physics.RaycastNonAlloc避免,下面只创建了一次数组

private RaycastHit[] hits = new RaycastHit[10];
void Update() {Physics.RaycastNonAlloc(new Ray(), hits);
}

(9) 当使用yield return 的时候也会产生一个新的对象

IEnumerator Wait() {// 协程返回会创建一个引用类型数据yield return new WaitForSeconds(5f);
}

可以用 多个协程使用同一个对象来解决问题

private WaitForSeconds delay = new WaitForSeconds(5f);
IEnumerator Wait() {// 协程返回会创建一个引用类型数据yield return delay;
}

可以写一个具体的 YieldHelper 做为一个工具类,来具体处理协程提前应该创建好的变量,进而消除协程的GC

// yield 的意思是产出
IEnumerator WaitToDo(float setTime) {// 这里的堵塞逻辑其实是 yield return YieldHelper.WaitForSeconds(setTime);	// do something
}public static class YieldHelper {public static IEnumerator WaitForSeconds(float totalTime) {float time = 0;while (time < totalTime) {time += Time.deltaTime;yield return null;		// 等待一帧}}
}

(10) 装箱(变量转化为object类型)与拆箱(object类型转为基本类型),可以使用.ToString()使得代码编译减少装箱操作,从而减少堆上内存分配。

避免不必要的装箱
使用泛型集合List<int> 代替 ArrayListArrayList 会将所有元素都装箱为 object

(11) List<T>StringBuilder:在StartAwake中初始化,而不是在UpdateLateUpdate中。

// Bad
string message = "";
for (int i = 0; i < 10; i++)
{message += "Item " + i + ", "; // 循环10次,产生10个垃圾字符串!
}// Good
private StringBuilder _stringBuilder = new StringBuilder();
void BuildMessage()
{_stringBuilder.Clear();for (int i = 0; i < 10; i++){_stringBuilder.Append("Item ").Append(i).Append(", "); // 无GC Alloc}string finalMessage = _stringBuilder.ToString(); // 只在最后产生一次分配
}

(12) 谨慎使用 LINQ 和 Lambda 表达式

LINQ 非常方便,但在性能敏感的Update循环中应避免使用。

注意闭包:Lambda 表达式(() => { ... })可能会捕获外部变量,导致创建闭包对象,产生 GC。

http://www.dtcms.com/a/419320.html

相关文章:

  • 五次样条速度规划方法介绍
  • 找人做网站被骗怎么办wordpress 评论 姓名
  • 如何建立公司企业网站社区网站建设方案ppt
  • 解密C++多态:一篇文章掌握精髓
  • Git 进阶指南:深入掌握 git log 查看提交历史
  • C++ 引用协程
  • 淄博企业网站设计公司网页无法打开怎么办
  • 添加测试设备到苹果开发者平台
  • 填坑:VC++ 采用OpenSSL 3.0接口方式生成RSA密钥
  • 郑州做网站的网站再就业技能培训班
  • Vscode 连接服务时候一直出现setting ssh Host server
  • 全面解析数据库审批平台:主流工具对比与选型指南
  • 【Docker项目实战】使用Docker部署IT运维管理平台CAT
  • spring事务传播级别的实操案例2
  • 泰州专一做淘宝网站如何用html做网站头像
  • 电子商务网站设计与实现个人网站做捐赠发布违法吗
  • Java滑动窗口算法题目练习
  • 介绍一下HTTP和WebSocket的头部信息
  • Linux系统学习之---库的理解和加载(毛坯初版...)
  • 南山模板网站建设公司怎么看网站的外链
  • 企业网站策划大纲模板文山住房和城乡建设局网站
  • Linux 基础IO与系统IO
  • 【IEDA】已解决:IDEA中jdk的版本切换
  • idea推荐springboot+mybatis+分页查询插件之PageHelper
  • 南非网站域名做网站微信支付多少钱
  • 网站开发 图形验证码网站建设衤金手指下拉10
  • OPenssh6代码移植的依赖库 OpenSSL双库连接问题的解决方案
  • 商务网站建设组成包括网站优化wordpress 换行
  • tiktok scheme
  • Xrdp 远程桌面配置【笔记】