首页 / 资讯中心 / 文章详情

UE4内存管理实战:深入理解垃圾回收与UObject引用追踪

UE4内存管理实战:深入理解垃圾回收与UObject引用追踪 ★ FEATURED ARTICLE
1. 开篇UE4内存管理绕不开的必修课但凡在UE4里做过中大型项目的开发者都会对内存管理这块有切肤之痛。UE4的垃圾回收机制不是我们平时理解的GC自动帮你清理一切它有一套基于UObject和反射系统的标记-清除Mark-Sweep方案。很多时候你发现自己内存只涨不降或者干脆崩溃在访问一个已经失效的UObject上根源往往就是对这套GC机制的理解出了偏差。这套机制能做什么简单说它能帮你自动回收那些已经不再被引用的UObject对象避免你手动delete带来的悬挂指针和野指针问题。它适合谁来学只要你在UE4里写代码——不管是蓝图还是C只要你用到UObject、AActor、UActorComponent你就必须弄明白这套东西怎么运转。否则写个多人联机项目或者做一个长期运行的大型世界场景内存问题会直接拖垮整个项目。这篇文章我会从底层原理讲到实操排查把我踩过的坑、调过的内存问题和验证过的排查思路都拿出来聊聊。文章不会停留在垃圾回收就是自动清理这样的表面解释我会尽量把引用追踪、Root Set、反射机制这些东西讲到能用得上为止。2. 先搞明白UObject在内存管理里扮演的角色2.1 普通C对象和UObject的差异很多刚转UE4开发的人都会问一个问题我用UE4写代码到底什么时候该用new、什么时候该用NewObject答案的核心就在内存管理的差异上。普通的C对象就是标准C意义上的堆内存分配你自己new出来的东西必须你自己delete没人会帮你兜底。UObject则完全不同你通过NewObject创建出来的对象包括AActor——Actor是通过SpawnActor创建是被UE4的GC系统追踪的。你不需要手动delete它们GC检测到这些对象不再有被追踪的引用了它会自动帮你清理掉内存。这里必须说明白GC追踪的不是所有C内存只追踪UObject派生的对象。你如果在UObject里塞了一个裸指针的TArrayFMyStruct*里面指向的是普通C堆对象那这部分内存GC根本管不着你必须自己负责释放。这是很多人一开始会犯迷糊的地方。GC系统的核心价值是解决了多人协作项目里谁负责释放这个对象的争论。在纯C项目里所有权不清晰会引发大量悬挂指针和二次释放问题。UE4的GC机制通过统一的引用追踪替代了手动所有权管理你只管引用它管回收。2.2 反射系统为什么是垃圾回收的地基UE4的GC不是像C#那种运行时扫描所有堆内存的传统GC它依赖UClass的反射信息来遍历对象的引用。每个UObject对象的属性布局里哪些字段是UPROPERTY、哪个UPROPERTY是指向另一个UObject的指针、哪个是TArray或TMap容器且容器里装的是UObject指针这些都是通过反射系统在运行时可以查询到的元数据。这套机制的流程是GC线程从根节点集合Root Set出发通过每个对象持有的UPROPERTY引用关系遍历整个对象图标记所有还能从根访问到的对象为存活。遍历结束后那些没有被标记的对象就进入待回收队列等待真正的清理。所以UPROPERTY的作用不只是给编辑器做序列化和细节面板展示。它就是GC追踪引用的核心标记。你在这个层面忽略了UPROPERTY就得面对各种离奇的内存问题。比如你写class AMyActor : public AActor { UPROPERTY() UObject* SomeRef; UObject* NoRefProp; // 危险GC不追踪这个引用 };NoRefProp这个裸指针GC根本不知道它指向谁。就算它指向的对象已经在GC时被判定为无引用并回收了你的NoRefProp里还是一个悬空地址下次访问就会崩溃。所以NoRefProp这类字段要么加UPROPERTY、要么改成TWeakObjectPtr、要么你在析构时手动置空。提示如果你在某个类里就是不想用UPROPERTY存UObject引用确定这个对象生命周期由其他机制保证——比如某个Manager单例持有它的强引用且永远不会被GC——那你必须极度谨慎因为一旦持有者被清理你就会踩悬挂指针。我见过太多这种因为少写一个UPROPERTY引发的不稳定崩溃。3. 垃圾回收的完整流程从Root Set到真正释放3.1 Root Set里都有谁GC启动时它不是从整个内存里所有对象开始扫描的而是从一个明确的入口集合——Root Set开始。被放入Root Set的对象被视为绝对存活它们持有的引用不会被回收。根集合一般包括引擎和模块的全局单例对象、核心引擎对象例如GEngine对象当前World的WorldContext、当前Level里的所有Actor所有添加到Root通过AddToRoot的对象某些系统的显式root对象所有挂起的UObject引用里的强引用对象你可以通过UObject::AddToRoot把对象手动加入根集合。加入根集合的对象只要不RemoveFromRootGC不会回收它。这个机制有两个典型用途一是防止长时间运行时的异步操作过程中对象被GC误回收二是实现全局单例——你不想让某个对象因为引用计数清空被清理。但是AddToRoot是最容易被滥用的接口。我见过有些团队把所有Manager对象都加到Root最后项目里几百个常驻对象其中一大半压根不需要全局常驻。这个操作一旦做了就没有GC帮你自动回收了必须手动RemoveFromRoot否则就是稳定内存泄漏。3.2 标记-清除的执行顺序UE4的GC执行分为几个阶段理解这些阶段才能在出问题时定位到是哪个环节出问题第一是初始化阶段。GC会汇总当前需要参与可达性分析的对象集合检查各种增量GC参数、收集所有需要遍历的UObject。第二是标记阶段。从Root Set出发遍历当前所有对象的引用标记所有可达对象。这里要注意为了减少卡顿UE4支持增量式GC把标记过程分帧执行。第三是收集阶段。对没有被标记的对象调用FinishDestroy进行真正的析构和内存释放。最后是清理阶段处理各种收尾工作包括Socket的关闭等。如果你在游戏运行中看到了明显的周期性卡顿很大概率是GC在一帧内做了大量标记和收集工作。需要排查是不是每帧创建了大量临时UObject、引用遍历范围过大或者当着GC运行的时候还产生了大量需要清理的对象。3.3 什么时候会触发GCGC不是定时器式每X秒执行一次它有好几种触发条件。最常见的是通过ForceGarbageCollection命令或GEngine-ForceGarbageCollection(true)来手动触发。另外在客户端加载、切换关卡、大量生成Actor之后UE4内部会自动触发GC。它还遵循一个增量GC的帧预算机制每帧最多分配多少时间来做GC。还有一个关键点UE4在PIEPlay In Editor模式下、打包后的游戏运行时GC触发策略不一样。编辑器里由于频繁加载资源、创建临时对象GC会频繁触发。这导致一个典型现象——某些内存问题在编辑器里复现不了打包后却现原形。不是代码逻辑变了而是GC触发时机和频率变了。4. 引用类型的正确用法别把GC当挡箭牌4.1 强引用UPROPERTY vs TWeakObjectPtrUPROPERTY(Transient)存的是强引用。只要这个强引用存在且持有者存活对象就不会被GC回收。TWeakObjectPtr是弱引用不参与GC追踪当指向的对象被GC回收后弱引用会自动置空变成nullptr访问前用IsValid()检查即可。选哪个核心原则其实很简单你想不想因为你的引用而延长对象的生命周期但实际项目里很多人的选择逻辑是我担心对象没了所以用强引用保命。这种思路会把大量本不需要长期存活的资源、Actor、组件都变成永久存活内存根本降不下来。正确的思路反过来除非你需要主动保活否则默认使用弱引用或者普通的不追踪引用并在使用前做有效性判断。我举个典型例子你保存一个当前锁定的敌人Actor引用用在UI显示和战斗判定里。这个敌人如果被打死并Destroy了你希望你的锁定引用自动变成无效。如果用的是强引用UPROPERTY()敌人被Destroy后并不会马上从内存消失因为你的引用还抱着它。你虽然不需要它了它却因为你的强引用被GC当作存活。结果你以为是释放了敌人内存里还留着它一整坨Mesh、Animation和组件。改用TWeakObjectPtrAEnemy之后敌人的存活只由Actor生命周期本身管理Destroy后就能正常走销毁流程。4.2 TSharedPtr和TWeakPtr何时上场TSharedPtr不是UObject体系的引用它管理的是普通C对象。TSharedPtr的引用计数不受GC影响它依赖标准C的强引用/弱引用计数来确定生命周期。TSharedRef是TSharedPtr的非空版本适用于明确不会为空的场合。有些开发者会误用TSharedPtr去管理UObject这是错的。UObject不能也不应该用TSharedPtr管理因为它们的生命周期由两部分协作——引用计数和GC。对UObject用TSharedPtr结果往往是引用计数归零了对象被手动删除但GC系统还认为这个UObject有效导致挂起状态混乱。正确场景是你创建了一个纯C的数据结构它内部持有一堆动态分配的子对象且这部分生命周期需要被自动管理。比如OnlineSession信息、自定义规则引擎里的节点等这些不是UObject用TSharedPtr管理合理高效。而UObject之间的引用要么用UPROPERTY强引用要么用TWeakObjectPtr弱引用要么用AddReferencedObjects来让GC追踪一组动态引用。4.3 AddReferencedObjects的适用场景如果你的UObject里持有的是动态生成的UObject引用数组而且数量不确定或者你不想挨个写UPROPERTY比如因为运行时动态维护的列表可以用AddReferencedObjects静态方法把引用告诉GCvoid AMyManager::AddReferencedObjects(UObject* InThis, FReferenceCollector Collector) { AMyManager* This CastCheckedAMyManager(InThis); Collector.AddReferencedObjects(This-Pools); Super::AddReferencedObjects(InThis, Collector); }这里Pools是一个TArrayTObjectPtrUPoolObject。通过这个方法GC在遍历引用时会把这些动态数组里的对象当作引用处理。如果不写这个GC根本不知道Pools里装的UObject需要被保活可能在你还用着的时候就把它们回收了。AddReferencedObjects要谨慎使用写错容易导致GC看不到引用而提前回收更常见的问题是干脆不写、依赖UPROPERTY。对于动态增删的引用集合UPROPERTY完全可以追踪动态数组。比如UPROPERTY() TArrayUObject* DynamicRefs增删都会反映到GC追踪上。只有当你想更精细控制哪些引用应当被追踪时才用AddReferencedObjects。5. 内存分配的底层逻辑和常见优化技巧5.1 默认分配器与内存池的结构UE4的内存分配默认方式是使用FMalloc。FMalloc负责所有UE内部的动态内存分配和释放。在游戏运行时默认引擎使用的是FMallocBinned——一种带桶结构的分配器。它把不同大小区间的内存块分配到不同的bucket里避免每次分配都走系统级malloc。这种池化的方式在大量创建和释放小对象时能显著减少系统调用和内存碎片。你在调用new UObject或NewObject时最终会走UE的分配器。大体积资源——比如贴图、Mesh数据——会走专门的资源加载和缓存系统不一定受GC直接管理。资源管理走的是FStreamableManager、AssetManager和各个资源缓存。一个常见的误区是我的UObject被GC回收了为什么内存没降下来很可能因为UObject内部引用了一个大的UAsset资源贴图、Mesh。UObject本身只有几十几百字节真正的显存内存是资源对象持有的。GC把这些资源引用断开后资源对象如果不再被任何地方引用下次GC会回收它们。但资源占用的GPU显存和源数据内存的释放有自己的延迟和策略。想立刻观察内存效果用obj list配合资源命令查看是更准确的途径。5.2 大量生成临时对象是内存不稳定的元凶多数内存问题的根源不是泄漏而是分配频率太高、GC还没来得及回收。你的游戏每帧生成成百上千的临时UObject下一帧就没用了但GC是分帧执行且有节流策略的。短时间内对象数量剧增会导致内存占用飙升、触发更多GC、影响帧率。典型示例是每帧用NewObject创建UI提示、特效Actor、临时数据包。解决办法不是想办法加速GC而是从源头减少生成。改用对象池把频繁消创建的对象放入池中复用只在池为空时新增。对象池不仅能降低内存分配频率还能避免GC压力这是一种我认为比调GC参数更根本的优化方向。用Actor池要注意一点Actor被Spawn和Destroy本身就有开销适当做法是Spawn后就保持在Level中休眠用了就激活不用就SetActorHiddenInGame(true)并停用Tick而不是反复Spawn/Destroy。5.3 编辑器与打包环境内存差异UE4编辑器因为加载了各种编辑器模块、Undo系统、资源浏览缓存、编译工具内存占用比打包后的游戏高很多。很多人拿编辑器里的内存占用去判断打包后内存是否达标这完全不科学。编辑器里一个Level可能有几十个编辑器特有对象而对GamePlay本身无意义。在内存排查时请以打包后的Development Build或Shipping Build为主参考在编辑器里用stat memory看趋势即可。另外一个血液教训编辑器里对象复用和缓存的策略和打包后不同某些在编辑器里GC正常回收的对象打包后整个生命周期都不会被清理原因就是某些引用在打包后变成了强引用路径。6. 核心工具和命令定位问题的第一板斧6.1 obj命令家族从对象视角查内存UE4编辑器控制台和Output Log里可以用这些命令查对象obj list列出当前所有UObject的实例数量、类名和每个类的总内存占用。先按类排序看到哪个类的实例数量异常膨胀——比如某个Actor类有几千个实例就是排查方向。obj list classAActor专门列出Actor实例。obj list -alphalist按字母排序展示全部类。obj count列出类数量统计用于对比两段场景前后的对象数量变化。obj refs name某对象查看该对象被谁引用非常有用——它能帮你定位这个对象为什么没有被回收。进一步用obj dependencies查看某对象引用了哪些资源obj refine细分筛选条件。使用obj list时数据是快照式的。你可以在一个游戏场景中执行做某些操作后再执行对比数量差异。但要注意GC没触发时无用的对象也还在列表里所以每次对比前先手动ForceGarbageCollection让快照更可靠。6.2 Memory Profiler和内存分析策略UE4自带的内存分析工具是Memory Insights旧版叫Memory Profiler。可以通过trace.Memory开启内存追踪并生成分析文件。分析文件能在Trace窗口里逐帧查看内存分配和释放、对象和资源内存占用曲线。它最大的亮点是能看到分配量和释放量的差值随着时间推移持续走高就是内存泄漏的方向。结合stat memory和stat unit能快速定位是引擎内存、资源缓存还是UObject内存占了大头。实操中我的排查思路分四步第一步判断是持续增长还是仅在高负载场景下升高。如果场景保持不变而内存持续上升优先怀疑泄漏。如果是特定场景升得很高则是资源或对象整体量过大。第二步按类统计对象数量发现异常膨胀的类。观察这类对象是在某个操作后不断产生还是GC后数量不减。第三步对膨胀的类逐一obj refs看谁在引用它们。如果引用者是某个全局Manager且用强引用持有全部实例多半是业务忘了清理。第四步检查这些对象的资源占用。如果对象只有KB级别但引用了几十MB的贴图或网格清理对象本身解决不了内存问题需要看资源加载和缓存策略。6.3 Console命令速查表实际排查过程中这些命令我用得最频繁gc或ForceGarbageCollection立即触发一次GC。obj list查看对象列表。obj list class类名只看指定类的对象。obj count类计数统计。obj refs name对象路径查看对象被谁引用。obj dependencies name对象路径查看对象依赖的资源。stat memory引擎内存实时统计。stat memory -LLM启用LLM分类追踪。FreeMemory强制释放缓存内存。RHI.DumpMemory查看RHI和GPU内存信息。TexturePool命令查看纹理池的使用情况。MehList列出所有加载的资源旧版命令行。Memory或MemoryTools相关命令更高阶的分配记录。7. 常见内存问题的深度调优与排查实录7.1 症状一PIE模式下频繁切换关卡后内存持续上升这是个发生在我之前接手项目里的真实场景。开发环境是PIE模式每次主动切换Level后Editor内存慢慢上涨。一开始以为是编辑器缓存但切了十几次以后内存再也降不回来研究了一会儿后发现是UMySaveGameSubsystem里每次加载关卡都会NewObject一个SaveGame数据对象并保存在TMap里而TMap没有用UPROPERTY标记。SaveGame对象本身不是UObject的Actor虽然也是UObject但因为TMap没有UPROPERTYGC在遍历引用时根本看不到这些NewObject出来的对象。加载一个新的Level后旧的SaveGame对象早就应该被释放了但每次新关卡又会创建新对象旧对象留在内存里形成不可达对象GC却因为引用未标记无法判断它们已经无用。重点是这个系统死循环了——每次切关卡都会创建每个旧对象都被看起来还在被引用但实际上永远不会再用。解决方法很简单给这个TMap加上UPROPERTY()。遇到这种情况排查方式就是先obj list classUMySaveGameData发现每次切图都增加几个实例且GC后数量不减。然后obj refs查看引用者发现被那个子系统引用着但子系统本身存活没问题却能确认就是因为字段未标记导致GC无法判断哪些该清理。7.2 症状二打包后的游戏运行几小时后内存飙高打包环境下出现这个问题相对难排查因为不能用编辑器控制台。我惯用做法是给项目加一个内部调试HUD内置命令可以查看当前内存统计和对象数量或者开启远程控制台从另一个设备连进游戏查看统计。定位到的原因往往是高频生成并Destroy特效Actor且这些特效Actor的Niagara系统绑定了一个引用目标Actor的强引用让GC每帧都得扫描特效的引用。这个场景的典型模式是敌人被击杀时播放一个特效特效类里有两个强引用——一个是目标敌人的AActor*一个是武器Mesh的UStaticMeshComponent*。当敌人被销毁后特效Actor也播放完了要销毁但GC还不能立刻处理它们因为它们两方互为引用、互相保活。这类互相引用的问题最简单方案是在特效结束时手动把强引用清空再Unbind。除此之外Niagara系统本身也会持有一些动态生成的UObject如果特效逻辑里有生成子对象而不及时回收就会产生成堆的残留DataInterface对象。常规做法是给特效Actor写一个Cleanup函数在Finish之前置空所有外部引用。7.3 症状三高度物量游戏卡顿每次GC都掉帧这是一个最古老也最常见的UE4问题GC导致卡顿。很多人留言说每次GC都会掉30毫秒这肯定不是个别案例。引起卡顿的原因大体是两种对象数量太多标记阶段遍历所有引用的耗时本来就高每一帧的GC时间预算设置不合理加上存量垃圾太多。我处理这个问题的思路第一步确认对象总量。obj count如果超过数万几乎必然GC掉帧。第二步找出对象量最大的类是谁在创建大量对象。第三步从源头优化——使用对象池、减少Non-GC对象分配、缓存不重复创建。第四步调整GC参数包括运行gc.TimeBetweenPurgingPendingKillObjects、gc.MaxObjectsNotConsideredByGC等。第五步如果项目允许对某些完全不参与GC的对象继承类里改写IsDestructionSignificant或ShouldBeConsideredByGC来减少GC扫描范围——但这要非常谨慎改错了会引发对象提前回收的崩溃或泄漏。我的实际经验是GC参数调优只能缓解不能根治。真正有效率的是把每帧几十个NewObject改成复用池里的对象把几百个无引用对象改成零增长的静态数据结构。7.4 症状四EditCondition作用下的状态残留另一个较为隐蔽的问题用条件编译宏或编辑器工具生成的动态对象经构造函数创建但未标记UPROPERTY比如AMyActor::AMyActor() { UMyRuntimeData* Data NewObjectUMyRuntimeData(this); DataRef Data; // DataRef恰好有UPROPERTY }看起来DataRef有UPROPERTY没问题。但有一种情况是构造时的NewObjectUMyRuntimeData(this)其Outer是当前Actor而这个Actor在GC流程里被清除时Outer关系会让GC理解这个子对象应该一并被清理。实际项目中经常碰到的却是Outer传了GetTransientPackage()或干脆没传正确导致子对象逃逸GC范围。这就是Outer和引用追踪的双重作用。UE4里UObject是有Outer的GC不仅通过属性引用追踪可达性还会处理Outer关系。一个对象是否存活既看是否被强引用引用着也看它的Outer链是否存活。如果你NewObject时不指定Outer它可能会被挂在临时包或根包下这类对象没有自然的生命周期管理容易造成长期残留。实操心得NewObject时Outer一定要传正确的持有者。这个持有者就是谁创建了它、谁发现自己没被清理时应该带着它一起清的那个对象。多数情况下传this即可传错Outer会在GC里产生隐蔽的泄漏或者提前清理。我在调过不止一个项目里发现的XX组件内存泄漏最后定位全是NewObject的Outer传了空。7.5 症状五资源缓存过高显存或内存被贴图Mesh塞满有些项目的内存是被资源撑爆的不是UObject对象量。表现是stat memory里Texture内存和StaticMesh内存占据八成以上场景来回切换后资源不释放。排查思路是查看AssetManager的Bundle引用或obj refs查看某个Level引用了哪些资源。另一个常用手段是FreeMemory命令脱机测试缓存会不会自己降下来。很多团队会踩一个坑一个贴图资源被一个不常用但在Root集合里长期保持的Actor引用导致它永远不卸载。即使场景切换了那个Actor还在PlayerController身边因为PlayerController在Root集合里全程存活。这就是你以为卸载了资源实际因为一个强引用挂在全局对象上内存永远不释放。方法很简单在Actor被Destroy或场景切换时检查有没有需要清空的资源引用。如果是一些可重置的贴图或Mesh引用关掉场景时手动置空、或者改用FSoftObjectPath延迟加载。这样既能保留路径又不持有硬引用资源可以被正常卸载。8. 实操向如何设计一套稳健的引用管理策略8.1 将引用生命周期与业务生命周期绑定不是所有UObject都应该由GC来决定生命周期有些对象的生命周期本来就应该跟着业务逻辑走。比如一个战斗管理器它在某一个战场里创建了若干战斗Actor这些Actor的战斗逻辑做完就销毁。在这个情况里比起依赖GC的引用追踪显式地管理战场结束时的销毁逻辑更可靠。我通常推荐的模式是服务类、管理器类使用普通C对象非UObject或纯逻辑类它们内部持有的业务数据尽量不直接用UObject强引用。它们需要调用UObject时通过外部的Actor或Component传入使用后立即断引用。不要在管理器内部长期持有大量强引用。另一种模式是借助FTickFunction或用BeginDestroy在对象销毁时做清理。在AActor的EndPlay、UActorComponent的BeginDestroy里把引用的外部资源显式置空这样GC可以在后续扫描时快速判断对象已经无引用立即回收。8.2 显式生命周期优于隐式GC我比较推崇的一条原则能用显式生命周期控制的就不要完全依赖GC。比如对象池池本身管理了对象的存活和复用它内部持有的引用不是我为了保活而持有而是我的池确实需要这些对象存在。这时如果你用UPROPERTY强引用池内的对象一直存活是合理的但池外的引用应该都用弱引用。对UI数据、技能系统、AI行为树等尽量让对象的持有关系就是依赖关系。当你确认一个对象销毁后没有东西还需要引用它时直接通过MarkAsGarbage或销毁。如果不确定才使用强引用。新手最容易犯的错是害怕对象被GC回收所以到处用强引用保活结果GC根本没法工作。8.3 数据驱动的资源引用方式很多UI和Gameplay配置里大量使用了TAssetPtr或者FSoftObjectPath来延迟加载资源比如UI图标、可变的模型资源。在性能敏感的地方你也可以直接硬引用资源但必须确保这些硬引用不会因错误的所有权导致资源常驻。关于资源加载一个常见的设计是使用FStreamableManager管理加载在对象销毁时调用ReleaseHandle释放加载的handle。如果不释放handle资源会一直保持在已加载状态。这个操作相比GC更直接因为GC只处理UObject而资源加载系统有自己的缓存和引用计数。8.4 建议的代码规范经过多轮踩坑我总结出几条比较实用的规范第一所有存放UObject引用的成员变量都写成UPROPERTY除了TWeakObjectPtr。这让GC能完整追踪你的对象图。第二周期性创建和销毁的对象优先考虑池化。池化不单是性能优化也是内存稳定策略。第三在对象被销毁时主动清掉对外部对象的强引用。第四避免在UObject构造和析构里做重量级操作GC回收大批对象的时候这些析构函数的开销会叠加成卡顿。第五调试代码里常开几个obj list控制台命令把对象数量的快照日志打出来。这样出现问题时的历史和实时数据都有迹可循。第六新增任何涉及UObject的操作先过一遍GC会怎么看待这个引用这个问题。9. 一个完整的调优案例复盘我拿一个实际优化项目来做复盘好久之前某个开放世界项目加载大地图时内存峰值居高不下频繁进出关卡后内存继续走高。第一步用stat memory实时看发现Texture内存占比极高且切换Level后下降缓慢。第二步用obj list classUTexture2D确认有几个纹理资源变成了常驻资源。第三步用obj refs name某大型纹理追溯引用者发现某个UI框架的全局对象持有整个库的贴图引用。这个问题根因是UI框架的通用资源表里用了强引用去配置图标贴图UI框架全局单例在Root集合导致所有图标贴图常驻。解法是把图标资源表改成FSoftObjectPath并在显示图标时异步加载。这样UI框架不再强制持有贴图GC可以从纹理对象上解除引用切出地图后纹理自然卸载。调整之后切换关卡的峰值内存降低了投标相当大的数值常驻内存稳定在一个区间不再随关卡切换累积上升。另外一次优化是数百回合的战斗玩法发现每次战斗创建了大量战斗数值计算对象非UObject的普通C对象因为没释放内存缓慢上涨。排查时用stat memory -LLM看非引擎直属内存部分在持续上升。定位后给这些计算对象用TSharedPtr管理战斗结束直接Reset内存回归迅猛。这类问题的核心就是大量高频创建的普通堆对象没有统一管理内存碎片和峰值随之上升。用TSharedPtr统一管理后配合内存池稳定很多。10. 最后聊点实际的体会GC这个东西你越把它当万能它越容易给你找麻烦。它真正擅长的是在复杂的对象关系网络里帮你解除显式所有权的负担。但代价不是没有——它需要你严格遵守引用追踪规则、你需要理解Outer关系、你需要控制对象总量、你还需要处理资源和UObject之间的生命周期差。我踩过最多的坑基本都源于同一个心态以为GC会自动清理就不用思考内存生命周期。真相恰恰相反正因为有了GC你必须要比在纯C时更清楚地知道哪个引用被追踪了、哪个没有、这个引用是不是真的应该在这里。GC只是把内存安全问题从手动delete转移到了引用设计是否合理上。所以每次别人问我UE4内存优化第一件事该做什么我的回答始终是先看代码里有多少对UObject的裸指针、有多少没标注UPROPERTY的成员、有多少只管NewObject不管置空的逻辑。把这些基础问题扫干净一半以上的内存问题自然消失。剩下的一半再用对象池、引用调整和配置调优去慢慢磨。这个过程没有捷径但每解决一个问题你对这套系统的理解都会深一层。
阅读完成 · 觉得有帮助?
咨询建站