Summary
This incident involved a Unity Android build crashing immediately after the splash screen on subsequent launches. The crash only resolved temporarily when the app cache was cleared, and on some devices it never recovered. The logcat trace pointed directly into Mono’s Boehm garbage collector (libmonobdwgc-2.0.so), indicating a memory corruption or invalid GC state triggered during startup.
Root Cause
The underlying issue was caused by Mono’s GC encountering corrupted or invalid memory during type reflection, specifically when Unity attempted to call Assembly.GetTypes() at startup. This typically happens when:
- Mono GC state becomes corrupted between sessions
- Reflection-heavy code runs during startup, allocating large arrays of types
- Cached domain data becomes invalid, especially when switching between IL2CPP and Mono builds
- Android OS restores stale process memory, causing Mono to read invalid GC heap regions
- Plugins or assemblies generate unexpected metadata, causing reflection to allocate invalid vectors
The crash stack clearly shows:
- GC attempting to mark objects (
GC_mark_from) - GC allocating a vector (
mono_gc_alloc_vector) - Managed call:
System.Reflection.Assembly:GetTypes()
This is a classic signature of Mono GC heap corruption.
Why This Happens in Real Systems
Real-world Unity Android builds using Mono can hit this issue because:
- Mono’s Boehm GC is not as robust as IL2CPP’s GC and is more prone to memory corruption
- Android aggressively kills and restores apps, exposing GC edge cases
- Unity’s domain reload behavior on mobile is fragile, especially with reflection-heavy code
- Switching scripting backends without clearing the project can leave stale metadata
- Some devices (especially low‑end or OEM‑modified Android builds) handle memory differently, making the issue device‑specific
Real-World Impact
This type of crash can cause:
- Permanent crash loops after the first successful launch
- User data loss if the fix requires clearing app data
- Device‑specific failures, making QA difficult
- Inability to ship Mono builds, forcing IL2CPP even during development
- Silent corruption, where no managed exception is thrown
Example or Code (if necessary and relevant)
Below is an example of a pattern that often triggers this crash when used at startup:
var allTypes = Assembly.GetExecutingAssembly().GetTypes();
This forces Mono to allocate a large vector and enumerate all types, which can destabilize the GC on Android when cached state is stale.
How Senior Engineers Fix It
Experienced engineers address this by:
- Switching back to IL2CPP, which eliminates Mono GC entirely
- Cleaning the project after switching scripting backends:
- Delete
Library/ - Delete
obj/andTemp/ - Rebuild from scratch
- Delete
- Avoiding reflection at startup, especially
GetTypes() - Lazy‑loading reflection only when needed
- Ensuring no stale assemblies remain, especially in:
Assets/Plugins/Android/Assets/Plugins/
- Disabling domain reload optimizations during debugging
- Testing on multiple devices to catch OEM‑specific GC behavior
Why Juniors Miss It
Junior developers often overlook this because:
- The crash appears random and unrelated to their code
- Logcat shows native GC crashes, not managed exceptions
- They assume Mono and IL2CPP behave similarly, which is not true
- They rely on reflection-heavy patterns without understanding memory cost
- They don’t realize Android caches app state, causing stale GC heaps
- They trust that switching scripting backends is harmless, when it often requires a full project clean
If you want, I can help you rewrite your startup logic to avoid reflection-heavy calls that trigger Mono GC instability.