内存管理和垃圾收集-02: 操作系统如何管理内存?
版权声明
- 本文为"优梦创客"原创文章,您可以自由转载,但必须加入完整的版权声明
- 文章内容不得删减、修改、演绎
- 为方便大家阅读,文字内容经过精简,完整的技术分享请参考文末视频
内存管理的3个核心问题
关于内存管理有3个核心要点问题,今天我们分享第二点:
- CPU如何访问
- 操作系统如何管理
- Unity如何使用内存
通过本次的分享,希望大家能够对操作系统内存管理有一个更深入的理解!
内存基础与移动端特性
内存从原理来说是分成物理内存和虚拟内存的。
首先我在这里想讲明白一个概念:大家要知道我们的CPU只能访问物理内存,访问不到虚拟内存。这个我等一下再说。
但是有几个知识点跟大家传统认知不一样的地方,这是大家在大学里学《操作系统》时可能没接触过的:
移动端设备有三大特性:
- 没有独立显卡,使用的是集成在SoC芯片上的显示芯片
- 没有独立显存,而是与内存共享内存芯片(从内存芯片里分配一块用于显示,称为on-chip memory)
- CPU缓存层级更少(桌面级通常是三级缓存,移动端可能只有二级或一级缓存)
考虑到移动设备的特殊性,它的CPU缓存大小也更小,这对我们开发会产生一些制约。
物理内存与虚拟内存原理
不管怎样,从传统操作系统角度来说,我们必须理解物理内存和虚拟内存的区别。物理内存其实很好理解,就是物理的内存条。但内存条的存储空间有限。
不知道你们平时用电脑时是什么情况?像我现在的电脑经常使用内存会达到32GB。有时用AI训练模型时,甚至会用到128GB内存(当然这需要显存配合)。假设我们的物理内存条只有16GB,如果操作系统只能使用物理内存,那多出来的内存是从哪来的呢?
物理内存条与虚拟内存硬盘交换示意图
虚拟内存工作机制
答案就是这些内存来自虚拟内存。操作系统为了让用户使用更多内存空间,允许用其他设备模拟内存空间。在PC上通常是硬盘。
例如只有8GB物理内存,但系统使用了32GB内存时,不常用的内存(如后台程序)会被交换到硬盘上,称为内存交换。所以虚拟内存的作用是作为物理内存的后备。
程序能看到的只有虚拟内存:
- 32位系统最大看到4GB内存空间
- 64位系统能看到2的64次方内存空间(非常大的空间)
但CPU实际能使用的空间只有物理内存大小。当程序访问内存地址时(都是虚拟地址),CPU通过内存映射表将虚拟地址转换为物理地址才能访问。
缺页中断处理流程
那么问题来了:当访问的内存不在物理内存中怎么办?这时会发生缺页问题,触发缺页中断(页面错误)。
CPU缺页中断与硬盘数据加载示意图
处理流程是:
- 查询页表(Memory Map)
- 找到虚拟地址对应的磁盘位置
- 将页面数据加载到物理内存
- 重新建立内存映射
这个过程实际比较复杂,但核心原理就是这样。现代CPU都会使用虚拟内存地址访问方式,无论是否实际进行内存交换。
iOS与安卓内存管理差异
移动端还有个重要特性:不支持内存交换。但iOS和安卓有显著差异:
- iOS支持内存压缩(将内存分为Clean Memory和Dirty Memory)
- 安卓不支持内存压缩机制
Clean Memory与Dirty Memory压缩对比图
这就是为什么比较手机时,大内存安卓机可能不如小内存iPhone流畅的关键原因——这是操作系统管理机制造成的!
Unity底层架构解析
接下来我们看Unity如何使用内存。Unity引擎本身是由C++编写的,Unity引擎的架构是分层的:
- 首先,C#代码通过C#编译器编译成IL中间语言
- 然后,使用IL2CPP时转换为原生C++代码
- 最终,编译为平台相关机器指令(安卓ARM64/Windows x86/x64)
C# -> IL -> C++ -> 机器码的转换过程图示
这里需要认知两个重点:
- Unity底层代码都是C++,使用IL2CPP编译时我们的代码会转换成C++
- 即使不用IL2CPP,C#调用Unity API时实际也是调用底层C++函数(绑定层会帮我们做转换)
Unity内存管理机制
Unity内存管理主要分为两种类型:
- Native Memory:C++传统方式分配的内存,需要手动释放
- Managed Memory:C#使用的托管内存,由垃圾收集器自动管理
Unity托管堆与Native内存分配对比示意图
为什么自己写的C#代码不需要手动释放?——因为它使用托管内存管理机制。
Unity有自己的垃圾收集器(GC),它会定期回收不再使用的内存(类似垃圾回收车),这个机制我会放在下次分享详细讲解~
性能分析实践要点
性能分析时要注意两个关键点:
- 避免在编辑器环境测试:编辑器会预加载所有资源,内存分配方式与真机运行时完全不同!
- Native内存的特殊性:Unity Profiler可能监控不到第三方C++插件分配的内存
ity编辑器与移动端内存占用对比图表
Unity近年做了资源加载优化:
- 早期:启动时加载所有资源
- 现在:启动时只加载必要资源,进入场景时才加载场景相关资源
但最终性能测试还是要在打包后的真机环境进行。
另外注意,当使用第三方C++插件导致内存泄漏时,问题会很难排查(因为Profiler监控不到)
性能优化全流程
性能优化需要多维度进行,主要包括:
- 内存优化(Native和托管内存)
- 渲染优化(UI/模型/动画/场景)
- 网络优化
- 数据库优化
- 机器人压力测试
Unity性能优化维度
内存、渲染、UI、网络等优化方向脑图
优化方法论有两条路线:
- 后验定位:通过性能分析定位瓶颈(如使用Profiler)
- 先验规范:在开发框架层面制定规范预防问题
关于Mono和IL2CPP的选择:
- Mono仍在被使用
- 但IL2CPP优势明显:
-
- 性能更高
-
- 不受Mono更新限制
-
- 支持更广泛的跨平台
-
- 支持Hybrid CLR热更新
小结
今天分享的核心要点:
- CPU访问内存的流程(缓存->物理内存->虚拟内存)
- Unity内存两大类型(Native内存/托管内存)
- Profiler的监控局限性
- 性能优化的多维度方向
下次分享将深入讲解:
- Native内存构成与优化
- 托管内存管理机制
- GC性能瓶颈定位方法
在商业项目中做好资源管理特别关键(尤其是大型游戏的渲染资源)。
同时热更新框架的实现也需要结合性能考量,这些都是高级开发者需要掌握的技能。
参考:
- 完整视频版本请访:https://www.bilibili.com/video/BV1nYTNzDEkp
- 更多技术交流请加alice17173