The total physical memory of 3GB iOS devices is very tight. After system memory occupation, the available memory for third-party apps is extremely insufficient, leading to frequent OOM termination.
Enabling or disabling JavaScriptCore causes a huge difference in app memory crash threshold. Without JSC, the app will crash at only 1.52GB memory usage, which severely restricts normal business running.
The common 4MB and 8MB memory allocation used in our project falls into the high memory fragmentation range defined by libmalloc, resulting in the lowest memory crash limit and worst stability.
Our core business relies heavily on JSC environment for JS interaction, resource rendering and dynamic logic execution. We cannot shut down JSC, but 3GB devices still face serious memory shortage even with JSC enabled.
Mass users with 3GB RAM old iOS devices suffer from frequent app crashes, freeze, background kill and loading failure, which badly damage user experience and product reputation.
We have finished all app-level memory optimization: adjust allocation size, optimize memory release, reduce resident memory, but still cannot break through the fixed memory crash limit. There is no effective solution on application layer.
We sincerely ask Apple official engineers to provide official suggestions, system-level memory tuning solutions and JSC memory scheduling optimization guidance to solve the memory limit bottleneck on 3GB RAM iOS devices.
Memory Crash Test Table on 3GB RAM iOS Devices
What are the details of the test you’re actually running here?
If you're just calling malloc() without touching that new memory and then looping until you crash, then what you're hitting isn't any physical memory limit but is actually the address space. That would match up with the numbers you're posting, both because of the general limit "range" and because I believe linking against JavaScriptCore may cause the system to increase that limit. Note that this limit is specifically about address space, NOT actual memory use.
That leads to here:
Without JSC, the app will crash at only 1.52GB memory usage, which severely restricts normal business running.
Keep in mind that hitting the address space limit basically requires using address space algorithms or techniques like memory mapping large files. You can't really hit it with "standard" memory usage patterns (malloc-> touch-> free) because actual physical memory available is generally WELL below 1 GB, particularly in real-world conditions where other apps like VoIP or music playback have HIGHER priority than the foreground. In any case, my general advice is to avoid or limit these techniques, as large-scale usage tends to hit... exactly the limit you're seeing.
In terms of the specific "dip" at 8 & 4 MB, but the obvious solution would be to use your own allocator instead of malloc— either by sub-allocating out of large malloc allocations, or by directly allocating memory using a lower-level API like vm_allocate or mmap. Either approach would allow you to get closer to the other limits you've listed; however, no approach is going to let you go significantly beyond the limits you've found.
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Thanks for your detailed analysis. First, to clarify our testing setup: we write and touch allocated memory after malloc, so the failures we encounter in production are not caused by pure untouched allocations exhausting virtual address space. We fully understand the distinction between virtual address space limits and physical memory constraints. On 3GB iOS devices, our app hits compound restrictions: linking JavaScriptCore expands the overall process virtual address space quota, yet JSC itself consumes nearly 1GB of that address space footprint. This large baseline reservation of JSC only becomes observable under Instruments memory profiling starting from iPadOS 26.4. Earlier OS releases did not expose this consumption in profiling tools. We also observed that this reserved size varies based on device physical RAM: it occupies roughly 1GB on 3GB devices, and expands to approximately 2GB on devices with larger memory capacity. We are continuing to complete verification on more hardware models. Without JSC linked, the app crashes at approximately 1.52GB memory consumption, which creates critical obstacles for core business workflows. Our project extensively performs 4MB and 8MB memory allocations. These allocation sizes fall into libmalloc’s high-fragmentation bucket, further lowering the effective crash threshold and worsening runtime stability. Unfortunately, we cannot remove JSC. Our core capabilities including JS bidirectional communication, resource rendering and dynamic business logic all depend on the JSC runtime. Even with JSC linked to unlock a higher virtual address limit, memory pressure remains severe on 3GB RAM hardware. A large group of users with these older devices continuously experience crashes, freezes, background Jetsam termination and loading failures. We have completed all feasible application-layer optimizations: adjusting allocation granularity, optimizing memory release paths and reducing resident memory. We have also evaluated building a custom allocator via bulk large allocations or low-level vm_allocate/mmap calls as you suggested. However, we recognize such improvements cannot fundamentally break through the hard system limits we have observed. Therefore we sincerely hope to obtain further official guidance:
- Tuning options for JavaScriptCore to cut its baseline virtual address reservation and runtime memory overhead;
- Recommended practices to mitigate libmalloc fragmentation for frequent 4MB/8MB allocations;
- Any available system-level memory scheduling advice for memory-heavy applications running on 3GB iOS devices.
This large baseline reservation of JSC only becomes observable under Instruments memory profiling starting from iPadOS 26.4. Earlier OS releases did not expose this consumption in profiling tools.
We also observed that this reserved size varies based on device physical RAM: it occupies roughly 1GB on 3GB devices, and expands to approximately 2GB on devices with larger memory capacity. We are continuing to complete verification on more hardware models. Without JSC linked, the app crashes at approximately 1.52GB memory consumption, which creates critical obstacles for core business workflows. Our project extensively performs 4MB and 8MB memory allocations. These allocation sizes fall into libmalloc’s high-fragmentation bucket, further lowering the effective crash threshold and worsening runtime stability. Unfortunately, we cannot remove JSC. Our core capabilities including JS bidirectional communication, resource rendering, and dynamic business logic all depend on the JSC runtime. Even with JSC linked to unlock a higher virtual address limit, memory pressure remains severe on 3GB RAM hardware. A large group of users with these older devices continuously experience crashes, freezes, background Jetsam termination, and loading failures. We have completed all feasible application-layer optimizations: adjusting allocation granularity, optimizing memory release paths, and reducing resident memory. We have also evaluated building a custom allocator via bulk large allocations or low-level vm_allocate/mmap calls as you suggested. Therefore, we sincerely hope to obtain further official guidance:
- Tuning options for JavaScriptCore to cut its baseline virtual address reservation and runtime memory overhead;
I wouldn't expect this to be "tunable" by your code; however, I do expect the current reservation to shrink. Having said that, I need to add a comment here:
Even with JSC linked to unlock a higher virtual address limit
JSC increases the memory limit because its unique requirements necessitate it. While that may also provide some additional memory to your application, that is an accidental side effect, not an intentional goal. To put that more directly, this is happening:
Without JSC linked, the app crashes at approximately 1.52GB memory consumption
...because your app is simply using too much memory. Any memory JSC provides above that wasn't/isn't something you should necessarily be relying on.
Recommended practices to mitigate libmalloc fragmentation for frequent 4MB/8MB allocations;
Any available system-level memory scheduling advice for memory-heavy applications running on 3GB iOS devices.
There isn't a lot of advice I can offer here, as the specifics really depend on the details and nature of your application. For example, if you're specifically doing large allocations at fixed, predictable sizes, then a fairly simplistic "bulk" sub allocator can provide substantial gains. You can reserve enough address space that all of your sub allocations will always come from contiguous memory, sub-allocate at fixed "slots", and then track freed objects using a linked list and/or by "marking" blocks at deallocation. This eliminates the overhead malloc uses for individual allocations and, unless your usage patterns are highly erratic, should reduce overall fragmentation.
However, that only works if your allocations are consistently fixed size. If they're not, then you'll end up either wasting space within slots or reinventing malloc, neither of which are ideal.
Of course, as you noted, any approach like this can only do so much:
However, we recognize such improvements cannot fundamentally break through the hard system limits we have observed.
...so the ultimate answer here may mean reducing actual memory usage, typically by shifting data out of memory and (typically) into files.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware