Android Memory Management
Memory on Android is shared across multiple processes. How much memory a process uses is not clear at first glance. Android memory management is complex but there is a great talk on Understanding Android memory usage from Google I/O 2018 which you should take a look at before reading on.
Paging is a method of moving memory from main memory to secondary memory or vice versa.
Android pages out to disk but does not use swap space for paging the memory. This makes it even more difficult to see the total memory, especially as every application in Android runs in a different process which runs its own instance of a Dalvik VM.
Android uses paging but does not utilize swap space. Paging relies heavily on the ability to memory map (mmap()) files and store the kernel page in data as needed. Although this doesn’t happen often, paging needs to drop kernel pages when memory is low and the system drops cache page files. Android does not swap spaces for paging out dirty pages, as doing so on mobile devices both lowers battery life and causes excess wear-and-tear on memory.
Android devices frequently come with very little onboard flash and limited space to store data. This is mainly used to store apps but could actually store a swap file. Onboard flash is slow and has generally worse access rates than those of hard disks or flash drives. Although onboard flash size has improved recently, it is still not enough to enable swapping spaces effectively. A basic rule of thumb for swap file size is about 512MB per 1-2GB RAM. You can always enable swap support by modifying the kernel .config file (CONFIG_SWAP) and compiling the kernel yourself, but doing so falls outside the scope of this guide.
Just how much memory can your app use before the Android system (aka memory killer) activates and starts shutting down processes? Unfortunately, there is no simple answer, and figuring it out involves a lot of profiling with the tools such as dumpsys, procrank, and Android Studio.
Many different factors can influence your ability to measure memory consumption on Android such as the following:
Different platform configuration for low, mid, and high-end devices
Different OS versions on the test device(s)
Different points in your application when you measure memory
Overall device memory pressure
It is important to always measure your memory at the same location in your code with the same platform configuration, OS version, and device memory pressure.
A good way to profile memory is to ensure that the device has plenty of free memory available (low memory pressure) while you profile the memory consumption of your application. If the device has no free memory available (high memory pressure) it can be difficult to get stable results. It’s important to keep in mind that although you use profiling to try to find the source of high memory pressure, there are still hard physical limitations. If the system is already thrashing memory caches, it will produce unstable results during memory profiling your app.
If you were to sum up all physical RAM mapped to each process, then add up all of the processes, the resulting figure would be greater than the actual total RAM. With dumpsys, you can get clearer information about each Java process. The stats dumpsys provides contain a variety of information related to the apps’ memory. dumpsys is an Android tool that runs on the device and dumps information about the status of system services and applications. dumpsys enables you to easily access system information.
Get system information in a simple string representation.
Use dumped CPU, RAM, battery, and storage to check how an application affects the overall device.
The following command lists all services offered by dumpsys:
~$ adb shell dumpsys | grep "dumpsys services"
You can use dumpsys meminfo to dump system memory on Android.
adb provides a host of tools to gain information about the memory of a running application on Android. The most common and quickest way to get an overview is the adb shell dumpsys meminfo command. It reports detailed information about the memory usage of each Java process, native heap, binary data as well as a variety of process and system information. The following command will provide a quick overview of system memory:
~$ adb shell dumpsys meminfo
It’s possible to track a single process via name, bundle ID or pid to determine the details of the Unity androidtest app as the following command shows. The androidtest app is an empty Unity Project with only one main Scene, no Skybox, and no content, to get a baseline for memory measurements.
~$ adb shell dumpsys meminfo com.unity.amemorytest
This prints the following information in the command line using a Nexus 6P (2560 by 1440 px - Android 8.1.0 and Unity 2018.1).
* Applications Memory Usage (in Kilobytes): * Uptime: 6815563691 Realtime: 10882940478 * * ** MEMINFO in pid 20676 [com.unity.androidtest] ** * Pss Private Private SwapPss Heap Heap Heap * Total Dirty Clean Dirty Size Alloc Free * ------ ------ ------ ------ ------ ------ ------ * Native Heap 31467 31448 0 0 51072 47261 3810 * Dalvik Heap 1872 1760 0 0 12168 7301 4867 * Dalvik Other 470 460 0 0 * Stack 492 492 0 2 * Ashmem 8 0 0 0 * Gfx dev 3846 2036 0 0 * Other dev 4 0 4 0 * .so mmap 17760 516 15908 161 * .jar mmap 4 0 4 0 * .apk mmap 243 0 0 0 * .dex mmap 116 4 112 0 * .oat mmap 6206 0 3244 0 * .art mmap 2571 716 232 22 * Other mmap 49 4 0 2 * EGL mtrack 99840 99840 0 0 * GL mtrack 64480 64480 0 0 * Unknown 1270 1264 0 14 * TOTAL 230899 203020 19504 201 63240 54562 8677 * * App Summary * Pss(KB) * ------ * Java Heap: 2708 * Native Heap: 31448 * Code: 19788 * Stack: 492 * Graphics: 166356 * Private Other: 1732 * System: 8375 * * TOTAL: 230899 TOTAL SWAP PSS: 201 * * Objects * Views: 7 ViewRootImpl: 1 * AppContexts: 2 Activities: 1 * Assets: 2 AssetManagers: 2 * Local Binders: 16 Proxy Binders: 21 * Parcel memory: 5 Parcel count: 23 * Death Recipients: 1 OpenSSL Sockets: 2 * WebViews: 0 * * SQL * MEMORY_USED: 0 * PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0 *
In contrast, executing the same command using an application featuring a full 3D Scene and a significantly higher amount of content prints the following information:
* Applications Memory Usage (in Kilobytes): * Uptime: 6823482422 Realtime: 10890859209 * * ** MEMINFO in pid 22903 [com.unity3d.androidtest] ** * Pss Private Private SwapPss Heap Heap Heap * Total Dirty Clean Dirty Size Alloc Free * ------ ------ ------ ------ ------ ------ ------ * Native Heap 304918 304900 0 0 327552 315885 11666 * Dalvik Heap 1240 1096 0 0 11858 7127 4731 * Dalvik Other 424 412 0 0 * Stack 528 528 0 1 * Ashmem 6 0 0 0 * Gfx dev 196934 132128 0 0 * Other dev 4 0 4 0 * .so mmap 23976 668 21920 199 * .apk mmap 368 0 0 0 * .dex mmap 116 4 112 0 * .oat mmap 6060 0 3768 0 * .art mmap 2774 604 332 25 * Other mmap 44 4 0 2 * EGL mtrack 21600 21600 0 0 * GL mtrack 384184 384184 0 0 * Unknown 6577 6568 0 17 * TOTAL 949997 852696 26136 244 339410 323012 16397 * * App Summary * Pss(KB) * ------ * Java Heap: 2032 * Native Heap: 304900 * Code: 26472 * Stack: 528 * Graphics: 537912 * Private Other: 6988 * System: 71165 * * TOTAL: 949997 TOTAL SWAP PSS: 244 * * Objects * Views: 7 ViewRootImpl: 1 * AppContexts: 3 Activities: 1 * Assets: 2 AssetManagers: 2 * Local Binders: 15 Proxy Binders: 20 * Parcel memory: 3 Parcel count: 14 * Death Recipients: 0 OpenSSL Sockets: 0 * WebViews: 0 * * SQL * MEMORY_USED: 0 * PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0 *
The following table compares the results and describes the detailed stats:
|Area||Empty Scene [MB]||Full Scene [MB]||Description|
|Pss||230||949||Proportional set size (Pss) is a metric the kernel computes that takes memory sharing into account. The system scales each page of RAM in a processby the ratio of the count of other processes using the same page. All private pages contribute 100% of their size, and shared memory contributes size/(num of processes shared). For example, a page that is shared between two processes will contribute half of its size to the Pss of each process. This way you can calculate the total RAM used by summing up the Pss across all processes. Comparing Pss between processes provides a rough idea of their relative weight.|
|Private Dirty||203||825||The most interesting and expensive metric is Private Dirty, which is the amount of RAM inside the process that cannot be paged to disk as it is not backed by the same data on disk, and the system cannot share with any other process. Another way to look at this is that this is the RAM that the system will reclaim when the application is destroyed. After reclaiming, it is quickly subsumed into caches and other uses because the system must fully utilize the limited memory available.|
|Native Heap||51||328||The Native Heap represents memory used by the process itself such as Unity Engine Code, Native C mallocs, and Mono VM.|
|Dalvik Heap||12||12||Dalvik Heap is memory the Dalvik VM allocates, for example; Variables in the Unity Java Android code.|
|Dalvik Other||0.4||0.4||Dalvik Other is memory used for JIT and Android GC.|
|Clean Memory||19||26||Android shares pages of memory among several processes such as code of common frameworks. As soon as memory in a page changes, the system must write to and modify the memory and flags the memory as dirty. However, clean memory is memory that hasn’t changed from when it was loaded from disk. If a change occurs, the memory becomes dirty.|
|Swapped Dirty||0.2||0.2||The application uses Dirty memory as space for computations. Android does not have a swap mechanism so dirty memory is also RAM that will be freed when the app exits. However, Swapped Dirty is used on some Android devices with the ability to remap, but they swap to RAM rather than flash. On Android, this is similar to Linux. ZRAM can compress pages and the Linux kernel swaps them to a special RAM area and decompresses them again when needed.|
|EGL mtrack||99||22||This is gralloc memory usage. It's primarily the sum of the SurfaceView and TextureView. It includes the frame buffer as well and therefore the size depends on the dimension of the framebuffers. The bigger the supported screen resolution, the higher the EGL mtrack number. In this test, the resolution of frame buffer for the full Scene was reduced to ensure good performance. Reducing the frame buffer size also reduces the amount of memory needed by these buffers.|
|GL mtrack & Gfx dev||69||581||GL and Gfx are driver-reported GPU memory, and are primarily the sum of GL texture sizes, GL command buffers, fixed global driver RAM overheads, and Shaders. Note that this number does not appear on older Android versions. Note: The client space driver and kernel space driver share a memory space. In some Android versions this sometimes gets counted twice and therefore the Gfx dev is bigger than it is in reality.|
|Unknown||1.3||6.5||Unknown is any RAM page that the system could not classify into one of the other more specific items. This includes native allocations or runtime metadata, which the tool cannot identify when collecting this data due to Address Space Layout Randomization. Private Dirty is unknown RAM dedicated to only your application.|
One alternative to dumpsys is procrank, another useful tool that you can use to view memory usage across all processes. It lists the memory usage of processes in order from highest to lowest usage. The sizes reported per process are Vss, Rss, Pss, and Uss.
~$ adb shell procrank
* PID Vss Rss Pss Uss cmdline * 890 84456K 48668K 25850K 21284K system_server * 1231 50748K 39088K 17587K 13792K com.android.launcher2 * 947 34488K 28528K 10834K 9308K com.android.wallpaper * 987 26964K 26956K 8751K 7308K com.google.process.gapps * 954 24300K 24296K 6249K 4824K com.unity.androidmemory * 888 25728K 25724K 5774K 3668K zygote * 977 24100K 24096K 5667K 4340K android.process.acore
Vss - Virtual set size is the total accessible address space of a process. It shows how much virtual memory is associated with a process.
Rss - Resident Set Size is how many physical pages are allocated to the process. Pages shared between processes are counted multiple times.
Pss - Proportional Set Size takes the Rss number but evenly distributes shared pages among the sharing processes. For example, if three processes are sharing 9MB, each process gets 3MB in Pss.
Uss - Unique Set Size is also known as Private Dirty, which is basically the amount of RAM inside the process that cannot be paged to disk as it is not backed by the same data on disk, and is not shared with any other processes.
Note: Pss and Uss are different than reports of meminfo. Procrank uses a different kernel mechanism to collect its data than meminfo which can give different results.
The meminfo command gives a summary of the overall memory usage of the system:
~$ adb shell cat /proc/meminfo
The first couple of numbers are worth discussing.
* MemTotal: 2866492 kB * MemFree: 244944 kB * Buffers: 36616 kB * Cached: 937700 kB * SwapCached: 13744 kB
MemTotal is the total amount of memory available to the kernel and userspace which is usually less than actual physical RAM as the handset requires memory for GSM, buffers, etc. as well.
MemFree is the amount of RAM that is not being used at all. On Android the number would typically be very small as the system tries to always use all available memory to keep processes running.
Cached is the RAM being used for filesystem caches etc.
Android Studio offers a memory profiler in addition to the command line tools available in the Android SDK. Similar to the command line tools reporting there is a split between managed and native memory.
In this case, the table compares the empty Project from the dumpsys meminfo section with the data from Android Studio. It basically covers the App Summary displayed from dumpsys meminfo with some additions.
|Others [mb]||3.1||Other dev + Unknown|
|Graphics [mb]||88.7||Gfxdev + EGL mtrack + GL mtrack|
|Native [mb]||40.8||Native Heap|
|Java [mb]||8||Dalvik Heap|
Usually, most of the memory goes into the Native Heap section. The Dalvik Heap is small compared to the Native Heap section. In case it grows, you should investigate the Android plugins you use in your application. The Native Heap makes it difficult to know where memory comes from and there is no great way to see Native Plugin allocations in the profiler. A possible solution to gain a greater insight is to isolate and measure the plugins used for 3rd party integrations and compare them with the memory baseline of an empty Project.