线程和内存

每当应用创建新线程时,操作系统都必须分配内存来支持该线程。虽然与完整进程相比,线程通常被视为“轻量级”,但其内存开销并非为零。线程泄漏是 Android 上非常常见的内存泄漏方式。

线程的开销

在 Android 上创建线程时,有两个主要组件会消耗内存:

  1. 原生堆栈:pthread 库会为线程分配一个堆栈,用于存储局部变量和调用帧。在 Android 上,默认情况下通常为 1MB。
  2. 内部 JVM/内核结构:ART 和 Linux 内核必须分配数据结构(例如 pthread_internal_t 和 java.lang.Thread 对象)来跟踪和管理线程。

堆栈内存和 mmap

请务必了解,Android 会使用 mmap 和 MAP_ANONYMOUS | MAP_NORESERVE 标志分配 1MB 的原生堆栈。

这意味着操作系统会预留 1MB 的虚拟内存地址空间,但不会立即分配 1MB 的物理 RAM。只有当线程实际执行代码并将数据推送到堆栈时,系统才会分配(调入)物理内存。不执行任何操作的线程会为其堆栈消耗极少的物理 RAM。

实操练习:泄漏线程

我们将使用 MemoryLab 示例应用来观察当应用创建数百个空闲线程时会发生什么。

1. 发布和基准

启动应用并获取基准内存读数:

adb shell am start -W -n com.android.memorylab/.MainActivity
adb shell dumpsys meminfo -s com.android.memorylab

查看输出中的 Stack 和 Native Heap 行。它们可能很小。

2. 创建 100 个消息串

点按 Create 100 Threads 按钮。该应用将生成 100 个新线程。为了确保堆栈内存实际分页到物理 RAM 中(以便我们可以在工具中看到它),每个线程在开始等待之前都会在其堆栈上分配并写入一个 10KB 的小数组。

在线程处于活动状态时,再次运行 meminfo 命令:

adb shell dumpsys meminfo -s com.android.memorylab

结果:

将新输出与基准进行比较。您会注意到,在线程处于活动状态时,以下指标会显著增加:

  • Stack:增加了 1 MB 或更多。这表示为 100 个线程堆栈(每个堆栈至少占用 10KB)分配的物理页面。
  • Native Heap / Private Other:由于系统为每个线程分配的内部 pthread 结构而增加。

如需进行清理,请点按销毁所有线程按钮。

3. 在 Perfetto 中查看线程

Perfetto 非常适合跟踪线程生命周期和一段时间内的线程数。

Perfetto 界面的屏幕截图,其中显示了一个计数器轨道,其中包含 memorylab 应用中活跃线程的数量以及启动和退出的线程

用于线程分析的 PerfettoSQL

您可以查询跟踪记录,以准确统计应用生成了多少个线程:

SELECT p.name AS process_name, COUNT(t.id) as thread_count
FROM thread t JOIN process p USING (upid)
WHERE p.name = 'com.android.memorylab' AND t.name LIKE 'LeakedThread-%';

此类查询可用于生成带有计数器的调试轨道,如屏幕截图所示。


← 应用代码 | ↑ 向上 | 局部性 →