浅谈Unity内存管理

news2025/2/26 22:58:42

浅谈Unity内存管理


前言

很早之前记录的Unity内存相关的知识点,在此补充到博客上来。有什么不对的地方欢迎指正探讨。

内存概念

虚拟内存(Virtual Memory)

众所周知,物理内存就是插在计算机主板内存槽上的实际物理内存。

虚拟内存其实就是计算机系统内存管理的一种技术。虚拟内存使得应用程序认为它拥有一个连续完整的地址空间,而实际上,虚拟内存通常是被分隔成多个物理内存碎片,在需要时进行数据交换。

Unity在运行时的内存占用可以参考下图:

在这里插入图片描述

原生内存(Native Memory)

Unity本质上是一个C++写的引擎,使用的.NET脚本虚拟机。Unity会给向操作系统请求维持虚拟机所需要的内存和我们开发所需要的内存。

而我们开发所需要的内存就包括原生内存和托管内存。

一些托管对象就会被分配到托管内存中,如引用类型数据等;而一些非托管对象和Unity自己的管理器对象就会分配到原生内存中,如AssetBundle、Assets、Scenes、GameObjects等。

在大多数情况下,使用者不能直接访问或修改原生内存,Unity也不建议这么操作。但是可以通过Unity 提供的C# API 间接访问原生内存。

不同于托管内存,当原生内存不在使用时,Unity会返还给操作系统。

托管内存(Mono堆、Mono Heap、托管堆)

托管内存是Unity项目的选定脚本运行时(Mono或IL2CPP)自动管理的内存部分。

托管堆里面的内存包含了所有C#托管对象,开发者无需显式地调用内存释放接口或进行内存释放操作,只需要考虑引用关系,引擎底层通过垃圾回收机制来释放堆内存。与显式分配/释放相比,自动内存管理编码更少,并减少了内存泄漏的可能性。

什么是Mono?

Mono是一套用于实现跨平台运行的框架。Unity通过Mono运行时的编译器将IL编译成各个平台的原生代码。

需要注意的是,托管内存即使它的大部分是空的,仍不会释放回操作系统。这是为了防止在发生更大的分配时需要重新扩展堆。

(在大多数平台上,Unity最终还是会将托管堆空白部分内存释放回操作系统。但是发生这种情况的时间间隔无法保证并且不可靠。所以总得来说可以看作是不还。)

已分配/申请内存(Reserved Memory)

内存页(Page)通常来说是内存管理最小的单位,Unity每次申请内存会按照内存块(Block,若干个Page)的方式申请。所有申请到的内存就被称为Reserved Memory。

已使用内存(Used Memory)

在所有Reserved Memory中,真正在被使用的内存,叫做Used Memory。

内存分配

在简单了解了上述内存概念之后,我们可以思考一下,游戏运行时内存是如何分配及管理的呢?

引擎在分配托管内存时并不是向操作系统 “即拿即用”,而是首先申请一定量的连续内存,然后供自己内部使用,待空余内存不够时,引擎才会向系统再次申请一定量的连续内存进行使用。

在这里插入图片描述

需要申请托管堆内存的时候,托管堆会首先检查当前堆内的空间内是否存在足够的连续空间。

在这里插入图片描述

如果不能找到足够的连续空间,就会进行一次GC。

如果在GC运行之后,仍然没有足够的连续空间来容纳所请求的内存量,托管堆就会执行内存扩展操作,向操作系统要更多的内存(Reserved Memory)。堆扩展的具体大小取决于平台,在大多数平台上,扩展量是先前扩展量的两倍。

这些空出来,却又不能被重复利用的内存就会成为内存碎片。

它们既不能被利用,又不会被销毁。

那么为什么会产生这样的内存碎片呢?

Unity GC

可以看到,Unity这里采取的GC策略,即不分代,也不压缩。

Unity的GC本质上就是Mono的GC,Unity早期版本的Mono,采用的是Boehm垃圾回收机制,一种非分代、非压缩、标记清除的保守式GC。托管内存只增不减。

Boehm垃圾回收机制使用的是Mark-Sweep,也就是先通过一个Root指针来遍历所有的被引用的对象,并标记。直到遍历完所有的指针。再次遍历整个,将未标记的内存释放。

在这里插入图片描述

Boehm
GC是开源项目,感兴趣的话可以去学习源码:Boehm垃圾回收机制开源网址。

后来IL2CPP由Unity自己重写垃圾回收机制,是升级版的Boehm垃圾回收机制。托管内存可以降低,但是内存返还的条件就是,如果某一个Block被GC了6次都是闲置时,就会将这个Block返还给系统。条件很难触发。

Unity2019.1版本引入Incremental GC,在使用Boehm垃圾回收机制的基础上以增量模式运行,将垃圾收集拆分到多帧进行。解决了GC峰值问题。

内存管理

Unity内存管理主要注重两大内存:原生内存和托管堆内存

原生内存
图片

图片是大多数游戏内存开销最大的一块。

咱们秉承两个原则:

一是能压缩即压缩,压缩过的图片虽然美术效果会没有那么好,但在内存上可以剩下很多。RGBA的格式的图越少越好。减少图片的色差范围,可以让图片在压缩过后,效果表现得没那么差。

二是能拆分即拆分,一张完整的背景图大小一定会大于所有背景内容拆分下来拼接的大小。

除此之外,Unity中图片的一些细节可以注意一下。

对于Read/Write选项,如果不需要进行像素基本修改的话,最好不要开启,因为在内存中会多一份图片的拷贝。

至于Mipmap功能,一般来说,对于UI图片是不需要的。开启的话会生成多张1/2、1/4、1/8…长的图片,用于减小渲染压力。整体则会增加1/21/2 + 1/41/4 + 1/8*1/8 + … 约1/3原图大小的内存占用。

在这里插入图片描述

音频

如果没有必要使用多声道,请一定要勾选强制转换为单声道的选项,这会使得音频大小减少很多。

如果对音效质量要求没有那么高,那么采样率可以适当降低一点,压缩格式可以选择压缩率高的选项。

Decompress On Load:整个音频文件加载到内存中后对其立即进行解压,最终内存占用为解压后的音频大小,即未压缩的音频大小。播放的时候没有延迟,但是一般来说,大音频文件由于未压缩版本占用内存过大,所以尽可能地减少对大音频文件应用此机制。

Compressed In Memory:整个音频文件加载到内存中后不立即进行解压,当音频播放的时候进行解压,最终内存占用的是压缩后的音频大小。但是相比上面一种机制来说,这种机制在播放时会有一丢丢延迟,解压速度不同,播放的延迟也就不同。所以说这种机制应用于大音频文件是比较合适的,既不会占用过多内存,又能接受解压带来的CPU开销和细微播放延迟。

Streaming:直读直放的模式。这种模式使用尽可能小的内存来缓存从磁盘中逐渐读取的数据,并且立刻解码播放音频,播放完后及时释放。但是这种模式的CPU开销是最高的,音频的播放延迟取决于频繁I/O的开销,所以说算是CPU换内存的一种方式。所以说,对内存压力比较大的项目,首选这种模式是不错的选择。除此之外,这种模式也比较适合大音频,或者只需要播放一次的音频。

还有一种情况,当玩家点开静音的时候,与其把音量大小变为0,不如采用合理的机制卸载音频资源。

AssetBundle
压缩格式:LZ4、LZMA、LZ4Runtime

LZMA是流压缩方式(stream-based)。流压缩再处理整个数据块时使用同一个字典,它提供了最大可能的压缩率,但是只支持顺序读取。所以加载AB包时,需要将整个包解压,会造成卡顿和额外内存占用。

LZ4是块压缩方式(chunk-based)。块压缩的数据被分为大小相同的块,并被分别压缩。如果需要实时解压随机读取,块压缩是比较好的选择。解压时可以一块一块解压,重复利用内存,减少内存峰值。

除此之外,Unity在每次运行第一次加载LZMA的ab包时,会把LZMA的ab包解压,再压缩成LZ4Runtime格式的ab包,存储在内存中。这也会导致内存增大非常多。

资源冗余:

同一张texture被不同的prefab引用,同时每个prefab又被打成不同的AB包,那么在没有针对texture进行依赖打包的前提下,该texture就会同时出现在两个的AB包中。

资源卸载:

及时卸载资源、AB包。一个优秀的资源管理策略非常关键。

在这里插入图片描述

场景

一个非常常见的原生内存增长的情况就是场景中GameObject过多。

对于像Scene、GameObject这类的实例,最终还是会在反映在原生内存中,而不是托管堆中,托管堆只是存放和维护了一个该Scene、GameObject的信息。

对这一部分来说,可行的方案有减少实例创建、活用对象池等。

托管堆内存

如果刚启动托管堆内存就达到了60MB,那么极有可能在一开始加载了一个非常大的配置表。

内存碎片

既然Unity的GC没有做内存压缩,那么内存碎片肯定是无法避免的。

如果我们能做到按照一定顺序分配内存,比如按照所需内存从大到小、内存释放时间从长到短等顺序,那么内存碎片会产生得更少。

但是在实际开发中肯定是很难做到这一点的,所以我们就要尽可能最大化利用内存,减少频繁的内存分配,也就减少了产生内存碎片的可能,从而减少了堆内存扩容的可能。

常用的策略:

  1. 减少new对象、类
  2. 减少装箱拆箱
  3. 减少闭包和匿名函数:所有的匿名函数和闭包在C#转IL时都会被new成一个匿名Class,里面所有函数、变量以及new的东西,都是要占内存的。
  4. 减少协程调用
  5. 减少Log输出
  6. 减少配置表加载
  7. 减少无效分配
内存泄露

托管内存可能不会下降,但并不应该一直上升。因为上面也提到了Unity的GC方式,GC只是把托管内存中分配的内存置为空闲状态,托管内存总量并不会减少。如果出现托管内存一直持续上升,大多情况下是出现了内存泄露。

如果重复打开关闭某个界面,或者停留在某个界面的时候,堆内存还在不断上涨,那这就妥妥的内存泄露了。

对于托管内存泄漏,一般都是代码中引用没有及时清除导致的问题。

举个简单的例子:

a = {}
b = {}

b.list = new List()
a.list = b.list

---------写了一堆代码之后----------

b = nil

上述类似的代码就很有可能出现在项目中。

b在一顿操作之后,需要清除置空,但是却忘记了a还引用着这个list。那么此时这个list就会成为内存泄露的一点。

一般来说,会出现在生命周期不对应、静态变量等情景下。

定位工具

Unity是无法检测到用户自行分配的原生内存的,比如Lua、cpp的插件等。

所以在Unity视角下的内存分析,更多的是注重Unity本身所管理的内存。

Profiler

下面是Profiler内存部分数据的概况界面:

在这里插入图片描述

第一行是已使用内存,第二行是已分配内存。里面具体内容如下:

Unity:所有Unity分配的内存,包括托管堆。(不包括后面的GfxDriver、FMOD、Video和Profiler)

Mono:托管堆。

GfxDriver:GPU显存占用,主要包括Texture、Vertex Buffer和Index Buffer。(后两者是在渲染管线流程中CPU传给GPU用来存储网格点信息的数据)

FMOD:音频引擎占用内存。

Video:视频播放器占用内存。

Profiler:Profiler本身占用内存。

注意:这里的Reserved
Memory不是完全精确的数值。因为Unity视角下的工具都只看到由Unity代码完成的分配,看不到第三方native插件和操作系统的分配。

下面是Profiler内存部分数据的详细界面:

在这里插入图片描述

它展示了虚拟内存的详细分配情况。

Assets:当前从场景、Resources和AB包中加载的总资源。一般来说,这里可以看到很多内存问题,比如出现没有引用的资源往往代表着资源泄露。

Built-in Resources:Unity Editor资源或者Unity默认自带资源。

Not Saved:项目中通过代码生成的各种资源。

Scene Memory:场景中的GameObject和它附属的Component。

Other:其他不在上面几条分类中的。

Memory Profiler Extension

下面为Memory Profiler Extension的某截图:

在这里插入图片描述

上图中可以看到:

Reserved Memory:256KB + 256KB + 128KB = 640KB

Used Memory:88562B = 86.48KB

已分配内存有640KB,但实际使用内存只有86.48KB。

其他

LuaProfiler、MemoryProfiler、PrefDog、WeTest的性能分析工具、UWA的性能分析工具…

小结

总结来说,内存问题都是一点一点积累起来的。

要解决内存问题,首先是要所有开发者知道“怎样会产生内存问题”。有了意识,才能在后续的开发中注意细节。

再者,就是一定合理加载,合理释放。勿以善小而不为,勿以恶小而为之。

资料参考

https://learn.unity.com/tutorial/memory-management-in-unity?uv=2018.1#

https://zhuanlan.zhihu.com/p/362941227

https://blog.uwa4d.com/archives/optimzation_memory_1.html

https://zhuanlan.zhihu.com/p/370467923

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1473618.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SAP中分包后续调整应用实例二(调减)

之前己写过一篇介绍过分包后续调整功能MB04的基本应用。当时的场景是某个原材料由于各方面原因(比如没有维护到BOM中),在委外加工模式成品收货后,并没有消耗或少消耗,这时可以用该事务功能来补充消耗。在生产报工中的M…

redis八股

文章目录 数据类型字符串实现使用场景 List 列表实现使用场景 Hash 哈希实现使用场景 Set 集合实现使用场景 ZSet 有序集合实现使用场景 BitMap实现使用场景 Stream使用场景pubsub为什么不能作为消息队列 数据结构机制SDS 简单动态字符串压缩列表哈希表整数集合跳表quicklistli…

CleanMyMac2024永久免费mac电脑版本安装包下载

CleanMyMac 4 for Mac:细致入微的功能介绍,CleanMyMac 4 for Mac作为一款系统清理和优化工具,提供了丰富而细致的功能,旨在满足Mac用户在不同场景下的清理和优化需求。以下是对CleanMyMac 4功能的更加细化介绍: CleanM…

苍穹外卖 -- day10- Spring Task- 订单状态定时处理- WebSocket- 来单提醒- 客户催单

苍穹外卖-day10 功能实现:订单状态定时处理、来单提醒和客户催单 订单状态定时处理: 来单提醒: 客户催单: 1. Spring Task 1.1 介绍 Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代…

CVE-2021–27065漏洞分析及复现

接着昨天的说 CVE-2021–27065 CVE-2021–27065是⼀个任意⽂件写⼊漏洞,它需要登陆的管理员账号权限才能触发。而CVE-2021–26855正好可以为我们提供了管理员账号权限。 登录管理员账号后,进入:服务器——>虚拟目录——>OAB 编辑OAB配置,在外部链接中写⼊s…

蜣螂优化算法DBO求解不闭合SD-MTSP,可以修改旅行商个数及起点(提供MATLAB代码)

一、蜣螂优化算法(Dung beetle optimizer,DBO) 蜣螂优化算法(Dung beetle optimizer,DBO)由Jiankai Xue和Bo Shen于2022年提出,该算法主要受蜣螂的滚球、跳舞、觅食、偷窃和繁殖行为的启发所得…

【加密算法】AES对称加密算法简介

目录 前言 工作原理 SubBytes ShiftRows MixColumns AddRoundKey 应用场景 在Java中使用AES 加密和解密数据 注意事项和最佳实践 结论 前言 AES(Advanced Encryption Standard)是一种对称加密算法,它在密码学中被广泛应用。AES取代…

数学建模入门必看|关于2024第九届数维杯数学建模,你想知道的都在这里!

数维杯大学生数学建模挑战赛每年分为两场,每年上半年为数维杯国赛(5月,俗称小国赛),下半年为数维杯国际赛(11月),2023年第八届数维杯大学生数学建模挑战赛共有近1.4万名学生参赛,参赛队伍来自国…

【总第49篇】2.3深度学习开发任务实例(2)机器学习和深度学习的对比【大厂AI课学习笔记】

机器学习和深度学习都是用于图片分类任务的强大工具,但它们采用的方法和原理有所不同。下面我将分别解释这两种技术是如何应用于图片分类的,并着重讨论深度学习中的卷积概念。 机器学习在图片分类中的应用 传统的机器学习方法在进行图片分类时&#xf…

【论文阅读】Vison-Language Navigation 视觉语言导航(1)

ACL 2022 VLN视觉和语言导航:任务、方法和未来方向综述 多模态任务新蓝海:视觉语言导航最新进展 Leader board in VLN RXR: Room-across-Room (RxR) is a large-scale, multilingual dataset for Vision-and-Language Navigation (VLN) in…

北邮毕业论文Latex模板使用教程(Windows)

1latex模板下载 下载地址: https://github.com/rioxwang/BUPTGraduateThesis2安装编译环境 TEX Live 2014 或者CTEX 2.9.2.164,以及更高的版本. 下载其中一个即可 (1)TEX Live下载地址: https://tug.org/texlive/acq…

网络初识(概念入门)

目录 1.局域网VS广域网 1.1局域网 1.2广域网 2.五元组 2.1 IP和端口 2.1.1 IP 2.1.2端口号 2.2协议 3.协议分层 4. TCP/IP五层模型 5.封装和分用 5.1封装 5.2分用 1.局域网VS广域网 1.1局域网 简单介绍:指在某一特定区域内由多台计算机组成的互联网组…

GDB动态调试学习-2-【断点】

文章目录 在程序地址上打断点在程序入口处打断点获取程序入口地址 在命名空间设置断点命名空间给命名空间的函数下断电 在文件行号上打断点保存已经设置的断点设置临时断点设置条件断点command指令 忽略断点 在程序地址上打断点 当调试汇编程序,或者没有调试信息的…

docker创建mongodb数据库容器

介绍 本文将通过docker创建一个mongodb数据库容器 1. 拉取mongo镜像 docker pull mongo:3.63.6版本是一个稳定的版本,可以选择安装此版本。 2. 创建并启动主数据库 容器数据卷配置 /docker/mongodb/master/data # 数据库数据目录(宿主机&am…

vue + koa + 阿里云部署 + 宝塔:宝塔前后端部署

接上篇,我们已经完成了宝塔的基本配置,下面我们来看如何在宝塔中部署前后端 一、上传前后端代码文件 在www > wwwroot目录下创建了一个demo文件,用来存放前后端代码 进入demo中,点击上传 这里前端我用的打完包的 dist文件&am…

微信公众号关键词自动回复

今天主要给大家讲一下如何实现微信公众号关键词的自动回复功能,就如网站的文章而言,进行人机识别,需要关注公众号回复验证码获取到验证码从而展示文章内容,,具体效果如下图。 springboot 2.3.2RELEASE 1、微信公众平台…

消息中间件篇之Kafka-消息不丢失

一、 正常工作流程 生产者发送消息到kafka集群,然后由集群发送到消费者。 但是可能中途会出现消息的丢失。下面是解决方案。 二、 生产者发送消息到Brocker丢失 1. 设置异步发送 //同步发送RecordMetadata recordMetadata kafkaProducer.send(record).get();//异…

vue3 使用qrcodejs2-fix生成二维码并可下载保存

直接上代码 <el-button click‘setEwm’>打开弹框二维码</el-button><el-dialog v-model"centerDialogVisible" align-center ><div class"code"><div class"content" id"qrCodeUrl" ref"qrCodeUrl&q…

设计模式-结构型模式-组合模式

组合模式&#xff08;Composite Pattern&#xff09;&#xff1a;组合多个对象形成树形结构以表示具有“部分—整体”关系的层次结构。组合模式对单个对象&#xff08;即叶子对象&#xff09;和组合对象&#xff08;即容器对象&#xff09;的使用具有一致性&#xff0c;又可以称…

IT廉连看——Uniapp——页面样式与布局

IT廉连看——Uniapp——页面样式与布局 目标&#xff1a; 了解样式与布局的规范 熟记px和rpx的区别 全局样式与index样式的区别 一、查看uniapp框架简介——尺寸单位 px尺寸单位的使用是贯穿始终的。 [IT廉连看] 二、尺寸单位——实操效果 1、打开Hbuilder X并进入in…