Repository navigation
[Bug] AnrMonitor.captureFullThreadDump OOM: 堆栈 joinToString 拼接撑爆堆内存 #1370
Copy link
Copy link
Open
Description
问题描述
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)
根因分析
joinToString没有限制堆栈元素数量 — 如果主线程栈很深,数组可能有上百甚至上千个元素,joinToString会在内部StringBuilder中不断append+ 扩容- 去重检查在字符串构建之后 — 即使
analysis == lastAnrAnalysis(重复 ANR),也会先执行完整的joinToString+analyzeStackTrace,白白消耗大量内存 - 100ms 轮询 + 无退避 — 主线程长时间阻塞时,每 100ms 就调用一次
captureFullThreadDump(),每次都构建大字符串,在内存压力场景下形成恶性循环 - 无 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)
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
yoruuuchan commentedon Oct 22, 2026
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.