Android 런타임(ART)과 Dalvik 가상 머신은 페이징과 메모리 매핑을 사용하여 메모리를 관리합니다. 즉, 새로운 객체를 할당하는 경우든 메모리 매핑된 페이지를 터치하는 경우든 앱이 수정하는 모든 메모리는 RAM에 계속 유지되며 페이지 아웃될 수 없습니다. 앱에서 메모리를 해제하는 유일한 방법은 앱이 보유한 객체 참조를 해제하여 가비지 컬렉터에서 메모리를 사용하도록 하는 것입니다. 한 가지 예외가 있습니다. 시스템이 메모리를 다른 데 사용하려는 경우 코드와 같이 수정 없이 메모리 매핑된 모든 파일이 RAM에서 페이지 아웃될 수 있습니다.
이 페이지에서는 Android가 앱 프로세스 및 메모리 할당을 관리하는 방법을 설명합니다. 앱에서 메모리를 더 효율적으로 관리하는 방법에 관한 자세한 내용은 앱 메모리 관리를 참고하세요.
가비지 컬렉션
ART 또는 Dalvik 가상 머신과 같은 관리된 메모리 환경에서는 각 메모리 할당을 계속 추적합니다. 프로그램에서 메모리 조각을 더 이상 사용하지 않는다고 확인하면 프로그래머의 개입 없이 그 메모리를 다시 힙으로 보냅니다. 관리된 메모리 환경에서 사용되지 않는 메모리를 회수하는 메커니즘을 가비지 컬렉션이라고 합니다. 가비지 컬렉션의 목표는 두 가지입니다. 바로 향후에 액세스할 수 없는 프로그램의 데이터 객체를 찾는 것과 그러한 객체에서 사용한 리소스를 회수하는 것입니다.
Android의 메모리 힙은 세대 간 메모리 힙이라서 할당되는 객체의 예상 수명과 크기에 기반해 추적하는 다양한 할당 버킷이 있습니다. 예를 들어 최근에 할당된 객체는 젊은 세대에 속합니다. 객체가 충분히 오래 활성 상태로 있으면 이전 세대로 승격되고 뒤이어 영구 세대가 될 수 있습니다.
각 힙 세대에는 객체가 차지할 수 있는 메모리 양에 자체 전용 상한이 있습니다. 세대가 채워지기 시작하면 시스템은 메모리 확보를 위해 가비지 컬렉션 이벤트를 실행합니다. 가비지 컬렉션 기간은 수집하는 객체가 어떤 세대이고 각 세대에 활성 객체가 얼마나 많은지에 따라 달라집니다.
가비지 컬렉션이 상당히 빠르게 실행된다고 해도 여전히 앱의 성능에 영향을 미칠 수 있습니다. 일반적으로 개발자는 코드 내에서 가비지 컬렉션 이벤트가 발생하는 시기를 제어하지 않습니다. 시스템에는 가비지 컬렉션을 실행할 시기를 판단하는 기준 세트가 있습니다. 기준이 충족되면 시스템은 프로세스 실행을 중지하고 가비지 컬렉션을 시작합니다. 가비지 컬렉션이 애니메이션과 같은 집중 처리 루프 중이나 음악 재생 중에 발생한다면 처리 시간이 늘어날 수 있습니다. 처리 시간의 증가로 인해 앱에서 효율적이고 원활한 프레임 렌더링을 위한 권장 16밀리초 임계값을 초과해 코드 실행을 푸시할 수 있습니다.
또한 코드 흐름은 가비지 컬렉션 이벤트를 더 자주 발생하게 하거나 정상보다 오래 지속되게 하는 작업을 실행할 수도 있습니다. 예를 들어 알파 블렌딩 애니메이션의 각 프레임에서 for 루프의 가장 안쪽 부분에 여러 객체를 할당한다면 메모리 힙을 많은 객체로 오염시킬 수 있습니다. 그런 상황에서 가비지 컬렉터는 여러 가비지 컬렉션 이벤트를 실행하여 앱의 성능을 저하시키게 됩니다.
가비지 컬렉션에 관한 자세한 내용은 가비지 컬렉션을 참고하세요.
메모리 공유
필요한 모든 것을 RAM에 맞추기 위해 Android는 프로세스 간에 RAM 페이지 공유를 시도합니다. 다음과 같은 방법으로 공유할 수 있습니다.
- 각 앱 프로세스는 Zygote라는 기존 프로세스에서 포크됩니다. Zygote 프로세스는 시스템이 부팅되고 일반 프레임워크 코드 및 리소스(예: 활동 테마)를 로드할 때 시작됩니다. 새로운 앱 프로세스를 시작하기 위해 시스템은 Zygote 프로세스를 포크한 다음 새 프로세스에서 앱의 코드를 로드하고 실행합니다. 이 방법을 사용하면 프레임워크 코드 및 리소스에 할당된 대부분의 RAM 페이지를 모든 앱 프로세스에서 공유할 수 있습니다.
-
대부분의 정적 데이터는 프로세스에 메모리 매핑됩니다.
이 기법으로 데이터를 프로세스 간에 공유할 수 있고 필요할 때 페이지 아웃할 수도 있습니다. 정적 데이터의 예로는 Dalvik 코드(직접 메모리 매핑을 위해 사전 연결된
.odex
파일에 배치), 앱 리소스(리소스 테이블을 메모리 매핑될 수 있는 구조로 설계 및 APK의 zip 항목 정렬),.so
파일의 네이티브 코드와 같은 기본 프로젝트 요소 등이 있습니다. - 여러 곳에서 Android는 명시적으로 할당된 공유 메모리 영역을 사용하는 프로세스에서 동일한 동적 RAM을 공유합니다(ashmem 또는 gralloc를 사용). 예를 들어 창 표면은 앱과 화면 컴포지터 사이에 공유된 메모리를 사용하고 커서 버퍼는 콘텐츠 제공업체와 클라이언트 사이에 공유된 메모리를 사용합니다.
공유된 메모리의 광범위한 사용으로 인해 앱에서 사용하는 메모리 양을 파악하는 데는 주의가 필요합니다. 앱의 메모리 사용을 올바르게 파악하는 기법은 RAM 사용량 조사에서 설명합니다.
앱 메모리 할당 및 회수
Dalvik 힙은 각 앱 프로세스의 단일 가상 메모리 범위로 제한됩니다. 이것은 논리적 힙 크기를 정의하며 이 힙 크기는 필요에 따라 시스템이 각 앱에 정의하는 한도까지만 커질 수 있습니다.
힙의 논리적 크기는 힙에서 사용하는 실제 메모리 양과 동일하지 않습니다. 앱의 힙을 검사할 때 Android는 PSS(Proportional Set Size)라는 값을 계산합니다. PSS는 다른 프로세스와 공유되는 더티 페이지와 클린 페이지를 일정 부분 모두 차지하지만, RAM을 공유하는 앱 수에 비례하는 정도로만 차지합니다. 이 PSS 총계는 시스템에서 실제 메모리 공간으로 간주됩니다. PSS에 관한 자세한 내용은 RAM 사용량 조사 가이드를 참고하세요.
Dalvik 힙은 힙의 논리적 크기를 압축하지 않습니다. 즉, Android는 힙을 조각 모음하여 공간을 닫지 않습니다. Android는 힙 끝 부분에 사용되지 않은 공간이 있을 때만 논리적 힙 크기를 축소할 수 있습니다. 그러나 시스템은 여전히 힙에서 사용하는 실제 메모리를 줄일 수 있습니다. 가비지 컬렉션 후 Dalvik은 힙을 탐색하며 사용하지 않는 페이지를 찾고 madvise를 사용하여 찾은 페이지를 커널에 반환합니다. 따라서 큰 청크의 페어링된 할당 및 할당 해제는 사용된 실제 메모리를 모두 또는 거의 모두 회수해야 합니다. 그러나 작은 할당에서 메모리를 회수하면 효율성이 많이 떨어질 수 있습니다. 작은 할당에 사용된 페이지가 아직 해제되지 않은 다른 것과 여전히 공유될 수 있기 때문입니다.
앱 메모리 제한
기능적인 멀티태스킹 환경을 유지하기 위해 Android는 각 앱의 힙 크기를 엄격히 제한합니다. 정확한 힙 크기 제한은 전반적으로 기기에서 사용할 수 있는 RAM 크기에 따라 기기마다 다릅니다. 앱이 힙 용량에 도달하여 더 많은 메모리를 할당하려고 하면 OutOfMemoryError
를 수신할 수 있습니다.
경우에 따라 시스템에 쿼리하여 현재 기기에서 힙 공간을 얼마나 사용할 수 있는지 정확히 파악할 수 있습니다. 예를 들어 캐시에 보관하기 안전한 데이터는 얼마나 되는지 확인합니다. getMemoryClass()
를 호출하여 시스템에 이 수치를 쿼리해도 됩니다.
이 메서드는 앱의 힙에 사용 가능한 MB 수를 나타내는 정수를 반환합니다.
앱 전환
사용자가 앱 간에 전환하면 Android는 포그라운드 상태가 아닌 앱 즉, 사용자에게 표시되지 않거나 음악 재생과 같은 포그라운드 서비스를 실행하지 않는 앱을 캐시에 보관합니다. 예를 들어 사용자가 앱을 처음 실행하면 프로세스가 만들어지지만 사용자가 앱을 닫으면 그 프로세스가 종료되지 않습니다. 시스템은 프로세스를 캐시된 상태로 유지합니다. 나중에 사용자가 앱으로 돌아오면 시스템에서 프로세스를 재사용하므로 앱 전환이 더 빨라집니다.
앱에 캐시된 프로세스가 있고 그 프로세스에 현재 필요하지 않은 리소스가 포함된 경우 앱은 당시 사용되고 있지 않더라도 시스템의 전반적 성능에 영향을 미칩니다. 메모리 같은 리소스가 부족해지면 시스템은 캐시에 있는 프로세스를 종료합니다. 또한 시스템은 가장 많은 메모리를 보유하는 프로세스를 차지하고 종료하여 RAM을 확보할 수 있습니다.
참고: 캐시에 있는 앱이 소비하는 메모리가 적을수록 앱이 종료되지 않고 신속하게 다시 시작할 가능성이 높습니다. 하지만 즉각적인 시스템 요구 사항에 따라 캐시된 프로세스는 그 리소스 사용률에 관계없이 언제든지 종료될 수 있습니다.
포그라운드에서 실행되지 않는 동안 프로세스를 캐시하는 방법과 Android에서 종료할 수 있는 캐시를 판단하는 방법에 관한 자세한 내용은 프로세스 및 스레드 가이드를 참고하세요.
메모리 스트레스 테스트
메모리 스트레스 문제는 고급형 기기에서 흔히 발생하지는 않지만 Android(Go 버전)를 실행하는 기기처럼 RAM이 적은 기기에서는 사용자에게 문제를 일으킬 수 있습니다. 계측 테스트를 작성하여 앱 동작을 확인하고 메모리가 적은 기기의 사용자 경험을 개선할 수 있도록 메모리 스트레스를 거친 환경을 재현하는 것이 중요합니다.
애플리케이션 스트레스 테스트
애플리케이션 스트레스 테스트(stressapptest
)는 현실적인 고부하 상황을 만들어 앱의 다양한 메모리 및 하드웨어 제한사항을 테스트할 수 있는 메모리 인터페이스 테스트입니다. 시간 및 메모리 제한을 정의하는 기능을 통해, 메모리를 많이 사용하는 실제 상황을 확인하는 계측을 작성할 수 있습니다.
예를 들어 다음 명령어를 사용하여 데이터 파일 시스템의 정적 라이브러리를 푸시하고 실행 가능한 상태로 만들고 20초간 990MB의 스트레스 테스트를 실행할 수 있습니다.
adb push stressapptest /data/local/tmp/ adb shell chmod 777 /data/local/tmp/stressapptest adb shell /data/local/tmp/stressapptest -s 20 -M 990
stressapptest
문서를 참고하세요.
stressapptest에 관한 관측
stressapptest
같은 도구를 사용하면 무료로 사용 가능한 것보다 큰 메모리 할당을 요청할 수 있습니다. 개발자는 이러한 유형의 요청으로 인해 다양한 알림이 발생할 수 있다는 점을 인지하고 있어야 합니다. 가용 메모리 부족으로 인해 발생할 수 있는 세 가지 기본 알림은 다음과 같습니다.
- SIGABRT: 시스템의 메모리가 이미 부족할 때 여유 메모리보다 큰 크기 할당을 요청하여 프로세스에 치명적인 네이티브 충돌이 발생합니다.
SIGQUIT
: 계측 테스트에서 감지되면 코어 메모리 덤프를 생성하고 프로세스를 종료합니다.TRIM_MEMORY_EVENTS
: 이 콜백은 Android 4.1(API 수준 16) 이상에서 사용할 수 있으며 프로세스에 관한 자세한 메모리 알림을 제공합니다.