Skip to content

[Bug] AnrMonitor.captureFullThreadDump OOM: 堆栈 joinToString 拼接撑爆堆内存 #1370

Open

Description

@2220229005

问题描述

AnrMonitor.captureFullThreadDump() 在第 291 行调用 stackTraceElements.joinToString("\n") 对主线程完整堆栈做无限制字符串拼接,在堆内存已接近上限(768MB)时 StringBuilder.ensureCapacityInternal → Arrays.copyOf 扩容触发 OutOfMemoryError,导致 ANR 监控器自身崩溃。

崩溃堆栈

java.lang.OutOfMemoryError: Failed to allocate a 3504 byte allocation with 5646840 free bytes and 5514KB until OOM, target footprint 805306368, growth limit 805306368
    at java.util.Arrays.copyOf(Arrays.java:3585)
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:177)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:543)
    at java.lang.StringBuilder.append(StringBuilder.java:186)
    at kotlin.text.StringsKt__AppendableKt.appendElement(Appendable.kt:92)
    at kotlin.collections.ArraysKt___ArraysKt.joinTo(_Arrays.kt:25080)
    at kotlin.collections.ArraysKt___ArraysKt.joinToString(_Arrays.kt:25321)
    at com.ai.assistance.operit.util.AnrMonitor.captureFullThreadDump(AnrMonitor.kt:291)
    at com.ai.assistance.operit.util.AnrMonitor.checkMainThreadHealth(AnrMonitor.kt:212)

根因分析

  1. joinToString 没有限制堆栈元素数量 — 如果主线程栈很深,数组可能有上百甚至上千个元素,joinToString 会在内部 StringBuilder 中不断 append + 扩容
  2. 去重检查在字符串构建之后 — 即使 analysis == lastAnrAnalysis(重复 ANR),也会先执行完整的 joinToString + analyzeStackTrace,白白消耗大量内存
  3. 100ms 轮询 + 无退避 — 主线程长时间阻塞时,每 100ms 就调用一次 captureFullThreadDump(),每次都构建大字符串,在内存压力场景下形成恶性循环
  4. 无 OOM 保护 — catch (e: Exception) 不捕获 OutOfMemoryError(它是 Error 不是 Exception),监控器自身直接崩溃

修复建议

companion object {
    // ...existing constants...
    private const val MAX_STACK_DEPTH = 60          // 单次堆栈最大深度,防止joinToString撑爆堆内存
    private const val CAPTURE_COOLDOWN_MS = 5000L   // 捕获冷却时间,避免持续ANR期间频繁构建大字符串
}

// 新增字段
private var lastCaptureTimeMs: Long = 0L

private fun captureFullThreadDump() {
    try {
        // 冷却期检查 — 避免持续ANR期间每100ms重复构建完整转储
        val now = System.currentTimeMillis()
        if (now - lastCaptureTimeMs < CAPTURE_COOLDOWN_MS) {
            return
        }
        lastCaptureTimeMs = now

        // 先获取主线程堆栈元素,限制深度
        val mainThreadStack: String = mainThread?.let {
            try {
                val stackTraceElements = it.stackTrace
                val limitedElements = if (stackTraceElements.size > MAX_STACK_DEPTH) {
                    stackTraceElements.toList().subList(0, MAX_STACK_DEPTH).toTypedArray()
                } else {
                    stackTraceElements
                }
                val stackStr = limitedElements.joinToString("\n") { element -> "    at $element" }
                context.getString(R.string.anr_main_thread_with_state, it.name, it.state, stackStr)
            } catch (e: Exception) {
                context.getString(R.string.anr_cannot_get_main_thread_stack, e.message ?: "")
            }
        } ?: context.getString(R.string.anr_cannot_get_main_thread_ref)

        // 先做去重检查,避免重复构建完整转储
        val analysis = analyzeStackTrace(mainThreadStack)
        if (analysis == lastAnrAnalysis) {
            AppLogger.w(tag, "检测到重复的ANR,跳过输出")
            return
        }
        lastAnrAnalysis = analysis

        // ...rest of method remains the same...

    } catch (oom: OutOfMemoryError) {
        // 捕获OOM,防止监控器自身崩溃导致进程死亡
        AppLogger.e(tag, "捕获线程转储时OOM,已跳过")
        lastCaptureTimeMs = 0  // 重置冷却,下次允许重试
    } catch (e: Exception) {
        AppLogger.e(tag, "捕获线程转储失败", e)
        captureMainThreadStack()
    }
}

同理 captureMainThreadStack() 中的 joinToString 也应限制深度:

val stackTrace = Thread.currentThread().stackTrace
    .drop(3)
    .take(MAX_STACK_DEPTH)  // 限制深度
    .joinToString("\n") { "    at $it" }

影响版本

当前 main 分支最新版本 (SHA: 3973e5ea)

Activity

  1. yoruuuchan commented on Oct 22, 2026

    @yoruuuchan
    Contributor

    On this OOM.

    Fix scope: cap the per-capture stack depth, move the repeated-ANR check before any string building, add a cooldown window plus a heap-headroom check before dumping, and keep OutOfMemoryError from taking the monitor down. Will include JVM regression tests; the PR will link this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions