Uncovering Hidden Android Thread Pitfalls: Memory Leaks, Monitoring, and Hook Solutions
This article explores obscure Android thread issues—including uncontrolled thread creation, stack memory leaks, and the impact of thread‑priority settings—while presenting monitoring techniques, a pthread hook implementation, and performance considerations to help developers detect and resolve thread‑related crashes.
[anon:thread stack guard page]
When analyzing crashes caused by virtual memory exhaustion, the
/proc/[pid]/mapsfile reveals many new entries that correspond to thread stacks. Each stack entry indicates a thread; a large number of such entries means many threads have been created.
Two main reasons can cause an excess of stack entries:
Threads are continuously created and never exit, causing the thread count to explode.
Threads exit but their stack memory is not released.
By comparing the total number of threads (from
/proc/[pid]/statusor
/proc/[pid]/task) with the number of
[anon:thread stack guard page]entries, you can determine whether the issue is a thread‑count explosion or a stack‑memory leak.
Case 1: Threads Do Not Exit
Even with thread pools, bugs can create “wild” threads that accumulate, leading to thread leaks. In Android, each Java thread consumes about 1 MiB of stack memory; native threads can have custom stack sizes via
pthread_attr_t. Unrestricted thread creation quickly exhausts the 32‑bit address space.
Excessive threads can trigger OOM crashes or other failures, as illustrated below:
Although 64‑bit devices have larger virtual address spaces, thread stacks still occupy physical memory, and the system imposes limits on thread creation:
System‑wide limit:
/proc/sys/kernel/threads-maxPer‑user limit:
RLIMIT_NPROC(applies to each app UID)
Failures in
mmapor
mprotectwhen allocating stack memory
When the limit is reached,
pthread_createreturns
EAGAIN. The following demo shows this behavior:
How to Monitor Excessive Threads
We periodically check the thread count via a watchdog. When the count exceeds a threshold, we report thread information for leak analysis. Java threads are obtained through
ThreadGroup, while native threads are read from
/proc/[pid]/status(field
Threads) and enumerated via
/proc/[pid]/task.
Hook Solution
Implementation Principle
By hooking
pthread_createand
pthread_setname_npusing PLT/GOT and export‑table hooks, we capture stack traces for both Java and native threads.
For details on the hook technique, see the article “快速缓解 32 位 Android 环境下虚拟内存地址空间不足的‘黑科技’”.
The hook handler for
pthread_createperforms three tasks:
Preserve original semantics and record the thread’s
pthread_tas a key.
Collect native and Java stack traces.
Listen for thread exit events via
pthread_key_createand remove the corresponding records.
The
pthread_setname_nphook updates the stored thread name.
Asynchronous Pitfall: Invalid pthread_t
Calling
pthread_gettid_npafter the child thread has already finished can cause a crash because the hook handler may run before the child’s resources are fully initialized. The fix is to defer the child’s start routine until after the original
pthread_createhandler completes, using a condition variable.
Hook Overhead
Time: On a Redmi Note7 (Snapdragon 660, Android 10), creating 1000 threads shows an average overhead increase from ~291 µs to ~479 µs.
Space: Each tracked thread stores <1 KB of data; 500 threads consume less than 1 MiB.
Case 2: Stack Memory Leak
Even when threads exit, their stack memory may remain if they are not detached or joined. The Linux man page states that
pthread_joinor
pthread_detachmust be called to release system resources (the stack).
Creating a thread without detach or join leaves its stack allocated, which appears as extra
[anon:thread stack guard page]entries in
/proc/[pid]/maps, unrelated to the current thread count.
Setting the thread’s detach state to
PTHREAD_CREATE_DETACHEDat creation avoids the need for explicit
pthread_detachor
pthread_join. Android Java threads are created in this detached state by default.
Detecting Stack Leaks
By also hooking
pthread_detachand
pthread_join, we can mark threads that have exited but still hold stack memory. When a thread exits, we check its
pthread_attr_tdetach state:
If
PTHREAD_CREATE_DETACHED, remove its record immediately.
If
PTHREAD_CREATE_JOINABLE, keep the record until a subsequent
pthread_detachor
pthread_joincall.
During a dump, any thread marked as exited but still recorded indicates a stack‑memory leak.
Conclusion
The watchdog and pthread hook have been deployed in WeChat for a long time. The watchdog provides metrics to track thread usage across releases, while the hook supplies detailed clues to resolve leaks, dramatically improving debugging efficiency. Future work includes adding a hook for the
cloneAPI to cover rare cases where threads are created via
clone.
The hook implementation has been merged into the open‑source Matrix project: https://github.com/Tencent/matrix
WeChat Client Technology Team
Official account of the WeChat mobile client development team, sharing development experience, cutting‑edge tech, and little‑known stories across Android, iOS, macOS, Windows Phone, and Windows.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.