Android Memory Management and Process Death

In this post I’ll describe one of the core memory management mechanisms inside Android OS, which is responsible for so-called “process death” phenomenon. This mechanism affects all Android applications and, as such, must be understood by professional Android developers.

Memory Limitations on Mobile Devices

Let’s start by comparing mobile devices to desktop PCs.

Mobile devices are physically small, have relatively low amount of relatively slow RAM memory and should be power-efficient to prolong battery life. However, despite all these limitations, mobile applications have memory demands which are comparable to their PC counterparts. For example, comparing the same application on my mobile phone and a desktop, I usually see a factor of 2-4 in memory consumption. But my desktop has 8 times more memory than my mobile phone and this memory is much faster too.

Furthermore, users use more applications on mobile devices. In their latest report, AppAnie indicated that Android users engage with 10 applications per day on average. I couldn’t find analogous statistics for PCs, but I would estimate that this number for PCs is at most half of that for mobile. I myself am a professional sofware developer and spend most of my day in front of the computer, but I don’t use more than 5-6 programs a day.

So, users use more apps on mobile devices, and each of those consumes a higher percentage of the total available RAM. This is an issue that needs to be addressed. There are several approaches that could be employed.

Single-Application Operating System

The simplest approach would be to allow just a single application to be “alive” at any given instant. Once the user switches to another application, the previous one is shut down and cleared from memory.

While this approach is the simplest to implement, such an OS would be horrible in terms of user experience. In particular, users would face three problems:

  1. Switching between application would take much time because the new app would need to be started from scratch and loaded into memory.
  2. The state of the previous application would be lost.
  3. No application would be able to provide background functionality.

It might be the case that MS-DOS was the last OS that employed this approach, which is simply inpractical for any modern OS.

Multiple-Applications Operating System

Allowing multiple applications to be executed concurrently is what operating systems do in general. However, when multiple applications are alive, each of them consumes some memory. Due to the reasons discussed earlier, mobile devices will quickly run out of memory as users switch between applications. Therefore, management of memory consumed by applications becomes a necessity.

Depending on the degree of user involvement, memory management can be automatic, semi-automatic, or manual. Android implements an automatic memory management system in which users are not involved in routine operations. This memory management scheme boils down to two aspects:

  1. Android limits the amount of memory a single application can consume and terminates violators.
  2. Android keeps track of total memory consumption and terminates applications when it runs low on memory.

The component that OS uses in order to terminate applications when memory consumption becomes too high is called “low memory killer”. The exact algorithm that low memory killer uses to determine which apps should be killed is not documented at all. Maybe it can be reverse engineered form AOSP source code, but I didn’t go that far myself, and I doubt that this information is relevant to most applications.

Using this approach Android allows for multiple applications to reside in memory and, if needed, even execute in background. If application misbehaves and attempts to claim more memory than allowed, then Android will kill it. If the total memory consumed by all applications grows beyond some threshold, then Android low memory killer will kick in and terminate some of them. This way users get nice and smooth experience of switching between applications because many of them remain in memory and are simply brought to foreground when “launched”.

All good, but there is still one “small” problem: when low memory killer terminates applications, the non-persistent state of these applications is lost.

For example: a user could have been writing a long body of text in application and then had to switch to some other application. If low memory killer terminates the now backgrounded application before the user gets back to it, then user’s input might be lost. That’s bad user experience.

In some cases, clearing the non-persistent state might be acceptable and even desirable, but, in other cases, the non-persistent state must not be lost (e.g. user input). This issue needs to be addressed in some way.

Multiple-Applications Operating System With “Save and Restore”

So, each application might be terminated by Android’s low memory killer at some point in time and lose its non-persistent state. However, part of that state mustn’t be lost. The solution to this problem is to save the critical parts of non-persistent state in some storage right before the application is killed, and then restore the saved state when the application is brought back to life.

This way, even though the application was terminated, when a user will get back to the application later, its overall state will be equivalent to the state it had when the user left it. In other words – the user will not be aware of the fact that the application was terminated due to system low memory conditions between interactions (except for a bit longer launch time).

This flow of persisting parts of non-persistent state before applications termination and restoring it later is called “save and restore”. In Android, since save and restore flow is associated with process termination by low memory killer, it’s often referred to as “process death”, “process kill”, “application kill” and, probably, several other similar terms.

Technically speaking, I don’t know whether Android actually stores applications’ saved state in persistent storage (e.g. on disk). My guess is that this state is still kept in memory, but this memory belongs to Android OS and is not accessible to low memory killer. In practice, however, when looked from application’s perspective, it is irrelevant whether the saved state is actually persisted or not. The only thing that matters for the application is that it will have a chance to restore its previous state next time the user interacts with it.

The crucial point here is that save and restore flow is not handled automatically by Android on its own. The operating system controls when and how the flow is being executed, but it has no means to know which exactly parts of application’s state should be saved. Therefore, it is up to applications’ developers to save the state when the system initiates save and restore flow and restore it later when the application is re-created.

Save and Restore is Very Common

One myth that many Android developers believe is that save and restore flow doesn’t happen often. Therefore, some developers say, it’s alright to be less attentive to the nuances and implementation details of save and restore in your apps.

But save and restore is very common. I make this claim confidently because I tested it.

On my Galaxy S4 with 2GB or RAM, I can easily make background applications being killed by OS. All it takes is to open 8-10 applications with above average memory consumption. In fact, I reproduced save and restore just now by opening the following 8 apps: Waze, Facebook, Gmail, Gett, Wunderlist, Evernote, Google Maps and the memory monitor in Settings. My phone can’t keep all these applications in memory at once, so only 7 out of 8 can be alive at any instant.

Given the fact that users use 10 applications a day on average, most users who own devices with 2GB of memory or less will experience save and restore on a daily basis. Probably much more than once because the users are also likely switching back and forth between applications. But how many users actually own such devices? The answer is that most of them do.

Take a look at the below charts that I copied form DeviceAtlas Mobile Web Intelligence Report for Q2 of 2017. These charts show the market share for devices having different amounts of RAM:

Most common RAM amounts 1
Most Common RAM Amounts 2

From these charts it is clear that ~40% of the users worldwide have devices with 2GB of RAM. But this stat becomes completely irrelevant when you realize that ~30% of the users have devices with just 1GB of RAM. I don’t have 1GB device for testing, but I would estimate that on such devices low memory killer kicks in after 3-5 applications are launched.

Since ~30% of users have this kind of devices, you should probably develop applications with this number in mind (unless you target a geographic or economic niche of users).

But even this is not the entire picture. This chart from the same report shows the countries with the highest volume of devices having 512MB of RAM:

Share of Phones with 512MB RAM

And that’s the point when we, Android developers, can become legitimately depressed.

Note: these charts do not contain statistics for China.

All in all, it looks like more than 70% of Android users will experience save and restore on a daily basis. But even users who own the latest flagship devices aren’t immune to it. It’s just the matter of starting enough applications until low memory killer will kick in and kill some of them. Therefore, if the application is being used at all, it is probably subject to save and restore very often and needs to support it.

Configuration Changes

At this point I would like to discuss configuration changes just a bit. What I’m going to share with you now is just an opinion of mine and it is not based on any large scale empirical research. Hopefully it will give you an interesting perspective.

Or you can totally skip to the next section because familiarity with this opinion is not required in order to understand the rest of this post.

In the past years there was a hype around optimization of configuration change flow in Android applications, without optimization of save and restore flow. I’m talking primarily about Loaders framework and ViewModel “architectural component” promoted by Google (well, Loaders have already been effectively deprecated, but they were all the hype when I got into Android development).

I find this hype artificially created and very bad for Android users.

In my opinion, it is created artificially because, on average, there is no justification for optimization of configuration changes. Such an optimization should be done on case by case basis, after there is hard data available that suggests that it is required.

Otherwise, it is “premature optimization” all over:

There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified.

Structured Programming with go to Statements; 1974; Donald E. Knuth

I think that this hype is bad for Android users because it wasted time and attention of development community. Instead of concentrating on stuff that really matters, the community is being constantly steered into useless churn by quite impressive Google’s PR budget.

If you go through the above numbers, you will realize that optimizing configuration changes without optimizing save and restore is completely wasteful activity on average.

For example, think about all the applications locked to portrait mode. They will rarely ever experience configuration change, but will still be subject to save and restore flow on a daily basis for the active users.

And just to make it crystal clear – I’m not saying that we should not support configuration change. I’m saying that we should not optimize configuration change flow unless there is hard data that suggests that such an optimization is required.

The above was my attempt to raise your awareness to “premature optimizations” related to configuration change handling.

Let’s get back to save and restore now.

Single Activity Save and Restore

In order to understand how save and restore in Android should be handled with framework’s assistance, it is enough to understand what happens to individual Activities. Fragment’s save and restore is very similar, and Services do not get any assistance from the framework at all, such that with Services you’re on your own.

This last piece always puzzled me – why developers of Android decided not to support semi-automatic save and restore for Services.

Anyway, let’s discuss what happens to Activities.

Right before the application is about to be terminated by low memory killer, each Activity is notified about this with a call to its onSaveInstanceState(Bundle) method.

In fact, it is not exactly guaranteed that the application is going to be killed if this method is called, but we, as professional Android developers, should assume that every single call to this method is our last chance to save the state.

In the body of this method we call through to super implementation, and also put all additional state we are interested in into the provided Bundle.

Why we put “additional state” and not all state?

Because the call to super implementation automatically saves much of the “standard” state for us. This includes the state of all standard sub-classes of View class in the view hierarchy of this specific Activity, the state of Activity’s FragmentManager, etc.

We only need to manually save additional state that Android framework is not aware of.

For example, this method could look like this for Activity in which the user places a complex order:

@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
    super.onSaveInstanceState(outState);

    outState.putBoolean(SAVED_STATE_FIRST_TIME_PROMOTION_SEEN, mFirstTimePromotionSeen);
    outState.putSerializable(SAVED_STATE_ORDER_IN_PROGRESS, mOrderInProgress);
}

(you might wonder why I use Serializable instead of Parcelable – let’s say it is spoiler for another discussion)

If there is no special state that needs to be saved, this method can be omitted.

So, after this method returns we can be rest assured that all the state required for proper re-creation of this specific Activity was saved.

Later, after the application had already been saved and destroyed, and the user launched it again, the system will initiate the restoration of application’s state. It will re-create the Activity that was last shown to the user and initiate restoration of that Activity’s state.

From Activity’s point of view it means that the Bundle which was saved after onSaveInstanceState(Bundle) returned, will be passed into onCreate(Bundle) method.

Then we can use this Bundle to get all the saved state back into the new instance of Activity:

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState != null) {
        mFirstTimePromotionSeen =
            savedInstanceState.getBoolean(SAVED_STATE_FIRST_TIME_PROMOTION_SEEN);
        mOrderInProgress =
            (Order) savedInstanceState.getSerializable(SAVED_STATE_ORDER_IN_PROGRESS);
    }
}

At this point, if you correctly identified all the state that needs to be included in save and restore flow, the state of this Activity should be equivalent to the state of the Activity destroyed by low memory killer.

The user will just get back to the same screen in the same state and will be able to continue whatever he/she was doing like the application has never been terminated.

Save and restore flow of a single Activity is drawn on this UML diagram:

android_save_restore

The “pre-save” instance of the Activity receives calls to onSaveInstanceState(Bundle) and later to onDestroy(), at which point this instance “dies” and can be garbage collected. Some time after that a new instance of the same Activity is created, and the Bundle containing saved state is being passed into its onCreate(Bundle) method.

It is our responsibility to ensure that all the state (except for the “standard” described above) is being properly saved and restored.

Multiple Activities Save and Restore

Hopefully you understand how save and restore operates on a single Activity level now (or already knew it before).

The more interesting and much less documented aspect is how save and restore works in case of multiple activities on the back-stack.

If low memory killer decides to terminate an application with multiple activities on the back-stack, the following will happen:

  1. For each individual Activity on the back-stack onSaveInstanceState(Bundle) will be called.
  2. After the state of each individual Activity is saved, Android will automatically save the sate of the back-stack itself.

The state of the back-stack contains all the information required in order to reconstruct equivalent “chain” of Activities upon future state restoration. This includes the type of the Activities on the back-stack, their order, Intents that started the Activities, etc.

In short: we are responsible for saving the state of each individual Activity; Android saves the relationship between them.

Then the application is killed. At some point in the future the user will launch the application again, and this will result in the following flow:

  1. Android will automatically restore the state of the back-stack.
  2. An instance of the topmost Activity on the back-stack will be created, and its onCreate(Bundle) method will be called with Bundle containing all the saved state for that specific Activity.

Did you notice that something is missing? What about all other Activities on the back-stack except the topmost one?

Turns out that Android doesn’t eagerly instantiate all Activities that were on the back-stack prior to save and restore flow. It instantiates only the topmost one and lets it restore its state, and then does nothing.

Only if the user navigates “back” in back-stack will Android instantiate the previous Activities and allow them to restore their state.

An example will be appropriate now.

Imagine this situation: user launched the app and ActivityA was shown. Then he/she navigated to ActivityB and then to ActivityC. Then left the application.

Now activities back-stack contains three “live” activities, including ActivityC at the top of the stack, which is the one the user saw the last.

Low memory killer kicks in and wants to terminate this application. Android will call onSaveInstanceState(Bundle) for each of the three Activities, automatically save the state of the back stack and allow low memory killer to do its job.

Later the user launches the same application.

Android restores the state of the back-stack and realizes that ActivityC is the topmost one. An instance of ActivityC is created and its onCreate(Bundle) method is called with Bundle containing its saved state.

A record of ActivityC is removed from the back-stack at this point, such that ActivityB becomes the topmost one.

At this point restore flow is completed and the user will be able to interact with the application.

If the user decides to navigate back now, then Android will instantiate ActivityB, let it restore its state and then remove ActivityB record from the back-stack such that ActivityA becomes the topmost one.

Note that even though the user had already been interacting with the application for some time, the act of simple back navigation caused a restore flow to be executed at Activity level.

Now you can try to formulate yourself what will happen if the user navigates back again.

Summary

Due to limited amount of memory on mobile devices and quite high expected demand by applications, low memory killer component had to be introduced. It terminates applications in order to free RAM, which allows the OS to support execution of multiple applications in parallel while not bothering the user with explicit memory management activities. However, in order to prevent state loss upon applications termination by low memory killer, all applications had to support save and restore flow.

After the general discussion of save and restore I tried to convince you (and, hopefully, succeeded) that majority of Android users experience save and restore flow on a daily basis. Probably much more than once a day on average.

Then we discussed the actual implementation of save and restore at the level of an individual Activity, followed by generalization to multiple saved activities on the back-stack.

The interesting realization from this discussion was that save flow is executed in one pass, while restore flow executes “lazily” – one Activity at a time.

That’s all for this post. The introduction is complete.

You heard me right, introduction.

The plan was to write an article about how to actually test that applications properly handle save and restore flow, but then I noticed that the introduction got quite long and decided to extract it into a separate article.

In the next post I will hopefully get to the testing part.

As always, you’re welcome to leave your comments and questions below, and consider subscribing to our newsletter if you liked this post.

Check out my premium

Android Development Courses

5 comments on "Android Memory Management and Process Death"

  1. Great article! I recently have been diving into the complexity of the save-restore flow, but with an emphasis on FragmentActivity that contains a number of attached Fragments with some on the FragmentManager’s backstack. Did you get the change to examine this flow as well? I found that Fragments seem to be reattached in a different order during the save-restore flow then during a configuration change flow and was quite surprised by this difference.

    Reply
    • Hey Ann,
      No, I’ve never encountered the issues you described. However, it doesn’t mean that it didn’t happen. It’s simply unimportant to me.
      The thing is, that if the order of re-attachment is important in your app, then I’d say you did something not good (no offense intended). As I said in my article about Fragments lifecycle, the only thing that keeps me sane when using Fragments is the fact that I never couple lifecycles. Each Fragment is on its own and neither Activity, nor other Fragments care about its state.
      So, while I can’t help you with this issue, I do recommend avoiding inter-dependencies between lifecycles as much as possible.

      Reply
  2. Awesome!
    Especially I like `I think that this hype is bad for Android users because it wasted time and attention of development community. Instead of concentrating on stuff that really matters, the community is being constantly steered into useless churn by quite impressive Google’s PR budget.` Totally agree with that.

    Reply
  3. Hi Vasiliy,
    What will happen if ActivityC sets result to ActivityB and onActivityResult() of ActivityB contains some of the saved state fields? Is onActivtyResult() called after the state was restored?

    “The state of the back-stack contains all the information required in order to reconstruct equivalent “chain” of Activities upon future state restoration. This includes the type of the Activities on the back-stack, their order, Intents that started the Activities, etc.” – Does it mean that if I have Intent with extra, for example boolean “FROM_LOGIN”, I need to REMOVE this extra, so the if(FROM_LOGIN) block will not run twice after activity is restored?

    Reply
    • Hello Pavel,
      As far as I know, onActivityResult won’t be called again on state restoration. If you receive any data in this callback and want to preserve it over process death, you’ll need to store it manually.
      As for removing data from Intents, that’s not something I’ve ever done. It’s much better to implement proper FSM for a screen, which will clearly define in which states that data will be used.

      Reply

Leave a Comment