技术博客

UObject 调试:obj list / obj refs 命令实战

第 19 / 100 篇 · 2025-09-16 · ue5 · 调试obj命令内存泄漏实战

UE 的内存问题分两类:该死不死(泄漏)和不该死却死了(野引用)。两类问题的排查工具是同一套——obj 系列控制台命令。这些命令直接操作 GUObjectArray 和 GC 的引用图,是定位 UObject 问题的第一手段。下面基于 UE5.3 把常用命令和实战套路整理出来。

obj list:看对象分布

obj list 按类统计对象数量和内存占用,输出到日志。常用变体:

obj list                          # 所有类,按数量排序
obj list class=AActor             # 只看 AActor 及其子类
obj list -alphasort               # 按类名字母序(方便对比两次快照)
obj list -mincount=100            # 只显示数量超过 100 的类

输出格式:

Class                           Count    MemoryKB
AMyEnemyCharacter               1234     5678
UMyWidgetComponent              5678     2345
...

排查泄漏的第一步是对比快照:在游戏运行到状态 A 时 obj list -alphasort > A.log,运行到状态 B 时再 > B.log,diff 两份日志找数量异常增长的类。比如发现 AMyProjectile 从 50 涨到 5000,说明发射物没回收。

obj refs:追踪引用链

找到泄漏的类之后,用 obj refs 查谁还引用着它:

obj refs name=AMyProjectile_123          # 查指定对象的所有引用
obj refs name=AMyProjectile_123 shortest # 只显示到 root 的最短路径

输出是一棵引用树,从目标对象向上追溯到 root:

AMyProjectile_123
  <- UMyGameMode::ActiveProjectiles[47]
    <- UMyGameMode (root)

这条链说明:Projectile 被 GameMode 的 ActiveProjectiles 数组持有,而 GameMode 是 root 对象——所以 Projectile 永远不会被 GC。修复就是在 Projectile 失效时从数组移除。

shortest 参数特别有用:引用链可能有很多条,shortest 只显示到 root 的最短一条,通常是"罪魁祸首"。

obj gc:强制 GC 并看结果

obj gc 立即触发一轮完整 GC(忽略帧预算),输出回收统计:

obj gc                # 触发 GC
obj gc incremental=0  # 强制非增量(一次性全量标记)

配合 obj list 用:obj list 记录快照 → obj gc → 再 obj list。如果 GC 后某类对象数量没降,说明有引用链连着它——转 obj refs 排查。

obj dump:查看对象属性

obj dump name=X 把对象的每个 UPROPERTY 当前值打出来,走 FProperty 链逐个读:

obj dump name=AMyActor_123

输出类似:

Health: 100
MaxHealth: 150
bIsActive: true
TargetEnemy: AMyEnemy_456
...

这个命令的价值在于不依赖 C++ 反射之外的任何信息——引擎纯靠 FProperty 的偏移量读内存,所以即使对象处于异常状态(比如 BeginDestroy 之后),只要内存没释放就能 dump 出来。

实战案例 1:UI 泄漏

症状:每次打开关闭背包界面,内存涨 2MB。

排查:

  1. obj list class=UUserWidget 发现 UBagWidget 数量只增不减。
  2. obj refs name=UBagWidget_47 shortest 显示被 UMyHUD::CachedWidgets 持有。
  3. 查代码发现关闭时只调了 RemoveFromParent,没从 CachedWidgets 数组移除。

修复:关闭时同步清理缓存数组。

实战案例 2:野指针崩溃

症状:随机崩溃,调用栈显示访问 AMyEnemy 的 Health 属性时违例。

排查:

  1. 崩溃日志里对象地址是 0xdeadbeef 模式(UE 的已释放内存填充值)。
  2. obj list class=AMyEnemy 确认该对象已被 GC。
  3. 查引用方代码,发现用裸 AMyEnemy* 缓存了敌人指针,没加 UPROPERTY——GC 不知道这个引用,敌人死亡后被回收。

修复:缓存改用 TWeakObjectPtr<AMyEnemy>,或加 UPROPERTY 让 GC 感知。

辅助命令

  • memreport -full:生成完整内存报告,包含 UObject 统计、按 LLM 标签的内存分布。
  • stat memory:实时显示内存总量和各子系统占用。
  • stat uobjects:显示 UObject 数量、GC 时间。
  • gccollect:等价于 obj gc 的简写。

实战结论

  • 泄漏排查三板斧:obj list 对比快照 → obj refs shortest 找引用链 → 修代码断链。
  • 野引用排查优先查裸指针缓存——UPROPERTY 漏标是最常见根因。
  • obj gc 之后数量不降 = 有强引用;降了但慢 = 正常,GC 是增量的。
  • 编辑器下 obj 命令的输出量比 standalone 大很多(编辑器对象多),建议在 standalone 或打包版本里排查。

UObject 泄漏排查流程 obj list 快照 运行到状态 B obj list 再快照 diff 找异常类 obj refs shortest 定位引用链 → 修代码 引用链示例:AMyProjectile ← UMyGameMode::ActiveProjectiles[47] ← UMyGameMode (root) shortest 参数只显示到 root 的最短路径,通常是罪魁祸首。 obj gc 后数量不降 = 有强引用;降了 = 正常回收。