Skip to main content

4 posts tagged with "Memory"

View All Tags

Production Memory Diagnostics: Tracking Leaks, OOMs, and Jetsam in Mobile Apps

Published: · 13 min read
Robin Alex Panicker
Cofounder and CPO, Appxiom

In local development, diagnosing a memory leak is a solved problem.

You fire up Android Studio Memory Profiler or Xcode Instruments Allocations, attach a debugger, record a trace, inspect the object graph, or let LeakCanary dump a heap snapshot. Within minutes, you can pinpoint the exact retain cycle or static reference pinning an Activity or UIViewController in RAM.

In production, that luxury vanishes.

Running a heap dump in a live production app is an engineering non-starter. A standard Android heap dump (Debug.dumpHprofData()) freezes the main thread for 10 to 30 seconds, creates an uncompressed 500MB+ .hprof file on user storage, risks exfiltrating unredacted Personally Identifiable Information (PII), and frequently triggers operating system watchdogs. On iOS, you cannot capture memory graphs or attach Instruments to App Store binaries at all.

Even worse, Out-Of-Memory (OOM) events are the most silent killers in mobile engineering.

When an iOS device runs out of physical RAM, Apple's Jetsam kernel daemon terminates your process with a SIGKILL. On Android, the LowMemoryKiller daemon (lmkd) sends a silent kill to reclaim pages.

Neither operating system gives your application time to execute an uncaught exception handler. Traditional crash reporting SDKs register zero crash logs. To your backend and analytics dashboards, the user simply disappeared - or worse, the app restarts cleanly on next launch, disguising catastrophic memory exhaustion as ordinary user session churn.

Here is how modern engineering teams move beyond development-only profilers to implement Production Memory Diagnostics - tracking runaway allocations, memory leaks, and OS termination events without degrading app performance.

Production Memory Diagnostics is the practice of continuously monitoring runtime memory health (Resident Set Size, dirty physical footprint, system memory pressure warnings, and screen-by-screen memory deltas) on live user devices without capturing heavy heap dumps. By correlating memory growth with user journeys and lifecycle events, production diagnostics identify retain cycles, native cache bloat, and LowMemoryKiller (LMK)/Jetsam terminations before they cause silent churn.

The Anatomy of Silent Memory Kills​

To diagnose production memory issues, you must first understand how modern mobile operating systems enforce memory limits and terminate processes.

1. iOS: The Jetsam Mechanism (FOOM)​

iOS devices operate without a traditional disk swap file. To manage memory pressure, iOS relies on memory compression and aggressive process termination enforced by the kernel daemon memorystatus (commonly known as Jetsam).

When an app's dirty physical memory footprint crosses the system limit (which varies by device model and memory tier - often between 1.2GB and 2.2GB on modern iPhones, but as low as 350MB on older iPads or memory-constrained extensions), Jetsam sends an uncatchable SIGKILL.

The Virtual Memory Trap​

Many developers attempt to track memory in Swift by reading task_basic_info.virtual_size. This metric is virtually useless: virtual size includes clean mapped read-only files (such as .dylib frameworks and asset catalogs) that the OS can evict at zero cost.

Jetsam only cares about Physical Footprint - dirty pages that cannot be compressed or paged out.

Low-Overhead Physical Footprint Telemetry in Swift​

To capture actual memory pressure in production, you must query the Mach kernel's task_vm_info:

import Foundation
import MachO

public struct MemoryDiagnostics {

/// Returns the exact physical memory footprint (in Megabytes) tracked by Jetsam.
public static func getPhysicalMemoryFootprintMB() -> Double? {
var vmInfo = task_vm_info_data_t()
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<integer_t>.size)

let result = withUnsafeMutablePointer(to: &vmInfo) { ptr in
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count)
}
}

guard result == KERN_SUCCESS else { return nil }

// phys_footprint is the exact metric used by iOS Jetsam to trigger kills
let footprintBytes = Double(vmInfo.phys_footprint)
return footprintBytes / (1024.0 * 1024.0)
}

/// Queries remaining memory available to the process before system intervention
public static func getAvailableMemoryMB() -> Double {
return Double(os_proc_available_memory()) / (1024.0 * 1024.0)
}
}

By querying os_proc_available_memory() upon entering critical funnels, your app can dynamically evict caches before Jetsam strikes.

2. Android: The LowMemoryKiller (LMK) & The OOM Paradox​

On Android, memory management is split across two layers:

  1. The ART (Android Runtime) Java/Kotlin Virtual Heap: Subject to garbage collection with an upper limit defined by Runtime.getRuntime().maxMemory().
  2. The Linux Kernel & Native Allocations: Governed by the lmkd daemon, which monitors system-wide RAM and kills processes based on their oom_score_adj priority.

The "Straw That Broke the Camel's Back" Fallacy​

When an Android app crashes with a fatal java.lang.OutOfMemoryError, developers almost always misdiagnose the root cause by staring at the stack trace:

java.lang.OutOfMemoryError: Failed to allocate a 32784 byte allocation with 12582912 free bytes and 12MB until OOM
at java.lang.StringFactory.newStringFromChars(StringFactory.java:328)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.example.app.ui.cart.CartAdapter.onBindViewHolder(CartAdapter.kt:42)

The stack trace points to a 32KB string allocation inside CartAdapter. Developers spend days optimizing StringBuilder usages in the cart screen.

In reality, the cart adapter was blameless. Five minutes earlier, a background image pipeline leaked five 50MB uncompressed Bitmap instances because an Activity failed to unsubscribe from an EventBus. The heap was already at 99.8% capacity; the 32KB string just happened to be the unlucky allocation that tripped the wire.

Production Memory Telemetry in Kotlin​

To monitor real memory consumption and intercept OS warnings before LMK strikes:

package com.appxiom.diagnostics

import android.app.ActivityManager
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.res.Configuration
import android.os.Debug
import android.os.Process

class ProductionMemoryObserver(private val context: Context) : ComponentCallbacks2 {

fun captureMemorySnapshot(): MemorySnapshot {
val runtime = Runtime.getRuntime()
val javaHeapUsedMB = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024)
val javaHeapMaxMB = runtime.maxMemory() / (1024 * 1024)

// Native allocation tracking (Bitmaps, C++ libs, OpenGL/Vulkan buffers)
val nativeHeapAllocatedMB = Debug.getNativeHeapAllocatedSize() / (1024 * 1024)

// PSS (Proportional Set Size): The most accurate representation of app footprint
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)

return MemorySnapshot(
javaHeapUsedMB = javaHeapUsedMB,
javaHeapMaxMB = javaHeapMaxMB,
nativeHeapAllocatedMB = nativeHeapAllocatedMB,
systemAvailableMemMB = memoryInfo.availMem / (1024 * 1024),
isSystemLowMemory = memoryInfo.lowMemory
)
}

override fun onTrimMemory(level: Int) {
when (level) {
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
// The app is running, but device is critically low on memory.
// Evict non-essential bitmap caches immediately.
}
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
// UI moved to background. Free heavy UI resources to lower LMK kill priority.
}
}
}

override fun onConfigurationChanged(newConfig: Configuration) {}
override fun onLowMemory() {
// Legacy callback: system is running out of memory
}
}

data class MemorySnapshot(
val javaHeapUsedMB: Long,
val javaHeapMaxMB: Long,
val nativeHeapAllocatedMB: Long,
val systemAvailableMemMB: Long,
val isSystemLowMemory: Boolean
)

3. Flutter & React Native: The Split-Heap Blindspot​

Cross-platform frameworks introduce a dangerous architectural duality: The VM Heap vs. The Native Graphics Engine.

In Flutter, Dart manages object lifecycle on the Dart VM Heap. However, images, shaders, Skia/Impeller render pipelines, and texture backings live in native C++ memory.

// DANGEROUS: Decodes a 4032x3024 12MB camera photo into raw RGBA memory
// Dart heap allocates only a tiny metadata pointer (~120 bytes)
// Skia / Native heap allocates 4032 * 3024 * 4 bytes = ~48.7 MB of uncompressed bitmap!
Image.network(
"https://cdn.example.com/products/4k_photo.jpg",
width: 150,
height: 150,
)

// SAFE: Resizes bitmap during decode step, saving 95% native RAM
Image.network(
"https://cdn.example.com/products/4k_photo.jpg",
width: 150,
height: 150,
cacheWidth: 300, // 2x density for 150dp
cacheHeight: 300,
)

If a list view displays 30 unconstrained images, the Dart VM reports a healthy 45MB heap, while the native iOS/Android process footprint spikes past 1.4GB. Within seconds, Jetsam or LMK kills the app. A standard Dart crash handler will never capture a stack trace.

Why Traditional Monitoring Tools Fail in Production​

CapabilityLocal Dev Profilers (Xcode / Android Studio)LeakCanaryStandard Crash Reporters (Crashlytics, Sentry)Production Memory Diagnostics (Appxiom)
Run in Production App Store / Play Store❌ No (Debug builds only)❌ Dangerous (5-30s freeze, PII risk)✅ Yes✅ Yes
Zero UI Jitter / Overhead❌ High overhead❌ High pause time✅ Low✅ Zero-overhead passive telemetry
Capture Jetsam / FOOM Kills❌ N/A❌ N/A❌ Misses SIGKILL terminations✅ Correlates last known footprint
Capture Native vs Managed Heap✅ Yes❌ Java heap only❌ None✅ Complete physical footprint
Attribution to User Action⚠️ Manual profiling session❌ Static reference path only❌ Single stack frame at crash✅ Full historical Activity Trail

The Appxiom Solution: Memory Activity Trail​

Diagnosing production memory leaks does not require capturing every allocated byte in RAM. It requires understanding Memory Delta Attribution.

A leak is not defined by high memory usage; it is defined by memory that is allocated during a user journey and fails to reclaim after that journey terminates.

Correlating Memory Deltas with User Journeys​

The Appxiom SDK records lightweight memory snapshots attached to each discrete step in the Activity Trail. By comparing memory footprint across screen transitions, network invocations, and user interactions, Appxiom calculates the exact memory delta ($\Delta$) per step.

Activity Trail Trace: Diagnosing a Retain Cycle in Production​

Consider this telemetry log captured by Appxiom immediately before an iOS Jetsam event:

TimestampScreen / Lifecycle EventAction / PayloadPhysical RAMMemory Delta ($\Delta$)Status
14:22:01.100AppLaunchCold start from springboard98 MB+98 MBNormal
14:22:04.250HomeScreenFeed fetch (GET /v2/feed - 200)132 MB+34 MBNormal
14:22:15.800CatalogScreenGrid populated (48 products)168 MB+36 MBNormal
14:22:28.400ARProductViewerCamera initialized + 3D model load495 MB+327 MBHigh Allocation
14:22:42.100HomeScreenUser navigates BACK to Home490 MB-5 MB (LEAK ALERT)CRITICAL
14:22:49.000CartScreenUser taps Cart icon510 MB+20 MBWarning
14:22:53.600CheckoutScreenTap "Pay Now"542 MB+32 MBJETSAM KILL

The Root Cause Revealed​

Without Appxiom, your team would be looking at an unhandled session termination during checkout. You might assume the payment gateway SDK crashed.

With the Appxiom Activity Trail, the bug is obvious in five seconds:

  1. When the user entered ARProductViewer, RAM jumped from 168 MB to 495 MB (+327 MB).
  2. When the user navigated back to HomeScreen, memory should have dropped back toward 168 MB. Instead, memory remained pinned at 490 MB (only -5 MB reclaimed).
  3. The ARProductViewer controller or its Metal texture buffers had a strong retain cycle (e.g., an uncancelled closure capture or unreleased camera delegate), keeping 320MB of native resources permanently alive in background memory.
  4. When the user reached Checkout, the normal memory requirement of the payment sheet pushed the app over the device's Jetsam threshold.

Connecting Memory Diagnostics to Business Health​

Memory diagnostics is not merely an infrastructure concern; it directly impacts revenue and customer retention.

1. Goal Friction Impact (GFI)​

When an app terminates due to an OOM during an onboarding or checkout flow, users rarely re-enter payment credentials - they abandon the transaction. Appxiom's Goal Friction Impact (GFI) quantifies the precise financial loss attributable to memory degradation, allowing engineering leads to prioritize memory fixes with hard ROI data.

2. Quality Score (QS)​

Rather than relying solely on the deceptive crash-free rate, Appxiom calculates a holistic Quality Score (QS). Devices experiencing excessive memory pressure warnings (TRIM_MEMORY_RUNNING_CRITICAL or didReceiveMemoryWarningNotification) trigger a score penalty, giving you an early warning system days before crashes escalate.

3. Version Analytics​

During staged rollouts, Appxiom's Version Analytics compares memory distributions across app versions:

  • v2.4.0: 90th percentile physical footprint = 185 MB.
  • v2.5.0: 90th percentile physical footprint = 340 MB (+83% regression detected in first 2% rollout).

You can halt the release before the regression compromises millions of devices.

Actionable Production Memory Hardening Checklist​

Follow these engineering best practices to eliminate memory leaks and build resilience against LMK and Jetsam:

iOS (Swift / SwiftUI)​

  • Enforce Weak Capture in Async Closures: Audit all Task { [weak self] in ... } blocks and delegates to prevent retain cycles holding view controllers in memory.
  • Drain Autorelease Pools in Heavy Loops: Wrap batch data transforms or file processing in autoreleasepool { ... } so intermediate objects are reclaimed immediately.
  • Listen to Memory Pressure: Implement UIApplication.didReceiveMemoryWarningNotification to flush image memory caches and clear transient view states.
  • Audit Background Tasks: Ensure background tasks end promptly with UIApplication.shared.endBackgroundTask(). Pinned background allocations are the #1 trigger for Jetsam priority kills.

Android (Kotlin / Jetpack Compose)​

  • Observe ComponentCallbacks2: Evict Glide/Coil bitmap caches upon receiving TRIM_MEMORY_RUNNING_CRITICAL or TRIM_MEMORY_RUNNING_LOW.
  • Beware of Static View/Context References: Never store a Context or View in a companion object, singleton, or long-lived coroutine scope.
  • Bind Coroutines to Lifecycle: Use viewLifecycleOwner.lifecycleScope in Fragments and rememberCoroutineScope() in Compose to ensure allocations cancel with UI teardown.
  • Optimize Large Bitmaps: Always decode images using BitmapFactory.Options.inSampleSize or rely on modern vector drawables and hardware-accelerated rendering.

Cross-Platform (Flutter / React Native)​

  • Constrain Image Cache Dimensions: Never use unconstrained Image.network() without specifying cacheWidth and cacheHeight.
  • Dispose Controllers Explicitly: Always invoke .dispose() on AnimationController, TextEditingController, FocusNode, and ScrollController in the widget dispose() lifecycle.
  • Verify Native Bridge Cleanup: Ensure custom native platform channels (MethodChannels) release native listeners and texture handles when widgets unmount.

Eliminate Memory Blindspots with Appxiom​

You cannot fix what you cannot measure, and you cannot debug production memory with development-only profilers.

By combining low-overhead physical footprint telemetry with the Appxiom Activity Trail, your engineering team gains complete visibility into every memory spike, retain cycle, and silent OOM kill across iOS, Android, and Flutter.

Stop losing users to invisible memory kills. Explore Appxiom's Activity Trail or start monitoring your mobile applications today.

How to Detect Abnormal Memory Usage and Memory Spikes in iOS Apps

Published: · Last updated: · 4 min read
Don Peter
Cofounder and CTO, Appxiom

Though it is often advised to keep the app’s memory footprint as low as possible, developers tend to overlook this aspect during app development. As a result, the elements that consume the app’s memory go unchecked, which leads to the app consuming an abnormal amount of the device’s memory.

When the iOS app consumes a considerable amount of allocated memory, it leads to an out of memory exception, resulting in an app crash and affecting user experience. This makes it important for developers to be notified about abnormal memory usages in both development and production environments at the earliest.  

Abnormal memory usage occurs when unused memory is not reclaimed properly or because of inefficient object reuse.​

With Appxiom, abnormal memory usage can be detected at the earliest and developers get to fix the issue fast.Appxiom is an automated bug reporting tool that is capable of detecting abnormal memory usages and memory spikes in iOS apps.

Using Appxiom to track memory issues in iOS​

With a simple 3-step integration process, Appxiom framework for iOS detects and reports abnormal memory usage in both development and production environments. Please visit our documentation page for more details. 

Tracking memory usage with Activity trail​

Appxiom reports high memory usage when the iOS app consumes more than 30% of the physical memory. The bug report will have data points like activity trail with memory usage patterns along with device and device state information, which will aid developers to reproduce the bug. 

Activity Trail from an issue reportActivity trail consists of a list of events that occurred one minute prior to the issue occurrence. The app events along with the percentage of free memory available at each point are displayed in a chronological order. 

Components of Activity trail​

Section of Activity Trail

Activity​

The activity segment has details of the app events. The user event will have two components separated by a colon

  •  The name of the class in which the event gets reported

  •  The textual description of the event.

Timestamp​

The timestamp segment has details related to the time at which the event gets recorded. 

Free Memory​

The memory segment also contains the percentage of free memory available for the app at the time of the event. 

How Appxiom helps in finding the root cause​

Abnormal memory usage reported by AppxiomLet’s consider a use case as indicated in the above screenshot. Here, Appxiom has detected high memory usage in an iOS app. The issue is that the app’s memory usage crossed 2587213824 bytes, which is more than 30% of the physical memory size i.e 8589934592 bytes. 

With an in-depth evaluation of the activity trail section in the issue report, we understand that while loading the first banner image, the free memory was around 79%. But when the final banner image was loaded, the memory dipped down to almost 71%. So with each banner image, the amount of free memory available goes down. By analysing the issue further in code, we can come to the conclusion that the root cause for the free memory dip was that the allocated objects for banner images are not released. 

This scenario may lead to an app crash. In order to avoid the app from crashing, Appxiom detects and reports whenever the app consumes more than an ideal amount of free memory.

Activity trail helps developers to pinpoint where exactly the issue is so that necessary changes can be made in the code and a patch version be released fast. 

Custom Activity Trail events​

Apart from default events, Appxiom allows developers to set custom app events in activity trail (refer our Objective-C and Swift documentation) with an API, which can be called anywhere in the code. These custom events will be recorded and shown along with default app events in the activity trail section in Appxiom dashboard. 

Along with high memory usage and spikes, Appxiom also detects memory leak in iOS apps. To know more read our blog Detect memory leaks in iOS

Apart from memory-related issues, Appxiom capable of detecting and reporting crashes, API call issues, frame rate issues, ANR, function failures, exceptions, delays and custom issues.  Appxiom is available for iOS as Objective-C and Swift Frameworks, watchOS as Objective-C and Swift Frameworks, and Android as Java and Kotlin SDKs platforms. 

Visit appxiom.com to know more about Appxiom. Detailed documentation is available at docs.appxiom.com

How to Detect Memory Leaks in watchOS Apps That Are Created Using Objective-C or Swift

Published: · Last updated: · 2 min read
Don Peter
Cofounder and CTO, Appxiom

Memory leak is a critical bug in watchOS apps, that often go unnoticed during development. Memory leaks can lead to EXC_BAD_ACCESS error and will manifest as app crashes in watchOS.

Memory leaks are caused when unused objects are not cleared from memory. It slows down the app, which affects the experience of the user. So detecting and fixing memory leaks as fast as possible is important for maintaining good user experience and thus retaining users. Tools like XCode Leaks Instrument available for watchOS will help developers detect memory leaks during the development of apps.

There are situations where developers tend to miss figuring out leaks during development. Such leaks are very likely to manifest in production.

This is where Appxiom come in. Appxiom watchOS framework monitors InterfaceControllers for retained objects and reports it in real-time.Appxiom detects memory leaks in watchOS apps just by extending InterfaceControllers from ObservableInterfaceController, a custom InterfaceController class provided by Appxiom.

Objective-c Integration​

@interface ProfileController : ObservableInterfaceController

@end

Swift Integration​

class ProfileController: ObservableInterfaceController {

}

Find the detailed documentation for Objective-C & Swift, on how to track memory leaks.

Appxiom reports memory leaks with relevant data points like leaked InterfaceController name, activity trail, device info, and device state info. This will help the developers to reproduce the memory leaks and find the root cause.

Here is a screenshot of the memory leak detected by Appxiom in a live watchOS app.

Memory Leak in watchOS appsMemory leak bug report provides the name of the InterfaceController where the leak occurred along with the total memory consumed by the app.

Apart from memory leaks, Appxiom also detects API call issues, frame rate issues, abnormal memory usage, crashes, exceptions, function failures & delays along with custom issues. For knowing more about how Appxiom detects memory leaks in iOS apps, please read our blog post Detect memory leaks in iOS (Objective-C & Swift).

Visit appxiom.com to know more about Appxiom. Detailed documentation is available at docs.appxiom.com.

Related Blog Posts

  • How to detect memory leaks in Android

  • Detect memory leaks in iOS (Objective-C & Swift)

Detect Memory Leaks in iOS (Objective-C & Swift)

Published: · Last updated: · 2 min read
Don Peter
Cofounder and CTO, Appxiom

Memory leak is one of the most dreadful issues developers fear. It's even bigger challenge for mobile app developers as very often the device where the issue manifests is out of their reach. Most of the time developers will not even get crash reports caused by memory leaks. They usually manifest in the form of EXC_BAD_ACCESS.

Tools like XCode Leaks Instrument for iOS help developers identify such leaks during development time itself. But what is the guarantee that all such leaks are identified and all possible use-cases are tested properly? There is no guarantee, and that is the hard truth.

So what if the developer and the tester missed some use cases and there are memory leaks happening in production? Here is where Appxiom help the developer to get notified about the issue in real-time and that too with as much relevant data as possible so that (s)he can reproduce the bug. Appxiom auto detects memory leaks.

Appxiom report bugs to the dashboard with important data such as activity trail, device info, and device state info. This will help the developer to be aware of the leaks happening in their app that is running in their users' devices and also helps them to reproduce the memory leak.

Here is a screenshot of such a leak detected by Appxiom in a live app.

With such reports, developers are notified of the occurrence of a memory leak in a ViewController. Developers can then use the Leaks instrument to drill down the corresponding ViewController for the leak.

Apart from memory leaks, Appxiom detects API call issues, frame rate issues, abnormal memory usage, crashes, exceptions, function failures & delays. It also has the capability to report custom issues. The memory leak detection is available in our iOS Framework starting from version 3.1.0. For knowing more about how Appxiom detects memory leaks in Android apps, read the blog How to detect bugs in Android.

Visit appxiom.com to know more about Appxiom. Detailed documentation is available at docs.appxiom.com.

Related Blog Posts

  • How to detect memory leaks in Android

  • How to Detect Memory Leaks in WatchOS apps that are created using Objective-C or Swift