안드로이드는 가능한 한 많은 메모리를 활용해 성능을 최적화하도록 설계되었다.
예를 들면, 사용자가 앱 사용 중 홈 버튼을 눌러 나가거나 다른 앱으로 전환하더라도 해당 앱의 프로세스를 백그라운드에서 캐시 상태로 유지해 사용자가 앱을 빠르게 재시작할 수 있도록 한다.
이렇게 메모리를 적극적으로 활용하는 덕분에, 안드로이드 기기의 성능을 유지하기 위해선 메모리 관리가 매우 중요하다.
이번 포스팅에서는 안드로이드가 어떤 방식으로 메모리 관리 및 최적화를 하고 있는지 살펴보려 한다.
안드로이드는 메모리 관리를 위해 애플리케이션마다 할당할 수 있는 힙 크기를 제한하며, 앱이 이 크기를 초과하면 OutOfMemoryError
가 발생한다. 힙 영역의 크기는 디바이스, OS, 앱 설정에 따라 정해진다.
📌 힙(heap)이란?
앱이 런타임에서 동적으로 객체(Activity, List, Bitmap 등)를 생성할 때 사용하는 JVM의 메모리 영역
매니페스트의 <application>
태그에서 largeHeap 속성을 true로 설정해, OS에게 "이 앱은 메모리가 많이 필요하니, 가능한 더 큰 힙을 할당해줘" 라고 요청할 수 있다.
다만 어디까지나 '요청'이기 때문에 실제 할당되는 크기는 기기 및 OS 버전에 따라 달라질 수 있다.
<application
android:name = "Example"
...
android:largeHeap = "true"
>
갤럭시S24+, 안드로이드 15 환경에서 largeHeap 설정에 따라 힙 크기가 어떻게 달라지는지 로그로 살펴보았다.
val maxHeap = Runtime.getRuntime().maxMemory() / (1024 * 1024)
Log.d("Memory", "Max heap size = ${maxHeap}MB")
largeHeap = false | largeHeap = true |
---|---|
![]() | ![]() |
무려 2배나 커지는 것을 확인할 수 있었다!
그러나 힙 메모리가 커지면 GC에 걸리는 시간이 늘어나 앱이 무거워진다는 단점이 있다.
따라서 섣불리 힙 할당 크기를 늘리기보다는, 메모리 누수 등 근본적인 원인을 해결해 메모리 사용을 최적화하는 것이 중요하다.
안드로이드는 리눅스 커널 위에서 동작하므로, 리눅스와 마찬가지로 페이징 기반의 가상 메모리 방식을 사용한다. 리눅스의 대표적인 메모리 관리 방식은 다음과 같다.
스왑(Swap)
메모리가 부족할 때 사용하지 않는 페이지를 디스크의 스왑 공간으로 내보내고(page-out), 필요할 때 다시 메모리로 불러오는(page-in) 방식.
OOM(Out Of Memory Killer)
스왑 공간을 포함해 시스템이 더 이상 메모리를 확보할 수 없을 때, 커널이 개입해 프로세스를 종료시킨다. 이 때, 일반적으로 중요도가 낮고 메모리 사용량이 큰 프로세스를 우선적으로 제거한다.
그러나 리눅스의 스왑과 OOM을 모바일 환경인 안드로이드에 그대로 적용하기에는 문제가 있었으니…
안드로이드 하드웨어 기기 또한 컴퓨터처럼 주기억장치와 보조기억장치를 갖고 있다.
컴퓨터가 HDD나 SSD를 사용하는 것과 달리, 대부분의 스마트폰은 보조기억장치로 반도체 기반의 플래시 메모리를 사용한다. (정확히는 NAND 플래시 메모리)
플래시 메모리는 전원이 꺼져도 데이터를 유지할 수 있어 SSD, USB 드라이브 등에서 사용되며, 빠른 읽기/쓰기 속도와 작은 크기라는 장점이 있지만 아래와 같은 한계점도 있다.
이러한 플래시 메모리 기반 모바일 기기의 한계로 인해, 안드로이드에서는 보조기억장치의 스왑 공간을 제공하지 않는다.
그러나 스왑 공간 없이도 안드로이드는 메모리를 효과적으로 관리하도록 설계되었다.
바로, RAM 일부를 압축해서 스왑 공간처럼 사용하는 기법 덕분이다.
Android 9(Pie) 버전 이후 대부분의 기기에서 기본으로 zRAM을 활성화한다.
zRam은 압축률이 높아, 1GB zRAM에 실제로 2GB 이상의 데이터를 저장할 수 있다.
/dev/zram0
).zip
과 같아서 사용할 수 없다.기존의 리눅스 메모리 정책인 (OOM killer)는 임베디드 환경에 적용하기에 아래와 같은 한계점이 있었다.
이를 개선한 방식이 바로 LMK이다.
OOM adj score
)를 계산cached
, background
, service
, foreground
순으로 제거 우선순위가 높음Android9부터는, 아래의 이유들로 LMK가 커널 영역에서 유저 영역으로 이동했다.
그에 따라 명칭도 LMKD (low memory killer daemon)로 변경되었다.
안드로이드 앱이 실행되면서 다양한 객체들이 메모리(heap)에 생성된다.
이 중 더 이상 사용되지 않는 객체들은 자동으로 제거되어야 메모리 누수를 막을 수 있다.
이 역할을 수행하는 것이 바로 가비지 컬렉터(Garbage Collector, GC)이다.
안드로이드 런타임 환경 ART(Android Runtime)의 GC는 Mark and Sweep 방식을 사용한다.
GC Root
에서 시작해 참조를 따라가며 사용 중인 객체들을Mark하는 단계GC가 실행되면 일시적으로 애플리케이션의 모든 스레드가 멈추는 현상이 발생한다.
이를 STW(Stop-The-World)라고 하며, GC를 수행하는 스레드를 제외한 모든 작업이 중단된다.
STW 시간이 길어지면 UI가 멈추거나 애니메이션과 스크롤이 버벅이며 UX가 저하될 수 있다.
따라서 안드로이드에서는 이 STW 시간을 최대한 줄이기 위해 아래와 같은 최적화를 한다.
기회가 된다면 안드로이드 버전 별 GC의 발전 역사도 다뤄보면 좋을 것 같다.