← All topics/Performance & optimization

Question 4

Memory growth and OOMs

The app doesn't obviously leak, but memory rises until the process is killed. How do you track down the cause?

Follow-ups

  • Leaks vs high but valid usage?
  • Which tools and patterns help most?

Answer outline

Memory that climbs until the system kills the process is usually growth the app allowed, and only sometimes a classic leak. Out-of-memory (OOM) kills happen when the footprint crosses the per-app limit. The process dies without a normal crash report, so you have to catch the growth while it's happening.

Reproduce it first. Run the Allocations instrument, mark a generation before and after a navigation loop, and repeat the loop several times. Whatever survives every generation is your suspect list, and each suspect falls into one of three buckets:

  1. 1.Retain cycles (true leaks): closures capturing self, strong delegate references, and NotificationCenter or key-value observing (KVO) observers that were never removed. Confirm them with the Leaks instrument and Memory Graph reference chains, then break them with a weak or unowned reference.
  2. 2.Valid but unbounded growth: arrays of full models that never shrink, duplicate decodes of the same blob, singletons holding screen data, and video buffers. Each needs a policy, such as eviction, weak references, streaming, or pagination.
  3. 3.Overall footprint: the Xcode memory gauge shows the total the system judges you by, including decoded image buffers that Allocations doesn't attribute to your objects. A big gap between the gauge and the Allocations total points at images or graphics memory.

Leaks and high but valid usage look identical on the memory gauge, and they need different fixes. In ARC code a leak is usually a retain cycle, objects keeping each other alive after everything else let go, so you break the cycle. Valid growth is memory something still holds on purpose, and it needs a policy that lets go.

Principles

  • Prove retention with Memory Graph paths back to a root instead of guessing from heap size alone.
  • Cap every cache, purge on memory warnings, and exercise the low-memory path on purpose before release.
  • Repeat the action several times, because a leak grows on every pass while a warm-up cost settles after the first.
  • Decoded images are the usual culprit, so check decode sizes before you go hunting for retain cycles.