在 HarmonyOS 的 ArkTS 运行时中,内存管理采用了基于对象追踪(Tracing GC)的高性能垃圾回收机制,并结合了专门的泄漏检测工具,以保障应用的流畅运行与内存安全。

一、 核心 GC 策略:HPP GC(高性能部分垃圾回收)

ArkTS 运行时采用 HPP GC(High Performance Partial Garbage Collection)模型,通过分代模型与混合算法,实现了高效的内存回收。

  1. 分代模型:将对象划分为年轻代(Young Space)和老年代(Old Space)。新分配的对象进入年轻代,经过多次 GC 依然存活的对象会被晋升到老年代。
  2. 混合回收算法
    • 年轻代:采用“标记-复制”算法,由于年轻代对象生命周期短,该算法回收效率高。
    • 老年代:采用“启发式 CSet 选择算法”,结合“标记-整理”与“标记-清扫”,优先回收存活率低、代价小的区域,既避免了内存碎片,又兼顾了性能。
  3. GC 类型与触发机制
    • Young GC:年轻代空间不足时触发,主要回收年轻代对象,速度快。
    • Old GC:老年代空间不足时触发,对年轻代和部分老年代进行整理压缩,耗时相对较长。
    • Full GC(CompressGC):应用切换到后台时触发,对全堆进行压缩,最大限度回收内存,主要用于性能不敏感场景。
  4. 流程优化:HPP GC 引入了并发标记、并行清扫、并行复制等优化,大幅减少主线程挂起时间(Stop-The-World),保障应用流畅度。
import { Component } from '@kit.ArkUI';

@Entry
@Component
struct SafeResourcePage {
  private timerId: number = -1;
  private largeBuffer: ArrayBuffer | null = null;

  aboutToAppear() {
    // 初始化资源
    this.largeBuffer = new ArrayBuffer(10 * 1024 * 1024); // 10MB
    this.timerId = setInterval(() => {
      console.info('Timer ticking...');
    }, 1000);
  }

  // 【核心防线】:组件销毁时释放所有资源
  aboutToDisappear() {
    // 1. 清除定时器
    if (this.timerId !== -1) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
    // 2. 释放大型内存对象
    this.largeBuffer = null;
    // 3. 开发阶段可手动触发 GC 验证内存是否下降
    // System.gc(); 
  }

  build() {
    Column() {
      Text('Safe Resource Management Demo')
    }
  }
}

二、 内存泄漏检测:JSLeakWatcher

为帮助开发者快速定位 ArkTS 对象内存泄漏,系统提供了 JSLeakWatcher 能力。

  1. 工作原理:利用 FinalizationRegistry 弱引用监听机制,定期对目标对象执行 Full GC。若对象在生命周期结束后仍被意外持有(如 Native 强引用、全局缓存),GC 无法回收,则判定为泄漏。
  2. 使用方式:通过 jsLeakWatcher.enableLeakWatcher 接口开启,可配置监测对象类型、检查间隔、泄漏阈值等。
  3. 结果输出:检测到泄漏后,生成 *.jsleaklist(泄漏对象列表)和 *.rawheap(堆快照)文件,导入 DevEco Studio 即可通过引用链定位到泄漏根节点。
class GlobalCache {
  // 使用 WeakRef 持有大对象,不增加强引用计数
  private static cachedData: WeakRef<ExpensiveObject> | null = null;

  static setData(data: ExpensiveObject) {
    GlobalCache.cachedData = new WeakRef(data);
  }

  static getData(): ExpensiveObject | undefined {
    // 尝试获取对象,如果已被 GC 回收则返回 undefined
    return GlobalCache.cachedData?.deref();
  }
}

class ExpensiveObject {
  constructor(public id: number) {}
}

三、 资源泄漏检测与系统级监控

除 ArkTS 对象外,系统还提供资源泄漏检测能力,覆盖句柄、线程及 Native 内存。

  1. 检测机制:系统每隔 60s 采样一次进程资源。当句柄数超 5000、线程数超 700,或 JS 堆内存使用率超 85% 时,自动抓取维测信息并上报。
  2. Native 内存泄漏:通过 DevEco Studio 的 Allocation 工具分析,可捕获未释放的 napi_ref 等 Native 对象分配栈,定位跨语言内存泄漏。
  3. 日志获取:资源泄漏日志存放于 /data/log/reliability/resource_leak/,可通过 DevEco Testing 或 HiAppEvent 订阅获取。
#include <napi/native_api.h>

// 模拟 Native 资源分配与释放
static napi_value CreateNativeResource(napi_env env, napi_callback_info info) {
    // 1. 分配 Native 内存
    char* nativeBuffer = new char[1024 * 1024]; 
    
    // 2. 创建外部对象,绑定释放回调
    // 当 ArkTS 侧的 JS 对象被 GC 回收时,会自动触发此回调释放 Native 内存
    napi_value result;
    napi_create_external(env, nativeBuffer, [](napi_env env, void* data, void* hint) {
        delete[] static_cast<char*>(data); // 安全释放
    }, nullptr, &result);
    
    return result;
}

// 模块初始化
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        { "createNativeResource", nullptr, CreateNativeResource, nullptr, nullptr, nullptr, napi_default, nullptr }
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

四、 内存管理最佳实践

  1. 避免循环引用与全局缓存:合理使用 WeakRef 或及时清理 Map/Set 中的过期对象。
  2. Node-API 规范:使用 napi_create_reference 创建强引用后,务必在适当时机调用 napi_delete_reference 释放;使用 napi_handle_scope 管理局部引用。
  3. 线程安全函数napi_threadsafe_function 使用后需调用 napi_release_threadsafe_function 确保引用计数归零,防止泄漏。
  4. 组件生命周期:在 ArkUI 组件的 aboutToDisappear 中及时取消定时器、移除事件监听、销毁 3D 场景等资源。
#include <napi/native_api.h>
#include <thread>

static napi_threadsafe_function tsfn = nullptr;

// 后台线程执行函数
void BackgroundThreadFunc() {
    // 1. 获取 TSFN,增加引用计数
    napi_acquire_threadsafe_function(tsfn);
    
    // 模拟耗时任务
    std::this_thread::sleep_for(std::chrono::seconds(2));
    
    // 2. 将数据发送到 ArkTS 主线程
    napi_call_threadsafe_function(tsfn, nullptr, napi_tsfn_blocking);
    
    // 3. 【关键】:任务完成后,必须释放 TSFN,使引用计数归零
    napi_release_threadsafe_function(tsfn, napi_tsfn_release);
}

static napi_value StartBackgroundTask(napi_env env, napi_callback_info info) {
    // 创建 TSFN(此处省略回调函数参数)
    napi_create_threadsafe_function(env, nullptr, nullptr, nullptr, 0, 1, nullptr, nullptr, nullptr, nullptr, &tsfn);
    
    std::thread(BackgroundThreadFunc).detach();
    return nullptr;
}

五、 底层引擎深化:HPP GC 的启发式 CSet 算法

老年代的回收是 GC 调优的核心难点。ArkTS 引入的启发式 Collection Set(CSet)选择算法,完美平衡了“标记-整理”与“标记-清扫”的优缺点。

  • 动态区域选择:在标记阶段,系统会统计每个内存区域的存活对象大小并计算存活率(存活对象大小/区域大小)。
  • 混合回收策略:根据设定的阈值,优先将存活率低、回收代价小的区域纳入 CSet 队列,对这些区域执行“标记-整理”(复制存活对象到空闲区);对未被选入 CSet 的其余区域,则直接执行低开销的“标记-清扫”。这既避免了内存碎片,又控制了性能开销。
  • SmartGC 智能延迟:在动画播放、页面转场等对帧率极度敏感的场景,SmartGC 会主动延迟 GC 触发,将回收推迟到空闲时段。开发者需严格控制图片并发加载数量,防止在 SmartGC 延迟期间内存水位暴涨,导致后续触发耗时的 CompressGC 引发卡顿。
import { taskpool } from '@kit.ArkTS';

@Concurrent
function decodeImageTask(url: string): ArrayBuffer {
  // 模拟耗时的图片解码任务
  return new ArrayBuffer(1024 * 1024);
}

@Entry
@Component
struct SmartGCSafeDemo {
  @State isLoading: boolean = false;

  async loadImagesSafely(urls: string[]) {
    if (this.isLoading) return;
    this.isLoading = true;

    // 【核心策略】:使用 TaskGroup 限制并发数量,避免瞬间创建过多大对象
    // 导致 SmartGC 延迟期间内存溢出
    const group = new taskpool.TaskGroup();
    const BATCH_SIZE = 3; // 严格限制单批次并发解码数量
    
    for (let i = 0; i < Math.min(urls.length, BATCH_SIZE); i++) {
      group.addTask(new taskpool.Task(decodeImageTask, urls[i]));
    }

    try {
      await taskpool.execute(group);
    } catch (e) {
      console.error('Batch decode failed:', e);
    } finally {
      this.isLoading = false;
    }
  }

  build() {
    Column() {
      Button('Safe Load Images')
        .onClick(() => this.loadImagesSafely(['url1', 'url2', 'url3', 'url4']))
    }
  }
}

六、 复杂泄漏场景:跨语言与资源句柄排查

除了常规的 ArkTS 对象泄漏,跨语言交互与底层资源句柄是内存泄漏的重灾区。

  • Native 强引用泄漏:使用 Node-API 时,若调用 napi_create_reference 创建了强引用但未调用 napi_delete_reference 释放,会导致 ArkTS 对象与 Native C++ 对象双重泄漏。可通过 DevEco Studio 的 Allocation 工具捕获 Native 分配栈,检查是否存在相关栈帧。
  • 线程安全函数(TSFN)泄漏napi_threadsafe_function 在使用 napi_acquire_threadsafe_function 增加引用计数后,必须在回调执行完毕时调用 napi_release_threadsafe_function 使其归零。推荐通过注册 napi_add_env_cleanup_hook 钩子函数,确保在 ArkTS 运行环境销毁时强制释放 TSFN。
  • PixelMap 内存陷阱:一张 4000x3000 的 RGBA_8888 图片解码后在内存中占用约 45.8MB。由于 PixelMap 的实际像素数据分配在 Native 堆上,ArkTS 侧仅持有轻量级引用句柄,GC 不会因其占用大量 Native 内存而急于回收。必须主动调用 pixelMap.release() 立即释放 Native 内存,不可完全依赖 GC。
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct NativeMemorySafeDemo {
  private pixelMap: image.PixelMap | null = null;

  async loadImage() {
    // 模拟解码一张 4000x3000 的大图(占用约 45.8MB Native 内存)
    // 注意:GC 不会因 Native 内存大而主动回收 ArkTS 侧的轻量句柄
    const buffer = new ArrayBuffer(4000 * 3000 * 4); 
    const decodingOptions: image.DecodingOptions = {
      editable: false,
      desiredPixelFormat: image.PixelMapFormat.RGBA_8888
    };
    const imageSource = image.createImageSource(buffer);
    this.pixelMap = await imageSource.createPixelMap(decodingOptions);
  }

  // 【核心防线】:组件销毁时,必须主动释放 Native 内存
  aboutToDisappear() {
    if (this.pixelMap) {
      this.pixelMap.release(); // 立即释放 Native 堆内存
      this.pixelMap = null;
      hilog.info(0x0000, 'Memory', 'PixelMap Native memory released.');
    }
  }

  build() {
    Column() {
      Button('Load Huge Image').onClick(() => this.loadImage())
    }
  }
}

七、 高级内存优化:缓存预算与降采样

针对图片密集型与高频数据场景,必须实施严格的内存预算管控。

  • 解码阶段降采样:严禁先解码全尺寸图片再进行 UI 缩放。必须在解码时通过 DecodingOptions.desiredSize 指定目标尺寸,或启用 JPEG 优化解码(YUV 格式),从源头降低内存占用。
  • LRU 缓存双维约束:构建内存缓存时,必须同时限制“条目数量上限”与“总字节数上限”。在写入新数据前,需循环淘汰最久未使用的条目(TrimToSize),确保缓存总大小严格控制在预算范围内,防止内存无限膨胀。
class LRUMemoryCache {
  private cache: Map<string, ArrayBuffer> = new Map();
  private maxCount: number;
  private maxBytes: number;
  private currentBytes: number = 0;

  constructor(maxCount: number = 100, maxBytes: number = 50 * 1024 * 1024) {
    this.maxCount = maxCount;
    this.maxBytes = maxBytes;
  }

  put(key: string, value: ArrayBuffer): void {
    // 1. 若已存在,先减去旧大小并删除
    if (this.cache.has(key)) {
      this.currentBytes -= this.cache.get(key)!.byteLength;
      this.cache.delete(key);
    }

    // 2. 【双维约束】:循环淘汰最久未使用的条目(LRU 头部)
    while (this.cache.size >= this.maxCount || this.currentBytes + value.byteLength > this.maxBytes) {
      const firstKey = this.cache.keys().next().value;
      if (firstKey) {
        this.currentBytes -= this.cache.get(firstKey)!.byteLength;
        this.cache.delete(firstKey);
      }
    }

    // 3. 插入新数据
    this.cache.set(key, value);
    this.currentBytes += value.byteLength;
  }

  get(key: string): ArrayBuffer | undefined {
    const value = this.cache.get(key);
    if (value) {
      // 刷新 LRU 顺序:删除后重新插入到尾部
      this.cache.delete(key);
      this.cache.set(key, value);
    }
    return value;
  }
}

八、 线上监控:HiAppEvent 资源泄漏订阅

为了在现网环境中捕获内存异常,需建立完善的线上监控闭环。

  • 事件订阅:通过 hiAppEvent.addWatcher 订阅 RESOURCE_OVERLIMIT 系统事件。
  • 阈值触发与日志抓取:结合 hidebug.setAppResourceLimit 设置内存限制,当应用 PSS 内存或 JS 堆内存超标时,系统会自动回调 onReceive 函数并生成包含内存详情(pss、rss、vss)的故障日志。
  • 上报频控:为避免日志风暴,同一应用在 24 小时内至多上报一次内存泄漏事件,若需二次触发需重启设备
import { hiAppEvent } from '@kit.BasicServicesKit';
import { hidebug } from '@kit.PerformanceAnalysisKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

export class MemoryMonitor {
  static startMonitoring() {
    // 1. 设置内存限制阈值(例如 300MB)
    hidebug.setAppResourceLimit(hidebug.ResourceType.MEMORY, 300 * 1024 * 1024);

    // 2. 订阅系统资源超限事件
    hiAppEvent.addWatcher({
      name: 'AppMemoryWatcher',
      appEventFilters: [{ domain: 'os', names: ['RESOURCE_OVERLIMIT'] }],
      onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
        for (const group of appEventGroups) {
          for (const event of group.appEventInfos) {
            hilog.error(0x0000, 'MemoryMonitor', 'Memory Overlimit Detected: %{public}s', JSON.stringify(event));
            
            // 3. 触发日志抓取与上报(实际工程中可接入云端上报 SDK)
            // 注意:系统频控限制,同一应用 24 小时内至多上报一次
            MemoryMonitor.reportToCloud(event);
          }
        }
      }
    });
  }

  private static reportToCloud(event: hiAppEvent.AppEventInfo) {
    // 提取 pss, rss, vss 等内存详情并上报
    console.info('Uploading memory fault log...');
  }
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐