深度内存探索🔰

上篇讲了对应的内存简单介绍后,我们对内存的基本表现有了一定了解,知道一些内存直观上的表现,本篇就针对上篇未提及的内容进行补充,并且其中会耦合一部分深度分析思路,希望能帮助到大家,另外该篇内容首次提交后后续会陆续提交一部分流程制图,方便大家理解,拖更挺久了,所以就暂时大家委屈一下,看看码的字,制图太麻烦了,望理解!~

展示方式

1、补充之前遗漏的内存上涨不一定是内存泄漏的栗子;
2、补充具体的内存泄漏问题如何定位及思路?
3、补充内存申请过程中会产生的问题以及影响;
4、补充内存不足时,硬件设备底层调度会怎样去做?
5、补充关于内存交换(swap)内容流程介绍;
6、补充透过显存去延伸性能问题;
7、拓展延伸其他内存表现 —— 缓存;

内存上涨不一定就是内存泄漏

上篇提到内存泄漏问题的定位,却少了对于内存上涨一定是内存泄漏的佐证了。接下来就来补充下,内存上涨还有可能是什么原因?内存泄漏的原因已经在上篇陈述了,忘记的回顾下。
内存上涨的正常表现,最直接就是缓存,当有一些 RPG 游戏,你在某个地图中活动就会把碰到的所有怪物,人物,等等一系列资源加载出来,内存最直接表现就是内存一直上涨,这种情况是内存泄漏么?不一定,当你更换了其他地图之后,之前加载的内存就正常卸载掉了,并且重新开始加载当前你这张地图的内容,一样会继续增加内存。这属于加载策略上的选择,是合理的,因此后续在测试过程中产生了对应的疑问之后你就可以跟开发聊聊,他们做了什么样的策略,避免产生误判。

内存泄漏定位思路

这边就直接拿 Unity 的内存泄漏定位去讲解了,比如说,当我们判断确定当前副本存在内存泄漏,那我们就开始分析是资源泄漏还是 mono 堆泄漏,亦或者是都泄漏,Unity 目前这块可选择的工具还是挺多的,不论是 UPR 还是 UWA 都有对于内存泄漏处理中有对应的 diff 处理,处理出来的内容中就含有对应卸载的资源信息与堆栈信息,这样的话,使用工具就成了我们最直接并且快速深度定位的有力手段。
同理,软件客户端性能测试一样,有现成工具使用的话就使用对应的工具去快速定位,比如常用的 leaks,由开发介入加个口子,会记录你对应的操作产生的泄漏,你只需要把对应的记录发送给开发即可,资源泄漏一般在软件侧很少见,不过也可以定位,还记得上篇描述的内存类型么?native pss 派上了用场,所以,dddd。嘿嘿
再同理,如果你知道了这些,你还是测开项的话,你就知道想了解这些内容的话,就找能获取对应数据信息的接口自己造轮子也不是不可。所以,加油,打工人。

内存申请/回收的破事

内存申请的内容也属于老生常谈的内容了,开发在编译脚本的时候,尤其是面向对象编程,大家听到的最多得就是 new 个对象,殊不知 new 对象这个创建变量的操作,底层编译时,是会去申请内存的,向谁申请?向内存的管理者去申请,上篇的内容图中也有提及过,申请内存时的表现。
依然以 Unity 项目举例,mono 是 Unity 用来管理行为数据的统一接口,所有执行行为都继承 mono,因此编写脚本时,每次 new 的时候都会向 mono 堆去申请内存,每次申请 mono 都会进行扩容,后续不使用时再把申请的回收回来。这样就能有个合理的循环。
mono 内存实际上也有内存泄漏的情况,而最骚的是 mono 的内存泄漏会导致内存碎片化,如:每次申请 10M 的内存,结果回收了 8M 回来,始终有 2M 漏在外面,但是堆管理又管不着,这样的话就会出现下次再用该堆去申请时,需要 10M 内存时,人堆管理只回收 8M 回来,而你又不够用,就会重新申请 10M 过来,结果循环往复,就会越来越过分,越申请越多,因为没做出图,可能大家不好理解,就是你借了 10 块,我还你 8 块,你想再借我 10 块的时,我没钱了,只能把你还我 8 块给你,你差钱,我就得重新给你搞个 10 块钱给你,场景是有点不合理,但是实际内存表现确实是这样的。
以上就是内存申请/回收的表现。
延伸:内存申请时,存在泄漏时,VSS 会不断上涨,感兴趣的可以留个作业大家去做一做,为啥会这样,后面评论区交流,公布答案。哈哈,内存回收时,常用的手段就是手动/被动触发 GC 操作,感兴趣的可以去了解下。

内存不足时,当前硬件设备底层都做了什么?

1、内存不足时,但是还有点点内存,硬件设备会按照系统设置优先级去杀掉对应的进程来释放内存,为当前执行项目进程腾空间;
2、内存不足用光时候,系统会直接杀掉当前项目进程,也就是所谓崩溃表现了;
这部分的内容还是有个图介绍最好了,先简单说一下结论后面做好图我一并更新上去并推送到群里,不好意思了,各位。

内存交换的意义

内存交换一直都属于一个非常有意思东西,又爱又恨,爱是因为它某种角度上来看保护了硬件设备,保护了当前正在执行的项目进程,恨是因为在平时的性能测试中真的很不好统计,计算方式比较麻烦。
内存交换主要是因为主内存中会有一部分内存单独抽出来用来存放长时间未使用或处于等待状态未执行的数据存放起来,可以理解为一个收纳盒,为真正用起来的数据腾出空间,让他们能有充分的空间。那为什么会叫它收纳盒,是因为你存放到这里面的数据是经过压缩的,这样你就相当于牺牲了部分时间去换取空间了,压缩,意味着 CPU 又忙了,这样的话,又可以看出来 swap 内存上涨的话,某种程度上又可能会引发卡顿问题了。
延伸:目前这种内存可以抓取得到,Perfdog 上也有体现,这样的话你观察这类数据也能侧面去分析对应场景内存。并且 swap 内存也是运行在 ram 上,因此计算当前真实的 PSS 时,是需要将当前 memory 与 swap memory 相加计算,只有这样才是当前项目运行时真实的内存表现。多嘴一句,Android 的低端机器保表现尤为明显。

渲染流程 —— 显存


从图中我们可以看到,显存担任的是一个中转站的角色,CPU 负责搬货,显存用来把货物存着让 GPU 从仓库中去拿并且拿来用,那又有人问了,那为啥不直接 CPU 处理完数据之后直接丢给 GPU 处理那不就更快了?理论上确实是,多了一步去掉不是更快么?这就涉及到硬件相关的内容了,因为 CPU 是处理复杂指令的处理器,本身计算速度就会非常快,但是 GPU 是处理简单指令的处理器,CPU 某种程度上来讲在进行复杂数据变更为便于 GPU 处理的简单数据指令效率会比 GPU 处理速度快很多,一旦出现这种情况,GPU 就会出现处理不过来的情况,成为瓶颈。这也是显存的意义了,用来做预缓存做数据预热,这样的话不论堆多少,让 GPU 有条不紊的从显存中拿数据处理就好,不影响 CPU 继续工作。
以上的内容也就延伸出了我们关注显存的一个点,如果显存上涨意味着什么?
显存上涨我们不难理解,CPU 处理的速度快,GPU 处理的速度慢,就会导致显存上涨,因为每帧要处理的数据太多,GPU 要花时间,这样的话,我们就知道 CPU 目前处理的每一帧的数据内容变更简单指令太多了,GPU 很花时间,有什么关系?跟进来的渲染数据有关,渲染数据开销过大又跟当前绘制帧的复杂度有关,顶点索引,纹理贴图等等,言而总之就是资源的精度高了。
所以,显存上涨某种程度上能反应要渲染的资源精度高了,需要压低资源精度,不论是顶点,面数,像素绘制等等。

缓存的意义以及表现

缓存属于典型的牺牲空间,换取时间,当为了减少其他硬件(CPU)访问压力/速度时,使用缓存去进行数据访问会有非常显著的执行效率提升。
常见的场景:
游戏场景中,比如:我们在进行首次某副本战斗时,消耗的时间会相对长一些,这是因为首次加载过程中会进行预加载的操作,而这个操作过程中是在 loading 过程中就进行了,为了挑战副本时能加速访问速度,按照策略可能并不会卸载当前已缓存内容,当进入副本战斗时就会带来非常流畅的游戏画面,不会说打副本时刷新野怪或者释放技能出现卡顿的情况,因为预加载阶段的时候就已经加载好了一切。
软件场景中,比如:现阶段比较火的直播专项,举个栗子就拿 dy 来讲,dy 每次刷新视频时,刷到第一个视频时会有比较长的加载时间,随后刷下一个视频的时候就会非常的流畅,几乎可以秒进,秒播放,也是得益于缓存机制的原因,通过一些分析工具,会看到对应的内存上涨了一大块,那是因为后台已经预加载了好几个视频的资源,因此访问速度会非常快,切换视频时只有切换逻辑在走,大大的减少 CPU 开销,还增加了使用体验。
延伸:缓存能一直放着不管么?怎么处理?
缓存不能一直放着不管,实际上不管是游戏还是软件,缓存最后是都会卸载掉的,具体什么时候卸载就是研发团队的策略了,游戏中比如在打完对应的副本之后就会卸载掉之前加载的内容,dy 也是在刷新到一定的程度后就会卸载掉对应已缓存的内容,不然的话,内存越来越高,最后终会因为内存告警出现性能问题,崩溃。缓存卸载操作都是可以通过程序去控制的,具体的卸载函数是什么就不举例了,不同项目是不同的。

总结

1、内存属于存储数据信息的具现化内容;
2、内存的存在加速了访问目标数据的速度,比如:缓存的存在;
3、内存中数据交换会延伸到执行效率的问题,会跟 CPU 的工作有强相关;
4、内存的作用与表现形式有很多,且有不同的意义;
实在是不好意思了,各位,因为最近工作比较忙,所以一直没时间去更新,每次到下班的时候,就简单写一写,实在是作图加写内容好麻烦,本次的帖子几乎就是纯文字了,大家就当是个预习了,后期图做好放上去方便大家理解。下面是讨论群,群里的小伙伴都很积极氛围蛮好,感兴趣的可以进群一起进步,一起成长!~😁


↙↙↙阅读原文可查看相关链接,并与作者交流