Developing Myself Everyday
article thumbnail

안드로이드의 메모리 관리


안드로이드의 ART와 Dalvik 가상 머신은 페이징메모리 매핑을 사용하여 메모리를 관리합니다. 

 

Android가 앱 프로세스 및 메모리 할당을 관리하는 방법을 이제부터 알아보고 이런 방식이 어떻게 나오게 되었는지도 같이 알아보고자 합니다.

 

 

 

메모리 할당


프로세스는 메모리(RAM)의 빈 공간에 할당되어야 합니다. 아래 그림과 같은 상황이라고 생각해 보겠습니다.

 

 

프로세스 C를 메모리에 할당해야 하는데 어디에 넣을지 잘 모르겠습니다. 이를 할당하는 방법은 최초 적합, 최적 적합, 최악 적압이 있습니다.

 

아래의 그림을 보면 이해가 됩니다.

 

운영체제가 빈 공간을 순서대로 검색하다 프로세스 C가 들어갈 수 있는 공간을 발견하면 그 공간에 프로세스를 배치하는 방식을 최초 적합이라고 합니다. 

 

최적 적합최악 적합은 운영체제가 빈 공간을 모두 탐색한 다음 할당하는 방식으로 결과를 얻습니다.

 

위의 그림에서는 최초로 발견한 빈 공간이 최적의 장소이기에 일치합니다,

 

 

 

External Fragmentation(외부 단편화)

그럼 만약 최적 적합으로 프로세스를 할당 했음에도 빈 공간이 발생한다면 어떻게 될까요? 바로 외부 단편화가 발생합니다.

 

프로세스들이 메모리에 적재되고 종료되길 반복하다보면 메모리 사이에 빈 공간이 발생하게 됩니다. 

 

 

위의 그림을 보면 프로세스 A가 빈 공간에 들어갈 수 없습니다. 프로세스 B, C를 잘 배치한다면 빈 공간에 프로세스 A를 넣을 수 있겠지만 지금은 불가능합니다. 이런 작은 메모리 공간들로 인해 메모리가 낭비되는 현상을 외부 단편화라합니다.

 

 

 

압축

흩어져 있는 빈 공간을 하나로 모아서 큰 공간으로 만들 수도 있습니다. 다만 이 방식은 많은 양의 오버헤드를 발생시킵니다.

 

이런 문제를 해결할 방법이 바로 페이징입니다. 이것이 안드로이드 OS가 페이징을 사용하는 이유입니다.

 

 

 

Paging(페이징)


페이징은 아주 단순한 발상의 전환으로 문제를 해결했습니다. 바로 프로세스를 일정한 크기인 페이지(Page)로 잘라서 빈 공간에 넣었습니다. 

 

 

다만 이렇게 하면 프로세스들이 잘라져서 흩어지게됩니다. 그럼 나중에 잘라진 프로세스들을 찾기가 어려워 집니다. 그렇기에 메모리 매핑을 합니다.

 

 

 

 

Memory Mapping(메모리 매핑)


프로세스의 논리 주소를 사용해서 실제 저장되는 프로세스의 물리 메모리의 물리 주소를 가상 메모리의 논리 주소 공간에 저장합니다. 바로 이 가상 메모리의 논리 주소를 물리 메모리의 물리 주소로 가리키도록 하는 것이 바로 메모리 매핑입니다.

 

 

여기서 가상 주소 공간의 단위를 페이지라하고 물리 주소 공간의 단위를 프레임(Frame)이라고 합니다.


안드로이드에서의 페이지는 4KB 단위로 나뉩니다.

 

 

메모리 매핑된 정보는 프로세스마다 있는 페이지 테이블로 저장됩니다.

 

페이지 테이블을 통해 물리적으로 분산되어진 페이지를 CPU 입장에서 바라본 논리 주소를 연속적으로 보이게 해 줍니다. 그렇기에 CPU는 오직 페이지 테이블을 보고 프로세스를 순차적으로 실행할 수 있게 합니다.

 


CPU는 프로세스 테이블 베이스 레지스터(PTBR)이 가리키는 페이지 테이블을 보고 프로세스가 어디에 적재되어 있는지를 알 수 있습니다. 즉, 프로세스 A가 실행되면 PTBR은 프로레스 A의 페이지 테이블을 가리키고 프로세스 B가 실행되면 PTBR은 프로세스 B의 페이지 테이블을 가리키게됩니다.

 

 

 

 

스와핑(Swapping)


메모리(RAM)은 일반적으로 프로세스가 설치되는 보조기억장치보다 작습니다. 프로세스의 크기가 매우 크다면 프로세스 전체를 메모리에 저장하는 불가능합니다.

 

생각해보면 시스템은 프로세스를 페이지 단위로 잘라서 메모리에 적재했습니다. 그렇다면 굳이 프로세스 전체를 다 메모리에 넣을 필요는 없다는 생각이 듭니다.

RAM
(랜덤 액세스 메모리)는 시스템의 단기 데이터 스토리지로, 정보에 빠르게 액세스할 수 있도록 컴퓨터가 실시간으로 사용하는 정보를 저장하는 공간이고, 시스템에서 많은 프로그램을 실행할수록 더 많은 메모리가 필요로 합니다.

 

필요없는 프로세스의 페이지는 스왑 아웃하고 필요한 프로세스의 페이지를 스왑 인하면 물리 메모리보다 훨신 더 큰 프로세스도 메모리에 적재해 실행할 수 있게 됩니다. 이를 스와핑이라고 합니다.

 

 

이런 방식 덕분에 실제 메모리보다 더 큰 프로그램을 구동할 수 있습니다.

 

 

 

안드로이드에서는 이 스와핑을 담당하는 공간이 있습니다. 바로 zRAM 입니다.

 

zRAM은 RAM의 파티션으로 스왑 공간에 사용됩니다. 모든 것은 zRAM에 배치될 때 압축되고 zRAM에서 복사될 때 압축이 해제됩니다. 이 부분의 RAM은 페이지가 zRAM으로 들어오거나 zRAM에서 나갈 때 크기가 커지거나 작아집니다.

 

 

 

Page Fault(페이지 폴트)

우리는 프로세스가 전부 물리 메모리에 저장되어 있지 않다는 것을 알게 되었습니다. 만약 우리가 원하는 페이지가 물리 메모리에 저장되어 있지 않다면?? 여기서 생기는 문제가 바로 페이지 폴트입니다.

 

CPU는 페이지 테이블을 확인하여 가상 메모리 주소에 해당하는 페이지가 물리 메모리에 적재되어 있는지 확인합니다. 만약 물리 메모리에 해당 페이지가 없는 경우 페이지 폴트가 발생합니다.

 

그렇기에 물리 메모리에 우리가 원하는 페이지를 할당해야 합니다. 보통 물리 메모리는 가득 차 있기 때문에 우리는 페이지를 교체해야 합니다.

 

페이지 교체 알고리즘은 다양합니다. 흔히 언급되는 것들은 다음과 같습니다.

  • FIFO (First In First Out)
  • Optimal
  • LRU (Least Recently Used)
  • LFU (Least Frequently Used)
  • MFU (Most Frequently Used)

 

 

 

 

메모리 부족 관리(Low Memory Management)


Android에는 메모리 부족 상황을 처리하는 두 가지 기본 메커니즘인 커널 스왑 데몬로우 메모리 킬러가 있습니다.

 

커널 스왑 데몬(kswapd, Kernel swap daemon)

커널 스왑 데몬(kswapd)은 Linux 커널의 일부로 사용된 메모리를 사용 가능한 메모리로 변환합니다.

 

보조기억장치(Storage)에서 메모리에 메모장 페이지를 가져왔다고 생각해 보겠습니다. 메모장은 아래 그림의 분홍색 네모입니다.

 

이 페이지는 사용되지 않은 Clean Page 입니다. 커널 스왑 데몬은 수정되지 않은 Clean Page 를 삭제하여 메모리 공간을 확보할 수 있습니다.

 

 

그렇다면 해당 페이지를 수정해서 Dirty Page가 되어버렸다면 어떻게 될까요? 이전 처럼 그냥 삭제하는 것은 좋은 방법이 아닙니다. 

 

 

그래서 안드로이드는 Dirty Page를 압축해서 zRAM에 넣습니다. 이렇게 하면 RAM의 공간이 확보되고 만약 프로세스가 Dirty Page를 사용하려고 한다면 페이지가 압축 해제되고 다시 RAM으로 돌아갑니다.

 

이렇게 커널 스왑 데몬은 메모리를 확보합니다.

 

 

 

로우 메모리 킬러 (LMK, low-memory killer)

메모리 부족을 관리하는 또 다른 방법은 LMK를 사용하는 것입니다. LMK는 프로세스를 Background, Foreground, System으로 나누고 점수를 매깁니다. 높은 점수일수록 해당 프로세스는 Kill될 확률이 높습니다.

 

 

 

 

 

예시를 한번 보겠습니다. 아래의 그림은 Foreground에서 게임을 실행했을 때를 나타냅니다.

 

오른쪽에 있는 막대 그래프는 가용 메모리의 양을 나타냅니다. Backgroud에 있는 Google Docs는 메모리 사용량이 905이기에 메모리의 양이 부족합니다. 그렇기에 LMK는 Google Docs를 종료시키고 메모리를 확보합니다.

 

 

 

만약 메모리 가용 용량이 0 이하로 떨어진다면 Foreground에 있는 게임도 종료될 것입니다.

 

다행히도 Android에서는 onTrimMemory()를 제공합니다. 시스템은 onTrimMemory()를 사용해 프로세스가 종료되기 전에 메모리가 부족하고 할당량을 줄여야 한다고 앱에 알립니다. 이 방법으로 해결되지 않으면 이제 LMK이 나서서 프로세스를 종료합니다.

 

 

 

lmkd (LMK-daemon)

지금까지 Android는 하드 코딩 값에 의존하는 강력한 메커니즘인 커널 내 로우 메모리 킬러(LMK) 드라이버를 사용하여 시스템 메모리 압력을 모니터링했습니다. 커널 4.12부터 LMK 드라이버는 업스트림 커널에서 삭제되고 사용자 공간 lmkd가 메모리 모니터링과 프로세스 종료 작업을 실행합니다.

 

lmkd에 대한 더 자세한 설명은 아래에서 확인할 수 있습니다.

 

로우 메모리 킬러 데몬  |  Android 오픈소스 프로젝트  |  Android Open Source Project

로우 메모리 킬러 데몬 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 로우 메모리 킬러 데몬(lmkd) 프로세스는 실행 중인 Android 시스템의 메모리 상태

source.android.com

 

 

 

 

프로세스 내에서의 메모리 관리


JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이 바로 Heap입니다.

 

아래 그림은 JVM의 Runtime Data Area를 나타낸 그림입니다.

 

 

클래스 로더는 클래스를 찾거나 로드할 때 논리 주소를 사용해 해당 클래스의 위치를 식별합니다. 그러나 클래스가 실제로 사용될 때, 실제 클래스의 인스턴스를 참조하는 주소가 필요하게 됩니다. 이게 바로 물리 주소입니다.

 

클래스 로더는 논리 주소를 처리하기 위해 Runtime Constant Pool(런타임 상수 풀)에 논리 주소를 저장합니다. 이때, 논리 주소는 물리 주소로의 변환을 기다리는 상태가 됩니다.

클래스가 메서드가 실제로 사용되는 시점에 해당 클래스를 로드하고, 논리 주소를 물리 주소로 변환해 사용합니다. 이 변환된 물리 주소는 힙 영역에 저장됩니다. 변환된 물리 주소는 객체의 인스턴스를 참조하는 레퍼런스로 사용됩니다.

 

여기서 주의해야할 점이 있습니다. 힙 영역에 저장된 물리 주소는 JVM 스택 영역의 변수나 다른 객체의 필드를 참조하고 있습니다. 그런데 만약 참조하는 변수나 필드가 없다면 의미 없는 객체가 즉 쓰레기가 되어버립니다. 그래서 힙 영역은 Garbage Collection(가비지 컬렉션)의 대상이 됩니다.

 

 

JVM에 대한 더 자세한 정보는 아래의 게시글을 참고하세요

 

JVM의 내부 구조와 작동

자바의 프로세스는 JVM에서 실행되는 독립적인 실행 프로그램이다. 자바 프로세스는 운영 체제에서 실행되기 위해 자원을 할당받고, 메모리 공간을 사용하며, 시스템 자원을 활용하여 작업을 수

everyday-develop-myself.tistory.com

 

 

 

Garbage Collection (가비지 컬렉션)

ART 또는 Dalvik 가상 머신과 같은 관리된 메모리 환경에서는 각 메모리 할당을 계속 추적합니다. 프로그램에서 메모리 조각을 더 이상 사용하지 않는다고 확인하면 프로그래머의 개입 없이 그 메모리를 다시 힙으로 보냅니다. 관리된 메모리 환경에서 사용되지 않는 메모리를 회수하는 메커니즘을 가비지 컬렉션이라고 합니다. 가비지 컬렉션의 목표는 두 가지입니다. 바로 향후에 액세스할 수 없는 프로그램의 데이터 객체를 찾는 것과 그러한 객체에서 사용한 리소스를 회수하는 것입니다.

 

 

아래의 그림을 보겠습니다. Android에서의 메모리 힙은 GC를 편하게 하기 위해서 할당된 타입에 따라서 구역 을 나누고 있습니다. 

 

 

각각의 구역의 크기에 객체가 할당되어 공간이 다 찬다면 시스템은 Garbage Collector를 불러 GC를 진행하게 합니다.

 

 

 

Dalvik과 ART에서의 GC

아래의 그림에서 알 수 있듯이 Dalvik에서 GC를 호출하면 모든 작업을 멈추고 GC를 진행합니다. 이러면 GC가 오래걸리거나 GC가 많다면 문제가 많습니다.

 

ART에서의 GC는 백그라운드에서 비동기적으로 처리됩니다. 완전히 작업이 멈추지 않는건 아니지만 Dalvik에 비해 많은 개선을 했습니다.

 

 

 

 

OOM Killer(Out of Memory - Killer)


위에서 우린 스와핑에 대해 알게 되었습니다. 스와핑으로 우린 실제 물리 메모리의 용량보다 더 큰 프로그램을 구동할 수 있습니다.

 

그럼 만약 애플리케이션이 할당된 범위를 넘어선 영역에 대해 메모리 할당을 요구한다면 어떻게 될까요?

 

바로 OOM이 발생합니다 OOM은 메모리가 부족으로 인해 발생합니다. 우리가 위에서 다뤘던 메모리 부족들은 다 OOM입니다.

 

다만 위에서 배운 커널 스왑 데몬과 LMK는 다 안드로이드에서 만든 메모리 관리 모듈입니다. 그래서 VM에서 동작합니다. 그렇다면 안드로이드가 영향을 끼치지 못하는 Native Memory는 누가 관리할까요??

 

바로 OOM Killer입니다. OOM Killer는 안드로이드 운영체제에서 절대 죽어서는 안되는 애플리케이션도 죽일 수 있습니다. 그렇기에 안드로이드는 자체적으로 메모리 관리 모듈을 만들어서 메모리를 확보하는 이유입니다.

 

 

 

 

마지막


이렇게 해서 메모리에 프로세스가 어떻게 할당되고 또 프로세스 내에서 메모리 힙을 어떻게 관리하는지 알아보았습니다.

 

아직은 안드로이드 개발자를 목표로하고 있는 취준생이기에 메모리에 대해 깊게 고민하고 생각해볼 계기가 없었습니다. (실제로 사용할 일도 없었고요)  

 

다만, 나중에 실제로 메모리에 대해 더 깊게 고민할 때를 대비해서 조금이라도 미리 알아보고자 이렇게 게시글로 한번 짚어보고 넘어갑니다.

 

 

 

 

Reference

 

메모리 관리 개요  |  App quality  |  Android Developers

메모리 관리 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 런타임(ART)과 Dalvik 가상 머신은 페이징과 메모리 매핑을 사용하여 메모리를 관리합

developer.android.com

 

프로세스 간 메모리 할당  |  App quality  |  Android Developers

프로세스 간 메모리 할당 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 플랫폼은 사용 가능한 메모리가 있다는 것은 메모리 낭비라는 전제 하에 실

developer.android.com

 

앱 메모리 관리  |  App quality  |  Android Developers

Android용으로 개발할 때 사전에 메모리 사용량을 줄이는 방법을 알아봅니다.

developer.android.com

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!