Analyzing and Resolving a Main Thread ↔ JavaScriptCore Heap Collector Thread Deadlock on iOS 13
This article investigates a deadlock between the Main Thread and the JavaScriptCore Heap Collector Thread on iOS 13, explains the underlying mutex and RunLoop interactions, reproduces the issue with CFTimer‑based timers, and proposes an AutoReleasePool‑wrapped solution along with best‑practice recommendations for JavaScriptCore usage.
The problem originates from a high‑frequency timer implemented with CoreFoundation's CFTimer that creates JSValue objects on the main thread; when the timer fires, the main thread’s RunLoop executes __CFRunLoopDoTimers , triggers an autorelease pool, and attempts to release JSValue objects while the JavaScriptCore Heap Collector Thread is simultaneously performing garbage collection.
Analysis of stack traces shows that both the Main Thread and the JSC Heap Collector Thread contend for the same mutex inside the main RunLoop, satisfying the four classic deadlock conditions (mutual exclusion, hold‑and‑wait, no‑preemption, circular wait). The JSC thread holds the lock while waiting for the main thread to finish its autorelease work, and the main thread waits for the JSC thread’s GC to release the lock, creating a circular wait.
Key observations include:
The issue appears only on iOS 13 because its __CFRunLoopDoTimers contains an extra autorelease‑pool step not present in other versions.
Using NSTimer does not reproduce the deadlock because its callback is already wrapped in an autorelease pool.
The deadlock can be reliably reproduced by a demo that schedules a high‑frequency CFTimer which repeatedly creates JSValue objects.
Solution: wrap the timer callback code in an explicit @autoreleasepool block so that JSValue objects are released before the RunLoop reaches the critical section, breaking the circular wait. The article provides pseudo‑code illustrating this fix.
Additional recommendations for safe JavaScriptCore usage on mobile include:
Encapsulate short‑lived JSContext ‑related objects in autorelease pools.
Use JSManagedValue for long‑lived references.
Access the current context via [JSContext currentContext] .
Avoid cross‑thread VM interactions; keep a single VM per thread.
Be aware that other RunLoop‑related APIs (e.g., performSelector , CFRunLoopPerformBlock ) can also trigger similar lock contention.
Kuaishou Tech
Official Kuaishou tech account, providing real-time updates on the latest Kuaishou technology practices.
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.