仓颉 std.runtime 包函数 API 全解析:GC 调控、堆内存监测与 CPU 性能剖析
仓颉 std.runtime 包函数 API 全解析:GC 调控、堆内存监测与 CPU 性能剖析
std.runtime 包是仓颉(Cangjie)语言标准库中与程序运行时环境交互的入口,围绕自动垃圾回收(GC)机制提供了手动触发 GC、设置 GC 触发阈值、读取堆内存与线程/处理器统计信息、生成堆快照以及 CPU 性能剖析等 17 个全局函数。本文基于 runtime_package_funcs.md 逐一解析这些函数的签名、参数、返回值与异常语义,并结合同包结构体文档与运行时环境变量手册补充底层原理,帮助你精确掌控程序的内存与性能状态。
一、std.runtime 包概览:为什么需要与运行时交互
仓颉语言使用自动垃圾回收机制管理内存,开发者通常无需手动释放对象。然而在以下场景中,直接与运行时交互变得必要:
- 在内存敏感型服务中,希望在业务低峰期主动触发一次 GC,减少高峰期停顿;
- 在长时间运行的程序中,需要监测堆内存使用量、GC 触发频率与回收效率,判断是否存在内存增长异常;
- 需要诊断 CPU 热点函数,将 profiler 记录导出后分析性能瓶颈。
std.runtime 包正是为这类需求设计的。根据 runtime_package_overview.md 的介绍,该包的作用是“与程序的运行时环境进行交互,提供了一系列函数和变量,用于控制、管理和监视程序的执行”,并针对 GC 提供“手动触发垃圾回收、设置垃圾回收的阈值、获取内存统计信息等功能”。
在使用这些 API 之前,需要通过 import std.runtime.* 引入包,例如:
import std.runtime.*
main() {
println(getProcessorCount())
}
包内的全部能力分为两类:全局函数(17 个,见 runtime_package_funcs.md)与结构体(3 个已废弃,见 runtime_package_structs.md)。整体 API 清单如下表:
| 函数名 | 功能 |
|---|---|
| dumpHeapData(Path) | 生成堆内存快照信息,写入指定路径的文件 |
| GC(Bool) <sup>(deprecated) | 执行 GC(即将废弃) |
| gc(Bool) | 执行 GC |
| getAllocatedHeapSize | 获取仓颉堆已被使用的大小(byte) |
| getBlockingThreadCount | 获取阻塞的仓颉线程数 |
| getGCCount | 获取触发 GC 的次数 |
| getGCFreedSize | 获取 GC 成功回收的内存(byte) |
| getGCTime | 获取 GC 总耗时(us) |
| getMaxHeapSize | 获取仓颉堆可使用的最大值(byte) |
| getNativeThreadCount | 获取物理线程数 |
| getProcessorCount | 获取处理器数量 |
| getThreadCount | 获取仓颉当前线程数量 |
| getUsedHeapSize | 获取堆/进程实际占用的物理内存(byte) |
| SetGCThreshold(UInt64) <sup>(deprecated) | 修改触发 GC 的内存阈值(KB,即将废弃) |
| setGCThreshold(UInt64) | 修改触发 GC 的内存阈值(KB) |
| startCPUProfiling | 启动 CPU profiler 跟踪 |
| stopCPUProfiling(Path) | 停止 CPU profiler 跟踪并写入记录文件 |
二、从结构体到全局函数:API 的演进与替代关系
在早期的 std.runtime 设计中,堆内存、处理器与线程的统计能力分别由 MemoryInfo、ProcessorInfo、ThreadInfo 三个结构体的静态属性提供(定义见 runtime_package_structs.md)。例如通过 MemoryInfo.maxHeapSize 读取堆上限:
import std.runtime.*
main() {
println(MemoryInfo.maxHeapSize)
}
运行结果(以实际环境为准):
268435456
如今这三个结构体均已标记为 deprecated,官方在文档中明确给出了替代方案:
MemoryInfo.allocatedHeapSize、heapPhysicalMemory、maxHeapSize→ 分别由全局函数 getAllocatedHeapSize、getUsedHeapSize、getMaxHeapSize 替代;ProcessorInfo.processorCount→ 由 getProcessorCount 替代;ThreadInfo.blockingThreadCount、nativeThreadCount、threadCount→ 由 getBlockingThreadCount、getNativeThreadCount、getThreadCount 替代。
类似地,函数层面也存在大小写命名的新旧交替:GC 与 SetGCThreshold 同样标记为 deprecated,未来版本将由小写命名的 gc 与 setGCThreshold 取代。新代码应直接使用小写命名的新函数,避免在后续版本升级时产生兼容性问题。
三、手动触发垃圾回收:gc 与 GC
仓颉的 GC 由运行时自动调度,但 std.runtime 允许开发者显式发起一次回收,用于在可控时机释放内存。
gc(heavy!: Bool = false)
public func gc(heavy!: Bool = false): Unit
- heavy! 参数:布尔类型,控制 GC 的执行程度。当为
true时,执行会变慢,但内存收集得更多;默认值为false。 - 返回值:无(
Unit)。
命名参数使用 ! 后缀标注,调用时可省略,即 gc() 等价于 gc(heavy!: false)。典型的主动回收场景是:程序在加载完成一批大对象并清理引用后,手动调用一次 gc(heavy!: true),尽量回收这批对象占用的内存,降低后续运行期的 GC 压力。
GC(heavy!: Bool = false) <sup>(deprecated)
public func GC(heavy!: Bool = false): Unit
这是旧版 API,功能与 gc 完全一致,但文档明确标注“未来版本即将废弃,使用 gc 替代”。两版函数的参数语义相同:heavy! 为 true 时执行慢但收集内存多,默认 false。
四、设置 GC 触发阈值:setGCThreshold 与 SetGCThreshold
仓颉运行时依据“堆水线”决定何时触发 GC:当仓颉堆大小超过阈值时,触发一次垃圾回收。std.runtime 提供了在程序运行期动态修改该阈值的函数。
setGCThreshold(value: UInt64)
public func setGCThreshold(value: UInt64): Unit
- value 参数:
UInt64类型,用户期望触发 GC 的内存阈值,单位为 KB。 - 返回值:无。
文档给出的示例为将 GC 阈值设置为 2MB:
import std.runtime.*
main() {
setGCThreshold(2048)
}
因为单位是 KB,2MB 对应 2048(2 × 1024 KB)。合理调低阈值可以让 GC 更频繁地触发,从而让堆占用保持在较低水平,代价是回收开销增加;调高阈值则减少 GC 频率,适合追求吞吐量的场景。
SetGCThreshold(value: UInt64) <sup>(deprecated)</sup>
public func SetGCThreshold(value: UInt64): Unit
旧版命名,功能与 setGCThreshold 相同,同样以 KB 为单位。文档明确标注“未来版本即将废弃”,新代码应使用小写版本。
与运行时环境变量 cjGCThreshold 的对应关系
除了运行期 API,仓颉运行时还允许在启动前通过环境变量配置 GC 水线。根据 runtime 环境变量使用手册,cjGCThreshold 用于“指定仓颉堆的参考水线值,当仓颉堆大小超过该值时,触发 GC”,支持 kb、mb、gb 等单位,默认值为堆大小。二者配合使用可以形成完整的调控闭环:
export cjGCThreshold=20480KB
启动前的环境变量适合确定程序整体的内存策略,运行期的 setGCThreshold 则适合根据实时业务状态动态微调。此外,同手册中的 cjHeapUtilization(堆利用率,默认 0.8)与 cjHeapGrowth(堆增长率,默认 0.15)也是 GC 后更新堆水线的参考依据,共同决定了触发 GC 的概率高低。
五、堆内存统计与堆快照
这一组函数用于回答“程序现在吃了多少内存、最多能吃多少、堆里发生了什么”等核心问题。
getAllocatedHeapSize()
public func getAllocatedHeapSize(): Int64
返回仓颉堆已被使用的大小,单位为 byte。它反映的是堆内已分配对象(含垃圾对象)占用的逻辑空间。
getUsedHeapSize()
public func getUsedHeapSize(): Int64
返回堆实际占用的物理内存大小,单位为 byte。文档特别强调了平台差异:
- 在 Linux 平台下,获取的是仓颉堆实际占用的物理内存大小;
- 在 Windows 及 macOS 平台下,获取的是仓颉进程实际占用的物理内存大小。
这与操作系统的内存计量方式有关:Linux 下可以从进程内存映射中单独区分出堆段,而 Windows/macOS 下则以整个进程的常驻物理内存为统计口径。
getMaxHeapSize()
public func getMaxHeapSize(): Int64
返回仓颉堆可以使用的最大值,单位为 byte,即堆的容量上限。该上限可通过启动前环境变量 cjHeapSize 配置(见 runtime_env.md):支持 kb、mb、gb 单位,设置范围为 [4MB, 系统物理内存],默认值在物理内存低于 1GB 时为 64MB,否则为 256MB。
dumpHeapData(path: Path)
public func dumpHeapData(path: Path): Unit
生成堆内存快照信息,并写入指定路径的文件,是排查堆内对象分布、定位内存泄漏的关键手段。
- path 参数:Path 类型,指定堆快照文件的保存路径。
Path是std.fs包提供的路径结构体,可通过Path("...")构造,且实现了Equatable、Hashable、ToString接口。 - 异常:生成堆内存快照失败时抛出
MemoryInfoException。
与堆快照配套的还有两个启动前环境变量(见 runtime_env.md):cjHeapDumpOnOOM(设为 on 时在堆溢出后自动输出堆快照文件)与 cjHeapDumpLog(指定堆快照输出路径,未指定时输出到当前执行目录)。运行期调用 dumpHeapData 与 OOM 自动转储互为补充,前者适合按需采样,后者适合事故兜底。
一个综合使用堆统计与快照的示例:
import std.runtime.*
import std.fs.*
main() {
println("已使用堆大小(byte): ${getAllocatedHeapSize()}")
println("实际占用物理内存(byte): ${getUsedHeapSize()}")
println("堆上限(byte): ${getMaxHeapSize()}")
dumpHeapData(Path("heap_dump.bin"))
}
六、GC 行为统计:次数、回收量与耗时
监控 GC 是否过于频繁、回收效率如何,需要以下三个统计函数,返回值均为 Int64:
| 函数 | 返回值含义 | 单位 |
|---|---|---|
| getGCCount() | 触发 GC 的次数 | 次 |
| getGCFreedSize() | 触发 GC 后成功回收的内存 | byte |
| getGCTime() | 触发的 GC 总耗时 | us |
通过计算 getGCFreedSize() / getGCCount() 可以得到平均单次回收量,结合 getGCTime() 可估算 GC 的平均停顿时间。例如,若观察到 GC 次数持续快速上涨但单次回收量很小,通常意味着程序存在大量短期存活对象或内存压力过高,此时可配合 setGCThreshold 与堆快照进一步定位。
GC 频率同样可以在启动前通过环境变量约束:cjGCInterval 指定两次 GC 的最小间隔(默认 150ms,若本次 GC 距上次小于该值则被忽略),cjBackupGCInterval 指定 backup GC 的间隔(默认 240 秒,超时未触发 GC 则强制触发一次),详见 runtime_env.md。
七、线程与处理器统计
std.runtime 提供一组线程/处理器信息查询函数,用于评估程序的并发规模与所在机器的算力:
| 函数 | 返回值含义 |
|---|---|
| getThreadCount() | 仓颉当前的线程数量 |
| getNativeThreadCount() | 物理线程数 |
| getBlockingThreadCount() | 阻塞的仓颉线程数 |
| getProcessorCount() | 处理器数量 |
其中“物理线程数”与“处理器数量”可用于估算机器的并行能力;getBlockingThreadCount 则能反映程序中有多少仓颉线程正阻塞在等待资源(如锁、IO)上,是分析线程池健康度的直观指标。仓颉线程的最大并发数也可以在启动前通过环境变量 cjProcessorNum 配置,支持范围为 (0, CPU 核数 × 2],默认值为 CPU 核数(获取失败时为 8),见 runtime_env.md。
八、CPU 性能剖析:startCPUProfiling 与 stopCPUProfiling
当需要定位 CPU 热点时,可以使用 profiler 跟踪记录函数调用耗时。std.runtime 提供一对函数:
startCPUProfiling()
public func startCPUProfiling(): Unit
启动 CPU profiler 跟踪,无参数、无返回值。
stopCPUProfiling(path: Path)
public func stopCPUProfiling(path: Path): Unit
停止 CPU profiler 跟踪,并将记录写入指定路径的文件。
- path 参数:Path 类型,记录文件的保存路径。
严格成对调用的约束与 ProfilingInfoException
这两个函数必须一一对应调用,否则抛出 ProfilingInfoException。具体规则如下:
- 调用了
startCPUProfiling()后,在未调用stopCPUProfiling(Path)的情况下又调用startCPUProfiling(),抛出异常; - 没有调用过
startCPUProfiling()而直接调用stopCPUProfiling(Path),抛出异常。
典型的正确用法是:
import std.runtime.*
import std.fs.*
main() {
startCPUProfiling()
// 被测代码段
var sum: Int64 = 0
for (i in 0..1000000) {
sum += i
}
stopCPUProfiling(Path("cpu_profile.bin"))
}
建议将分析范围收窄到需要诊断的函数或循环体,避免将启动、初始化等一次性开销计入 profile 数据。记录文件生成后,可结合仓颉配套的性能分析工具进行离线解析(相关工具介绍见 tools 文档)。
九、综合实战:运行时指标采样工具
将上述 API 组合起来,可以快速构建一个运行时指标采样工具,周期性打印内存、GC 与线程状态:
import std.runtime.*
import std.time.*
func sampleRuntimeInfo() {
println("===== Runtime Snapshot =====")
println("堆已使用(byte): ${getAllocatedHeapSize()}")
println("堆实际占用(byte): ${getUsedHeapSize()}")
println("堆上限(byte): ${getMaxHeapSize()}")
println("GC 次数: ${getGCCount()}")
println("GC 回收总量(byte): ${getGCFreedSize()}")
println("GC 总耗时(us): ${getGCTime()}")
println("仓颉线程数: ${getThreadCount()}")
println("物理线程数: ${getNativeThreadCount()}")
println("阻塞线程数: ${getBlockingThreadCount()}")
println("处理器数量: ${getProcessorCount()}")
}
main() {
sampleRuntimeInfo()
gc(heavy!: true)
sampleRuntimeInfo()
}
借助这套 API,开发者可以在不借助外部工具的情况下,将运行时状态直接打印到日志或上报到监控系统,为内存泄漏排查、GC 参数调优和性能回归测试提供一手数据。
十、小结
std.runtime 包以 17 个全局函数覆盖了仓颉程序运行时监测与调控的核心需求:
- GC 调控:手动触发(gc)与运行期动态设置触发阈值(setGCThreshold),配合启动前的
cjGCThreshold、cjHeapUtilization等环境变量(见 runtime_env.md)形成完整策略; - 内存观测:堆使用量、物理占用、堆上限与堆快照(dumpHeapData),以及 GC 次数/回收量/耗时三项统计;
- 并发观测:仓颉线程数、物理线程数、阻塞线程数与处理器数量;
- 性能剖析:startCPUProfiling 与 stopCPUProfiling(Path) 成对使用,导出 CPU 热点记录。
使用时要特别注意新旧 API 的命名差异:GC、SetGCThreshold 以及 MemoryInfo、ProcessorInfo、ThreadInfo 三个结构体均已标记为 deprecated,新代码应统一采用小写命名的全局函数形式,并为 dumpHeapData/stopCPUProfiling 传入合法、可写的 Path,避免触发 MemoryInfoException 或 ProfilingInfoException。