仓颉 std.runtime 运行时控制与监控 API 全解:GC 手动回收、阈值调优、堆内存统计与 CPU 采样

原创2026-10-14 21:41:09723 阅读
文章标签:编程语言语言运行时标准库内存管理OpenHarmony

仓颉 std.runtime 运行时控制与监控 API 全解:GC 手动回收、阈值调优、堆内存统计与 CPU 采样

std.runtime 是仓颉(Cangjie)标准库中面向程序运行时环境的控制与管理包,它把垃圾回收(GC)的手动触发、触发阈值调整、堆内存/线程/处理器统计采集以及 CPU Profiler 启停等能力封装为一组全局函数。本文以官方 API 文档 runtime_package_funcs.md 为骨架,逐函数讲解签名、参数、返回值与异常语义,并结合 cangjie_runtime 仓库中的标准库源码(stdlib/libs/std/runtime)与运行时 C++ 实现(runtime/src/CompilerCalls.cpp)剖析其底层原理。读完本文,你将能自主完成 GC 调优、内存水位监控、线程状态观测与 CPU 采样分析等典型运行时诊断任务。

包定位与整体能力

根据官方包总览文档 runtime_package_overview.md,runtime 包的核心作用是“与程序运行时环境交互,提供一系列用于控制、管理与监控程序执行的函数与变量”。由于仓颉语言采用自动垃圾回收机制管理内存,该包重点围绕三件事展开:

  • 主动控制 GC:手动触发一次垃圾回收、修改 GC 触发阈值;
  • 运行时统计:读取堆内存分配量/使用量/上限、GC 次数/耗时/回收量、线程与处理器数量;
  • 性能采样:启动/停止 CPU Profiler 并将采样记录写入文件。

从实现布局看,标准库侧由多个源文件组成:runtime_gc.cj(GC 控制与统计)、runtime_memoryInfo.cj(堆内存统计与堆转储)、runtime_threadInfo.cj(线程统计)、runtime_processorInfo.cj(处理器统计与 CPU Profiler)、runtime_signal.cj(信号处理,需 cjnative 后端)。其中多数函数通过 @Intrinsic 或 foreign 声明直接下沉到运行时,最终由 CompilerCalls.cpp 中对应 MCC_* 导出函数实现,这是理解本文各 API 底层行为的关键线索。

手动触发垃圾回收:gc 与已废弃的 GC

gc(Bool):推荐的触发方式

public func gc(heavy!: Bool = false): Unit
  • 功能:立即执行一次垃圾回收。
  • 参数 heavy!:Bool 类型,控制回收强度。为 true 时回收更彻底、释放更多内存,但耗时更长;为 false 时更快但回收较少。默认值为 false。

底层实现中,该函数通过 @Intrinsic 声明的 invokeGC(heavy) 将调用下沉到运行时(见 runtime_gc.cj),运行时侧对应 MCC_InvokeGCImpl:

extern "C" void MCC_InvokeGCImpl(bool sync) { HeapManager::RequestGC(GC_REASON_USER, !sync); }

GC_REASON_USER 表明这是一次用户主动发起的回收请求,heavy 参数直接映射到同步/异步语义;可见 gc(true) 在运行时被当作更“重”的同步回收处理。

GC(Bool):已废弃的旧接口

public func GC(heavy!: Bool = false): Unit  // deprecated

旧版接口 GC 功能与 gc 完全相同,但文档明确标注将在未来版本中废弃,请改用 gc。源码中的迁移关系一目了然——runtime_gc.cj 中:

@Deprecated[message: "Use 'public func gc(heavy!: Bool = false): Unit' instead."]
public func GC(heavy!: Bool = false): Unit {
    return gc(heavy: heavy)
}

旧接口只是对 gc 的一层转发包装,因此新代码应直接调用 gc,避免编译期收到废弃告警。

手动 GC 的使用场景

手动 GC 适合以下场景:

  • 在内存峰值敏感的关键路径(如大对象分配前)主动控制堆水位;
  • 在测试或基准(benchmark)中,通过 gc 在测量前清理垃圾,获得更稳定的堆状态;
  • 结合统计函数观察一次手动 GC 的回收效果(见下文 getGCFreedSize)。

调整 GC 触发阈值:setGCThreshold 与已废弃的 SetGCThreshold

仓颉运行时会根据堆内存水位自动触发 GC。若希望改变自动触发的“松紧度”,可用以下两个函数(单位均为 KB)。

setGCThreshold(UInt64):推荐的调参接口

public func setGCThreshold(value: UInt64): Unit
  • 功能:修改触发 GC 的内存阈值(单位 KB)。当仓颉堆大小超过该值时触发 GC。
  • 参数 value:UInt64 类型,目标阈值。

官方示例:将 GC 阈值设置为 2MB(2048 KB)。

import std.runtime.*
main() {
  setGCThreshold(2048)
}

SetGCThreshold(UInt64):已废弃的旧接口

public func SetGCThreshold(value: UInt64): Unit  // deprecated

旧接口功能一致,同样以 KB 为单位,但文档标注未来版本废弃,应改用小写命名的 setGCThreshold。源码同样只是转发包装(runtime_gc.cj):

@When[backend == "cjnative"]
@Deprecated[message: "Use 'public func setGCThreshold(value: UInt64): Unit' instead."]
public func SetGCThreshold(value: UInt64): Unit {
    return setGCThreshold(value)
}

阈值参数在运行时的作用

setGCThreshold 最终调用 @Intrinsic 的 setCjGCThreshold(value),运行时侧为 MCC_SetGCThreshold:

extern "C" void MCC_SetGCThreshold(uint64_t GCThreshold) { Runtime::Current().SetGCThreshold(GCThreshold); }

即该值会被写入当前 Runtime 实例,作为后续 GC 调度决策的参考阈值(标准库源码注释将其表述为 “Set GCThreshold that user expected for GC as reference threshold”,见 runtime_gc.cj)。需要注意:该参数是用户期望的参考阈值,最终是否触发还取决于运行时自身的堆增长与调度策略。

堆内存统计:分配量、使用量与上限

getAllocatedHeapSize():堆分配量

public func getAllocatedHeapSize(): Int64
  • 功能:获取仓颉堆已分配的内存大小,单位为字节。
  • 返回值:Int64,已分配堆大小。

getUsedHeapSize():实际占用物理内存

public func getUsedHeapSize(): Int64
  • 功能:获取仓颉堆/进程实际占用的物理内存大小(字节)。
  • 平台差异:在 Linux 上返回仓颉堆实际占用的物理内存;在 Windows 与 macOS 上返回仓颉进程实际占用的物理内存。

该函数的实现值得关注(runtime_memoryInfo.cj):

public func getUsedHeapSize(): Int64 {
    var ret = Int64(getRealHeapSize())
    match {
        case ret >= 0 => return ret
        case ret == -1 => throw MemoryInfoException("Failed to open smaps file.")
        case ret == -2 => throw MemoryInfoException("Failed to get valid value.")
        case _ => throw MemoryInfoException("Failed to get valid error code.")
    }
}

它通过 @Intrinsic 的 getRealHeapSize() 读取物理内存信息:Linux 下运行时读取进程 smaps 文件并统计堆对应区域(MCC_GetRealHeapSize 调用 Heap::GetHeap().GetHeapPhysicalMemorySize(),接口声明见 Heap.h);当底层文件读取或解析失败时,会抛出 MemoryInfoException,错误码 -1 表示打开 smaps 文件失败,-2 表示未能取得有效值。

getMaxHeapSize():堆上限

public func getMaxHeapSize(): Int64
  • 功能:获取仓颉堆可用的最大堆大小,单位为字节。
  • 返回值:Int64,最大可用堆大小。

该值对应运行时 MCC_GetMaxHeapSize 中的 Heap::GetHeap().GetMaxCapacity(),即堆区域的最大容量(接口见 Heap.h)。

堆内存统计函数一览

函数 返回内容 单位 说明
getAllocatedHeapSize() 仓颉堆已分配大小 字节 对应 MCC_GetAllocatedHeapSize → Heap::GetAllocatedSize()
getUsedHeapSize() Linux:堆实际物理占用;Win/macOS:进程实际物理占用 字节 失败时抛 MemoryInfoException
getMaxHeapSize() 最大可用堆大小 字节 对应 Heap::GetMaxCapacity()

GC 运行统计:次数、耗时与回收量

getGCCount()

public func getGCCount(): Int64
  • 功能:获取 GC 已触发的次数。
  • 返回值:Int64。

getGCTime()

public func getGCTime(): Int64
  • 功能:获取 GC 累计消耗的总时间,单位为微秒(µs)。
  • 返回值:Int64。

getGCFreedSize()

public func getGCFreedSize(): Int64
  • 功能:获取 GC 成功回收的内存总大小,单位为字节。
  • 返回值:Int64。

统计数据的产生位置

这三个函数的运行时数据源位于 GcStats.cpp 的全局计数器:

size_t g_gcCount = 0;
uint64_t g_gcTotalTimeUs = 0;
size_t g_gcCollectedTotalBytes = 0;

它们由 Copy Collector 在每次回收完成时累加更新(CopyCollector.cpp):

g_gcCount++;
g_gcTotalTimeUs += (gcTimeNs / NS_PER_US);
g_gcCollectedTotalBytes += gcStats.collectedBytes;

运行时导出函数 MCC_GetGCCount / MCC_GetGCTimeUs / MCC_GetGCFreedSize 直接读取这三个计数器并返回给标准库。由此可知:这些统计是进程生命周期内的累计值,适合在程序启动时记录基线、运行一段时间后再对比差值,以评估 GC 频率与回收效率。

线程与处理器统计

getThreadCount()

public func getThreadCount(): Int64
  • 功能:获取当前仓颉线程的数量。
  • 返回值:Int64。

getBlockingThreadCount()

public func getBlockingThreadCount(): Int64
  • 功能:获取处于阻塞状态的仓颉线程数量。
  • 返回值:Int64。

getNativeThreadCount()

public func getNativeThreadCount(): Int64
  • 功能:获取物理线程(操作系统线程)的数量。
  • 返回值:Int64。

getProcessorCount()

public func getProcessorCount(): Int64
  • 功能:获取处理器(CPU 核心)数量。
  • 返回值:Int64。

底层实现

线程类函数仅对 cjnative 后端开放(runtime_threadInfo.cj),底层由运行时调度器统计(CompilerCalls.cpp):

extern "C" size_t MCC_GetCJThreadNumber() { return ScheduleCJThreadCountPublic(CJTHREAD_PSTATE_ALL); }
extern "C" size_t MCC_GetBlockingCJThreadNumber() { return ScheduleCJThreadCountPublic(CJTHREAD_PSTATE_BLOCKING); }
extern "C" size_t MCC_GetNativeThreadNumber() { return ScheduleRunningOSThreadCount(); }

处理器数量则来自平台 API:Linux 使用 sysconf(_SC_NPROCESSORS_ONLN),macOS 使用 sysctl(HW_AVAILCPU/HW_NCPU),Windows 使用 GetSystemInfo()(实现见 runtime_ffi.c),标准库侧通过 foreign func CJ_OS_ProcessorCount() 调用(runtime_processorInfo.cj)。

生成堆内存快照:dumpHeapData(Path)

public func dumpHeapData(path: Path): Unit
  • 功能:生成堆内存快照(heap snapshot)信息,并写入指定路径的文件。
  • 参数 path:Path 类型,快照文件的输出路径(Path 结构参考)。
  • 异常:生成失败时抛出 MemoryInfoException。

实现与安全校验

标准库实现(runtime_memoryInfo.cj)在写文件前会做路径校验与解析:

  • 校验:拒绝空路径、超过 4096 字符的路径以及包含 \0 空字节的路径,防止路径注入与截断攻击(validatePathString,见 runtime_memoryInfo.cj);
  • 解析:相对路径会基于当前目录 canonicalize 为绝对路径,符号链接会解析到真实目标(resolvePath);
  • 写入:以 ReadWrite 模式打开文件,取得文件描述符后调用 @Intrinsic 的 dumpCjHeapData(fd);失败时抛出 MemoryInfoException("Failed to dump heap data.")。

Windows 平台走独立的 CJ_Runtime_OpenFileForFd/CJ_Runtime_CloseFd 桥接(见 runtime_ffi.c 中的宽字符路径转换),非 Windows 平台直接使用 File.fileDescriptor.fileHandle。

运行时快照生成

运行时侧 MCC_DumpCJHeapData 会先进入安全区(ScopedEnterSaferegion),再创建 CjHeapData 对象调用 DumpHeap(fd) 输出堆数据(实现位于 CjHeapData.cpp)。堆快照可用于排查内存泄漏与对象分布,配合 HeapSnapshotJsonSerializer 等序列化组件进一步分析。

CPU 性能采样:startCPUProfiling 与 stopCPUProfiling

startCPUProfiling()

public func startCPUProfiling(): Unit
  • 功能:启动 CPU Profiler 采样。
  • 异常:若在未调用 stopCPUProfiling(Path) 的情况下重复调用,抛出 ProfilingInfoException。

stopCPUProfiling(Path)

public func stopCPUProfiling(path: Path): Unit
  • 功能:停止 CPU Profiler 采样,并将采样记录写入指定路径的文件。
  • 参数 path:Path 类型,采样记录输出文件路径。
  • 异常:若未先调用 startCPUProfiling() 就调用本函数,抛出 ProfilingInfoException。

文档强调:startCPUProfiling 与 stopCPUProfiling(Path) 必须成对调用。

使用示例(成对调用)

import std.runtime.*
import std.fs.*

main() {
  startCPUProfiling()
  // ... 需要采样的业务代码 ...
  stopCPUProfiling(Path("profile.data"))
}

底层原理

标准库侧通过 @Intrinsic 的 startCjCPUProfiling() 与 stopCjCPUProfiling(fd) 下沉到运行时(runtime_processorInfo.cj),失败时抛出 ProfilingInfoException。运行时实现(CompilerCalls.cpp):

extern "C" bool MCC_StartCpuProfiling() {
    return CpuProfiler::GetInstance().StartCpuProfilerForFile();
}
extern "C" bool MCC_StopCpuProfiling(int fd) {
    return CpuProfiler::GetInstance().StopCpuProfilerForFile(fd);
}

CpuProfiler.cpp 显示:StartCpuProfilerForFile() 会检查是否已在采样(重复启动返回 false,对应标准库抛出的 ProfilingInfoException),随后创建独立的采样线程 SamplingThread 周期性采集调用栈;StopCpuProfilerForFile(fd) 则停止采样并把记录写入指定文件描述符。

关联的废弃结构体:MemoryInfo、ProcessorInfo、ThreadInfo

除了函数 API,runtime_package_structs.md 中还定义了三个已标注废弃的结构体:

结构体 功能 替代方案
MemoryInfo(废弃) 提供堆内存统计静态属性 改用 getAllocatedHeapSize()、getUsedHeapSize()、getMaxHeapSize()
ProcessorInfo(废弃) 提供处理器数量静态属性 改用 getProcessorCount()
ThreadInfo(废弃) 提供仓颉线程统计静态属性 改用 getBlockingThreadCount()、getNativeThreadCount()、getThreadCount()

其中 MemoryInfo.maxHeapSize 的官方示例输出为 268435456(即 256MB,数值随环境变化),可参考其格式验证堆上限读取是否正常。源码中这些结构体均带有 @Deprecated 注解(如 runtime_memoryInfo.cj),新代码应直接使用本文介绍的全局函数。

典型组合实践:写一个运行时诊断小工具

将上述 API 组合起来,可以快速搭建一个运行时健康检查工具。核心思路:记录基线 → 执行业务 → 记录快照 → 计算差值,评估 GC 压力与内存增长;必要时导出堆快照与 CPU 采样记录做离线分析。

import std.runtime.*
import std.fs.*

main() {
    // 1. 基线数据
    let baseCount = getGCCount()
    let baseTime = getGCTime()
    let baseFreed = getGCFreedSize()

    // 2. 业务代码(示例:触发一次完整回收)
    gc(heavy: true)

    // 3. 快照数据
    println("GC count delta: ${getGCCount() - baseCount}")
    println("GC time delta(us): ${getGCTime() - baseTime}")
    println("GC freed delta(B): ${getGCFreedSize() - baseFreed}")
    println("Heap: allocated=${getAllocatedHeapSize()}B, "
            + "used=${getUsedHeapSize()}B, max=${getMaxHeapSize()}B")
    println("Threads: cj=${getThreadCount()}, blocked=${getBlockingThreadCount()}, "
            + "native=${getNativeThreadCount()}, processors=${getProcessorCount()}")

    // 4. 导出堆快照与 CPU 采样
    dumpHeapData(Path("heap.hprof"))
    startCPUProfiling()
    // ... 采样目标代码 ...
    stopCPUProfiling(Path("cpu.profile"))
}

运行前提:上述 API 多数面向 cjnative 后端(源码中大量 @When[backend == "cjnative"] 条件编译),其中 isGCRunning() 还要求非 ohos 环境;getUsedHeapSize() 在 Linux 下依赖 smaps 文件读取,权限或环境受限时可能抛出 MemoryInfoException。编写跨平台程序时需注意这些平台差异。

总结

std.runtime 包为仓颉程序提供了完整的运行时控制与观测手段:

  • 控制类:gc(heavy:) 手动回收、setGCThreshold(value) 调整自动回收阈值;
  • 统计类:堆内存(getAllocatedHeapSize/getUsedHeapSize/getMaxHeapSize)、GC 运行(getGCCount/getGCTime/getGCFreedSize)、线程与处理器(getThreadCount/getBlockingThreadCount/getNativeThreadCount/getProcessorCount);
  • 诊断类:dumpHeapData(Path) 堆快照、startCPUProfiling()/stopCPUProfiling(Path) CPU 采样;
  • 迁移提醒:GC、SetGCThreshold 及三个统计结构体均已废弃,统一改用同名小写函数。

这些 API 在标准库侧通过 @Intrinsic/foreign 与运行时 MCC_* 导出函数一一对应(完整映射见 CompilerCalls.cpp),理解这层调用链有助于在内存调优与性能诊断时准确判断数据的含义与边界。

登录后查看全文
cangjie_runtime