【用途】 这是一个面向桌面 JVM 应用的内存诊断技能,目标是帮助判断应用“占用 1 GB”这类数字的真实含义,并给出可验证的降低总占用(footprint)的路径,而不是盲目调参。
【主要能力】
- 判读方法:以 Java 堆与总占用的比例作为行动依据。堆小(如约 150 MB)而总占用大,说明原生内存占主导,属于分配器问题;堆大且接近上限,则属于 Java 侧问题,应从堆信息和堆转储入手,分配器调参无用。
- 决定性实验:请求分配器把空闲内存归还给操作系统,测量前后的 footprint。资料示例为:归还前 920 MB,malloc_trim(0) 返回 1,归还后 753 MB。可按需归还的内存本来就已是空闲的,因此这一结果排除泄漏;若存在泄漏则应无处可还。
- 先排除明显嫌疑并写明方式:堆无限增长(在 footprint 高时查看堆信息,远低于上限即可排除);原生句柄泄漏(使其可计数,例如每个句柄对应具名线程时,采样 30 分钟线程名,数量平稳即说明已释放)。
- 解释不同机器上的差异:Linux 上 C 库为争用线程分配各自 arena,上限为 8×核数且不归还;四核上限 32,二十核上限 160;其他平台同样按核缓存但扩展较缓;三平台出现相同症状本身即是线索。
- 调节杠杆表:应用空闲/暂停时归还空闲页(Linux、Windows);在打包配置的 JVM 选项中让收集器反提交未使用堆(所有平台);关闭小分配区(macOS,应用属性/环境);限制分配器 arena(Linux,启动器环境 MALLOC_ARENA_MAX)。各平台归还空闲页的调用拼写不同(Linux 为 malloc_trim(0),Windows 为 heap-optimize 调用),可通过已有原生访问层绑定。
【使用方式与陷阱】
- 堆上限只约束增长,不约束归还;实测中收集器在会话中途保持的已提交内存约为存活量的两倍。
- 空闲比例阈值若无周期性回收就是空操作;收集器只在并发周期结束或完整 GC 时反提交,分配很少的应用从不触发,堆会停在高水位;周期性间隔开关是承重的一半。
- 归还空闲内存必须仅在空闲时执行:该调用持分配器锁遍历堆,会阻塞所有分配线程(含解码线程),在播放或动画中执行会被感知;应在暂停/空闲切换时触发,限频(每分钟一次足够),并放到非 UI 线程。
- macOS 上不要对全部已注册 zone 调用归还:后台线程执行可能撞进主线程 UI 事务提交,在当前 macOS(26 及以后,资料已定位)上进程会硬崩;窗口很窄,挂日志流会掩盖,只在真实文件管理器启动时复现。macOS 应改用 zone 设置,无损。
- 上限不等于实际分配:降低每句柄大缓冲上限看似节省,应先测量实际预取量,从未接近的上限不花成本,降低只带来欠载风险。
- 先放宽再移除 arena 上限:上限以内存换分配器锁争用,数倍于激进值仍远低于大机器默认上限并保留大部分收益。
- 为打包 macOS 应用添加环境字典会副作用影响 PATH,采用 macOS 杠杆前先审计应用启动的所有外部进程。
【验证】 使用约 30 分钟后同时读取堆与 footprint,并记录核数。Linux 可用 pgrep 取 pid,读取 /proc/$pid/status 的 VmRSS,用 j cmd 执行 GC.heap_info,并通过 maps 中 64 MB 对齐的匿名映射计数 arena;macOS 用 ps -o rss、vmmap -summary 与 GC.heap_info。每次改动后重跑决定性实验:仍能下降说明还有可回收空间;不再下降说明剩余占用确在使用中,继续调分配器是浪费。
【适用场景与限制】 适用于桌面 JVM 应用的内存占用分析与调优,尤其是堆小、footprint 大的情况。资料仅在一种平台上做了详细测量,三平台症状一致;对于具体应用、具体数值和具体平台行为,需以自身实测为准。