本原创文章帖发布在华为开发者联盟社区欢迎开发者前往访问评论交流更多与该内容相关讨论请点击原帖查看开发态快速定位ArkTS泄漏-华为开发者话题 | 华为开发者联盟1.1 ArkTS内存泄漏分析流程与高频泄漏场景1.1.1 术语介绍术语解释GC RootGC RootGarbage Collection Root垃圾回收引用链根节点 是垃圾回收器进行可达性分析的起点。从 GC Root 能访问到的节点对象是“存活的”否则会被回收。引用链在 ArkTS 中“引用链”就是从某个GC Root如VMRoot、FrameRoot等出发经过一连串的对象引用最终到达目标对象的路径。如果这个路径存在则该对象是“存活”的不会被 GC 回收如果从任何 GC Root 都无法找到一条到达该对象的引用链该对象就是“不可达”的会被标记为垃圾对象并回收。VMRootVMRoot是 ArkTS 虚拟机层面的根引用集合代表 GC 遍历的起点。FrameRootFrameRoot是函数调用栈帧在 GC 遍历过程中的根节点。当函数被调用时其局部变量和入参对象会被当前栈帧引用从而成为 GC 的“可达”起点。Local HandleLocal Handle本地句柄是一种短期引用用于在本地作用域如函数调用栈内持有对象防止对象被垃圾回收。当作用域结束时这些句柄会自动释放。Global HandleGlobal Handle全局句柄例如 napi_ref用于长期持有对象引用确保在对象生命周期内不会被垃圾回收机制回收。使用此类句柄时开发者需要手动管理其生命周期包括创建和销毁以避免潜在的内存泄漏问题。1.1.2 高频泄漏场景1.1.2.1 JS对象被VMRoot类型持有导致内存泄漏常见构成 VMRoot 引用的来源包括•模块导出对象export 出的对象被底层的 SourceTextModule 系统对象持有而模块本身在应用生命周期内不被卸载。•全局对象属性通过 globalThis.xxx ... 挂载的对象globalThis 是贯穿应用始终的根对象。•内置原型链扩展修改 Array.prototype、Object.prototype 等内置对象原型导致意外全局持有。一旦业务对象挂载到上述根节点上即使页面销毁、组件卸载该对象依然被 VMRoot 强引用GC 无法回收。1.1.2.2 JS对象被Local Handle/Global Handle引用导致内存泄漏在鸿蒙的JS-Native交互中JS对象可以通过NAPI Native代码访问或持有。Native代码为了引用这些JS对象会使用两种主要的句柄类型napi_value、napi_ref。其中napi_ref 是一种引用计数的句柄用于保持对JS对象的引用防止其在Native代码持有引用期间被垃圾回收器回收。• Local Handle (napi_value): 通常指在Native代码执行上下文中创建的、作用域较短的引用。当Native 代码执行环境切换时这些Local Handle通常会被自动清理。Local Handle受Handle Scope管理大部分场景下如同步调用、napi框架异步调用等系统会为创建的Local Handle添加Handle Scope但仍有部分场景如libuv异步调用等系统不会主动添加Handle Scope需要应用自行添加Handle Scope否则就会导致JS对象无法回收。•Global Handle (napi_ref): 这是一种作用域为整个应用生命周期的引用。一旦创建除非显式删除否则它会一直保持对JS对象的引用。通常用于需要跨模块、跨上下文甚至跨JS线程访问的JS对象。由于其持久性如果不加注意很容易成为JS对象泄漏的根源。当Native代码持有无论是local还是global一个JS对象时它实际上建立了一种强引用关系。在JS引擎的垃圾回收机制中如果一个JS对象的所有可达性引用包括JS代码内部的引用、DOM树等都被清除该对象就可以被回收。然而如果存在一个Native句柄指向它那么这个句柄就构成了一个阻止根使得该JS对象在Native代码持有该句柄期间从垃圾回收器的角度看它是“可达”的。这就阻止了JS对象被回收从而可能导致内存泄漏。1.1.2.3 JS对象被FrameRoot类型持有导致内存泄漏正常情况下函数执行完毕退栈后栈帧销毁这些临时引用自动失效内存随之释放。然而若函数长期不退栈局部变量/参数所引用的对象将持续被 FrameRoot 锚定即便业务逻辑已不再需要它们GC 也无法回收。常见导致栈帧滞留的情形包括• 死循环或无限递归函数永不返回。• 在函数内启动了一个长期运行的同步阻塞操作如同步网络请求、大文件同步读写。• 函数内部创建了闭包并被外部长期持有且闭包捕获了该函数栈帧中的变量导致整个栈帧无法释放。• 使用了生成器Generator或 async/await 但未正确消费导致协程挂起栈帧保留。1.1.3 标准化排查流程复现与观察使用DevEco Profiler的Realtime Monitor(Memory泳道)重复多次进出目标页面观察内存曲线是否呈阶梯状上升。识别泄漏点在操作前后各采集一次堆快照使用 Snapshot 对比功能关注新增对象数量和 Shallow Size从而识别泄漏点。查看对象引用链在Snapshot快照的对象引用链中找到异常存活对象如本该销毁的Component实例通过“Shortest Paths”分析引用链情况。1.是否VMRoot持有若引用链顶端为 VMRoot / SourceTextModule多为模块导出单例或全局变量未清理。2.是否Local/Global Handle持有若泄漏对象的Distance为1则多为Native侧持有未释放导致。可分为2种场景a. Local Handlenapi_value未使用napi_open_handle_scope 或未成对使用即使用napi_open_handle_scope但是未使用napi_close_handle_scope 释放b. Global Handlenapi_ref 未调用 napi_delete_reference 释放或未成对使用即使用napi_create_reference但是未使用napi_delete_reference 释放在这2种情况下无法基于引用链分析需要通过Allocation模板开启Local Handle及Global Handle录制选项来进一步分析。3.是否FrameRoot持有若非VMRoot和Local/Global Handle持有则多为函数不退栈或闭包捕获对象被外部持有。代码审查根据引用链中的关键节点如 EventHub、Timer、全局变量、napi_ref、闭包上下文定位代码中的持有关系。修复与验证修改代码后重复步骤 1~2确认内存曲线回归平稳。图11.2 ArkTS内存泄漏分析案例1.2.1 案例背景现象本案例中通过复现‘首页至消息页’的反复进出操作观察到应用内存占用呈现阶梯式持续增长趋势。在循环操作10次后应用出现显著卡顿现象。初步判断典型的“阶梯式内存增长”高度疑似内存泄漏。1.2.2 分析流程1.2.2.1 步骤1Memory泳道确认泄漏1. 打开 DevEco Studio连接真机点击Profiler工具RealtimeMonitor也可使用snapshot模板的Memory录制观察以下是使用RealtimeMonitor观察。2. 启动应用选择设备与应用进程。3. 点击录制按钮在设备上重复操作进入消息页面→停留2秒→返回首页重复多次。4. 观察内存曲线•正常预期每次退出后内存回落至基线附近整体呈锯齿状。•实际现象曲线呈阶梯状上升多次操作后内存增长至314.1MB且无明显回落。✅ 确认存在内存泄漏进入下一步。图21.2.2.2 步骤2堆快照对比定位异常对象1. 在 Profiler 中切换到Snapshot模板请参考使用 Snapshot 模板基本操作 选择Profiler工具 → 选择设备与应用进程 → 选择Snapshot模板 → 创建Session → 启动录制图32.抓取快照1首次在进入消息页前点击“Take Heap Snapshot”。图43.抓取快照2重复进出消息页面7次后回到首页点击“Take Heap Snapshot”再抓取第二次快照并停止录制。图54. 在快照对比视图Comparison中选择CompareToSnapshot1。图65. 查看对象新增销毁情况优先关注• 操作次数的整数倍或整数倍1export 出的对象本身也有一条引用链• 业务对象即Constructor的结构为包名/模块名/文件路径#泄漏对象图7关键发现• 对比结果中Test 对象实例数量从0个增加到8个。• 正常情况下页面退出后组件实例应被回收快照2中应只有1个Test 对象被export持有。✅ 确认 Test 对象实例泄漏。1.2.2.3 步骤3追踪引用链1. 在快照对比视图中展开并选中一个 Test 对象实例优先选Distance数量较多的此处我们选 “6” 的并打开右侧详细信息面板。注选择 Distance 数量较多的对象实例主要是因为这些实例频繁地被创建且未能得到及时释放。这表明存在潜在的内存泄漏问题需要进一步调查以确定具体的泄漏源。图82. 点击 “Shortest Paths” 获得如下Test 对象实例的最短引用链图91.2.2.4 步骤4分析VMRoot类型持有导致泄漏问题1. 根据步骤3得到的Test 对象实例的最短引用链分析该引用链的GC Root为 SourceTextModule 符合被export模块导出对象持有VMRoot泄漏类型场景。图102. 从GC Root向上排查引用链找到第一个业务对象即CacheTest.ts文件中的CacheTest对象的cache属性持有了Test 对象未释放。点击后面跳转图标打开CacheTest对象实例详细面板图113. CacheTest对象实例面板中点击对象名后跳转按钮跳转对应代码行。图124. 跳转代码后分析CacheTest对象存在静态属性cache该静态属性中保存了Test 对象。图135. 结合业务代码分析MessageCenterPage 组件创建时会保存一个Test 对象到CacheTest中但组件销毁时未清理该缓存导致内存泄漏。图146. 修改代码在页面销毁时清除缓存。修改后重新复现操作7次并抓快照验证Test 对象创建数量正常。但分析发现Proxy 对象数量异常创建70个未销毁。图157.参考“步骤3追踪引用链”找到最短引用链发现该 Proxy 对象的 Distance为1。说明其为GC Root直接持有的对象被Native侧直接持有未释放即JS对象被Local Handle/Global Handle引用导致内存泄露。图168. 通过 Proxy 对象的 Distance为1的References确认内存泄露是由JS对象被Global Handle引用导致。图171.2.2.5 步骤5分析Local Handle/Global Handle类型持有导致泄漏问题基于步骤4分析得到 Proxy 对象实例可能被Local Handle/Global Handle引用导致内存泄露我们可以通过以下步骤继续分析定位1. 配置Allocation录制模板并捕获数据•打开DevEcoStudio确保你的工程已加载并连接了目标设备或模拟器。•进入Profiler模块在主界面下方菜单栏找到并点击Profiler选项卡。•选择应用进程运行应用并在 “区域2” 选择目标设备和应用进程。•创建Allocation录制模板选择 “Allocation” 并点击 Create Session 创建录制模板图18•配置录制参数▪配置模式选择详情模式即关闭Statistics Mode。当前仅详情模式支持进行ArkTS和Native的关联分析▪配置开关勾选“Local Handle”和“Global Handle”这是关键配置。这将使Allocation专门捕获与JS-NAPI句柄相关的内存分配事件。○ 如果底层镜像不支持该功能则会提示“当前镜像版本不支持请升级镜像”图19▪配置泳道范围勾选 ArkTS Snapshot泳道。这将使Allocation在录制结尾时自动抓取一份Snapshot快照用于关联分析。图20▪启动录制勾选了“Local Handle”开关后如果是在应用本生命周期内首次录制local handle数据会触发弹窗请求重启应用以便录制对应信息此时点击OK允许重启即可。图21▪运行应用程序运行目标应用执行相关被怀疑引入内存泄露的业务操作持续一段时间以增加内存压力和捕获更多数据。▪停止录制自动触发抓取一份Snapshot快照用于关联分析。点击快照查找到疑似泄漏对象 Proxy 。图222. 泄漏对象关联分析•定位可疑ArkTS对象选中一个怀疑被泄漏的ArkTS对象实例或对象类型查看扩展标签页。图23•查看Native List若某个 ArkTS 对象的 distance 值为 1则可以通过扩展标签页中的Native List标签页查看所有当前与该 JS 对象关联的 Native 句柄引用以确认该 JS 对象是被Local Handle或Global Handle引用的对象。图24•关键信息1.句柄类型调用栈底层的符号ArkGlobalHandle或ArkLocalHandle判断泄漏类型2.调用栈通过调用栈可以定位到应用的Native代码可能是ArkUI框架代码或你自己代码中创建napi_ref的地方。3.注意点▪ 如果该JS对象节点不是一个被Local Handle或者 Global Handle引用的对象则会提示 No Detail▪ 如果该JS对象确实是一个被Local Handle或者 Global Handle引用的对象但是对应的native 内存的申请事件已经在此次录制之前完成内存分配本次录制结果则无法展示对应的内存申请调用栈需要重新录制录制时需要注意将录制时执行的业务逻辑范围调整的尽量更早一些。3.分析内存分配调用栈•排查调用栈“Native List”标签页中的调用栈找到对应业务代码•关键排查点▪ 检查是否在适当的时候调用了对应的句柄释放接口如napi_delete_reference等。▪ 梳理这段Native代码需要引用ArkTS对象的合理性识别这个引用的生命周期是否过长是否应该在某个条件满足后被释放▪ 对于napi_ref其引用计数是关键。确保在不再需要引用时正确调用了napi_delete_reference。注意引用计数可能因其他代码路径的创建或删除操作而意外增减。▪ 检查句柄的作用域是否合理。local handle是否应该只在JS执行上下文切换前使用global handle是否真的需要在整个应用生命周期都有效 。图251.2.3 修复验证1. 重新运行应用再次使用 Memory 泳道监控。2. 重复多次进出消息中心页。3.验证结果• 内存曲线恢复锯齿状每次退出后回落至基线。• 再次抓取快照对比多次操作后业务对象实例数量无明显增长。• 泄漏问题已修复。图26图271.3 附录1 ArkTS泄漏根因ArkTS运行时采用分代回收模型 将对象按生命周期划分为新生代和老年代。使用标记-清除mark-and-sweep算法回收内存所有可达对象被标记为存活不可达对象则被回收。每次GC都是从根节点开始遍历因此根节点被称为GC Root。该树的快照如图1所示。图28理解GC的核心在于一个关键洞察GC只回收“无法从GC Root到达”的对象。只要你的对象还挂在那棵引用树上哪怕你已经忘记它、不再需要它GC也无法回收。换句话说内存泄漏的本质不是GC失效而是开发者留下了不该留的引用链。因此排查内存泄漏的关键就是找到那条从GC Root出发、让无用对象“存活”的引用路径并在适当位置将其切断。