内存泄漏是一种难以捉摸的错误,往往要过很长时间才会显现出来。如果我们为某些对象分配了内存,然后停止了对它的追踪,也没有释放它使用的内存,就会发生这种错误。内存泄漏会促成整体不稳定,导致应用程序速度变慢,最终当它逐渐消耗掉所有可用 RAM 时,就会使程序崩溃。
虽然很难堵住每一个分配漏洞,但我们可以使用一些技巧来找到元凶,确保它们不再吞噬它们不需要的内存。最近,Paragon 团队就这样解决了一些可能使游戏在几个小时后崩溃的内存泄漏问题。虽然 Paragon 比赛很少会持续到耗尽系统的所有内存,但是很难预测比赛会持续多久。毕竟没有比客户端在关键的团战中崩溃更令人恼火的事了!
根据他们的经验,我要在这里介绍一些追查内存泄漏源头的技巧和诀窍。
用 MemReport 查找内存泄漏的证据
追查内存泄漏的第一步是确定泄漏是否正在发生。一个简单的办法就是在两个不同的时间点分别生成当前内存分配的快照,比较两者来发现变化。如果在游戏中没有进行任何操作,那么无论过去了多少时间,内存的使用情况应该都不会有很大变化。但是如果被占用的内存量随着时间推移稳定地上升,可能就需要查找泄漏了。
为了举例,我创建了一个 Actor,为它每个 Tick 分配 1,000 个整数,并且立即丢弃引用。我把我的 Actor 放到一个空场景中,让它运行 10 分钟,并且在我的会话开始和结束时分别生成 MemReport。比较两个报告后发现,虽然对象计数完全没有变化,内存使用量却显著增加。
可以在以前的一篇博客 Debugging and Optimizing Memory 中找到关于 MemReport 的更多信息。
MemReport 可以帮助区分内存泄漏和其他类型的泄漏(例如对象泄漏)。如果场景中的对象不断增加,就可能发生对象泄漏。通常这意味着产生的对象没有得到正确清理。例如,如果我们创建一个在与其他对象碰撞时会被销毁的抛射体类会怎样?射到空中的抛射体会向着天空永远飞翔,始终占用相当多的内存。MemReport 会显示在某一时刻全局中存在的每个对象的实例数,提供找到泄漏对象的线索,以便我们调查产生和消除逻辑是否有问题。
开放的分配
如果我们怀疑某些分配的内存从未得到释放,就需要追查这些分配是什么原因造成的。为了实现这个目的,Paragon 团队构建了一个工具,可以监视任何尚未得到释放的开放内存分配。这个工具仍然是试验性的,默认情况下会被禁用,因此您需要稍稍更改 MallocLeakDetection.h 并重新编译:
#define MALLOC_LEAKDETECTION 1
完成此操作后,确保正在运行 Visual Studio 并使其与您的游戏实例关联。可以在控制台中输入“MallocLeak Start”来开始记录,输入“MallocLeak Stop”停止记录。然后该工具会将记录期间进行的所有开放分配转储到 Visual Studio 的输出窗口中。您可以在记录器运行时输入“MallocLeak Dump N”,其中 N 是以字节为单位的大小。这样就可以立即对转储的开放分配进行过滤,仅限于至少 N 字节的分配。最好在游戏完成初始化之后开始记录,因为在初始化过程中会进行许多分配,它们直到游戏关闭时才会释放。
我修改了我的泄漏的 Actor,使它在游戏开始 10 秒之后进行一个大分配。然后我确认分配期间 MallocLeak 在运行,并使用“MallocLeak Dump 1000000”转储所有大于 1 兆字节的开放分配。不出所料,我在结果中发现了一些疑点。一个 Actor 的 Tick 函数分配了相当多的内存!
AllocSize: 12345678, Num: 1, FirstFrameEverAllocated: 1522
UE4Editor-Core.dll!FWindowsPlatformStackWalk::CaptureStackBackTrace()
[d:\release-4.11\engine\source\runtime\core\private\windows\windowsplatformstackwalk.cpp:233]
UE4Editor-Core.dll!FMallocLeakDetection::Malloc()
[d:\release-4.11\engine\source\runtime\core\private\hal\mallocleakdetection.cpp:180]
UE4Editor-Core.dll!FMallocLeakDetectionProxy::Malloc()
[d:\release-4.11\engine\source\runtime\core\private\hal\mallocleakdetection.h:116]
UE4Editor-MemoryLeak.dll
UE4Editor-Engine.dll!AActor::TickActor()
[d:\release-4.11\engine\source\runtime\engine\private\actor.cpp:807]
UE4Editor-Engine.dll!FActorTickFunction::ExecuteTick()
[d:\release-4.11\engine\source\runtime\engine\private\actor.cpp:111]
UE4Editor-Engine.dll!FTickFunctionTask::DoTask()
[d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:262]
UE4Editor-Engine.dll!TGraphTask<FTickFunctionTask>::ExecuteTask()
[d:\release-4.11\engine\source\runtime\core\public\async\taskgraphinterfaces.h:999]
UE4Editor-Core.dll!FNamedTaskThread::ProcessTasksNamedThread()
[d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:932]
UE4Editor-Core.dll!FNamedTaskThread::ProcessTasksUntilQuit()
[d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:679]
UE4Editor-Core.dll!FTaskGraphImplementation::WaitUntilTasksComplete()
[d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:1776]
UE4Editor-Engine.dll!FTickTaskSequencer::ReleaseTickGroup()
[d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:530]
UE4Editor-Engine.dll!FTickTaskManager::RunTickGroup()
[d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:1435]
UE4Editor-Engine.dll!UWorld::RunTickGroup()
[d:\release-4.11\engine\source\runtime\engine\private\leveltick.cpp:704]
UE4Editor-Engine.dll!UWorld::Tick()
[d:\release-4.11\engine\source\runtime\engine\private\leveltick.cpp:1197]
UE4Editor-UnrealEd.dll!UEditorEngine::Tick()
[d:\release-4.11\engine\source\editor\unrealed\private\editorengine.cpp:1346]
UE4Editor-UnrealEd.dll!UUnrealEdEngine::Tick()
[d:\release-4.11\engine\source\editor\unrealed\private\unrealedengine.cpp:368]
UE4Editor.exe!FEngineLoop::Tick()
[d:\release-4.11\engine\source\runtime\launch\private\launchengineloop.cpp:2772]
UE4Editor.exe!GuardedMain()
[d:\release-4.11\engine\source\runtime\launch\private\launch.cpp:148]
UE4Editor.exe!WinMain()
[d:\release-4.11\engine\source\runtime\launch\private\windows\launchwindows.cpp:189]
UE4Editor.exe!__scrt_common_main_seh()
[f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl:264]
kernel32.dll
ntdll.dll
和大多数分析技巧一样,以批判的眼光审视结果很重要。要记住,在您开始记录前进行的分配不会显示在结果中。而在您停止记录之后释放的分配会显示在结果中,尽管它们并不代表泄漏。但是,MallocLeak 是调查内存去向的好工具,能在您查找问题代码时提供一些线索。祝您追查愉快!