메모리 사용량 모니터링

게임의 메모리 사용량을 효과적으로 최적화하려면 먼저 Android 플랫폼에서 메모리를 측정하는 방법과 시스템 원격 분석, 진단 API, 프로파일링 도구를 사용하는 방법을 이해해야 합니다. 이 가이드에서는 새로운 플랫폼 가이드라인에 따라 게임의 메모리 할당을 모니터링, 캡처, 분석하는 방법을 자세히 설명합니다.

RSS 및 스왑 측정항목 이해

게임의 메모리 동작을 효과적으로 분석하고 디버그하려면 Android 플랫폼에서 메모리 적용에 사용하는 정확한 기술 측정항목을 이해해야 합니다. 이 원격 분석 매개변수가 실제 환경에서 처리되고 모니터링되는 방법에 관한 자세한 배경 정보는 Android Vitals - 메모리 사용량 (익명 RSS + 스왑) 문서를 참고하세요.

1. 익명 RSS (RssAnon)

상주 집합 크기 (RSS)는 기기의 실제 RAM에 보관된 프로세스가 차지하는 메모리 부분을 측정합니다. RSS는 파일 지원 메모리와 익명 메모리로 나뉩니다. Android의 적용 측정항목은 익명 RSS에만 중점을 둡니다.

  • 포함되는 항목: 저장소의 실제 파일에 연결되지 않은 게임 프로세스에서 직접 할당한 메모리 페이지입니다. 이러한 페이지에는 자바 또는 Kotlin 힙, 스레드 실행 스택, 그리고 중요한 네이티브 메모리 할당 (예: 맞춤 C++ 엔진 할당자 또는 네이티브 malloc 또는 new를 사용하여 요청되고 게임 로직에 의해 더티된 메모리 블록)이 포함됩니다. 이 측정항목에 관한 자세한 내용은 프로세스 메모리 (RSS) 사전을 참고하세요.
  • 중요한 이유: 게임 엔진은 대규모 네이티브 메모리 풀을 사용하여 물리, 렌더링, 로직을 처리합니다. 이러한 풀은 파일로 지원되지 않으므로 익명 RSS에만 존재하며 게임의 실제 공간을 구성합니다.

2. 압축 해제된 스왑 (VmSwap)

Android는 플래시 저장소 마모 및 지연 시간 제약으로 인해 기존의 디스크 기반 스왑 공간을 지원하지 않습니다. 대신 zRAM (압축 해제된 스왑)을 사용합니다.

  • 포함되는 항목: 실제 RAM 압력이 증가하면 커널의 메모리 관리 데몬이 비활성 익명 페이지를 압축하여 실제 RAM (zRAM)의 전용 압축 해제 부분으로 이동합니다.
  • 측정항목 계산: 시스템은 압축 해제된 크기 (VmSwap)를 기반으로 이를 추적하여 게임의 실제 실제 메모리 요구사항을 평가합니다. 게임에서 메모리를 할당하고 시스템에서 이를 zRAM으로 스왑하더라도 게임의 총 메모리 공간에 포함됩니다.

3. 프로세스 상태

메모리 사용량은 Android Vitals의 프로세스 상태별로 분류됩니다. 게임 개발자의 경우 서드 파티 SDK 또는 게임이 사용자 인식 서비스 또는 백그라운드 서비스를 예기치 않게 트리거할 수도 있습니다.

  • 포함되는 항목: 포그라운드, 인식 가능한 서비스, 백그라운드, 캐시됨.
  • 중요한 이유: 프로세스 상태마다 Android OS 메모리 관리에 미치는 영향이 다릅니다. 서드 파티 SDK가 의도치 않게 백그라운드 작업을 트리거하는 경우 게임이 민감한 프로세스 상태로 실행되고 있는지 알 수 없습니다. `RunningAppProcessInfo`를 사용하여 게임이 백그라운드에서 실행되고 있는지 모니터링합니다.

애플리케이션 프로그래밍 인터페이스 (API)

Android는 게임이 메모리 압력에 동적으로 응답하고 런타임에 세부 메모리 진단을 캡처할 수 있는 시스템 API를 제공합니다.

메모리 트림 이벤트에 응답

시스템은 onTrimMemory를 사용하여 앱이 메모리 사용량을 자발적으로 줄이고 메모리 부족 종료 (LMK)로 인해 종료되지 않도록 하여 다른 앱에서 사용할 수 있도록 메모리를 해제할 수 있는 좋은 기회를 제공하는 수명 주기 이벤트를 앱에 알립니다.

시스템에서 백그라운드에서 앱을 종료하면 사용자는 다시 시작할 때 느린 콜드 스타트를 경험합니다. 백그라운드 메모리 사용량을 줄이면 이러한 백그라운드 종료를 방지하는 데 도움이 됩니다.

트림 이벤트에 응답할 때는 즉시 필요하지 않은 재구성 가능한 대규모 메모리 할당을 해제합니다.

  • 예: 캐시된 비트맵 (로컬 저장소에서 디코딩됨)을 트림하거나 삭제하여 TRIM_MEMORY_UI_HIDDEN에 응답합니다.

Kotlin

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
    override fun onTrimMemory(level: Int) {
        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }
        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

자바

public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
    public void onTrimMemory(int level) {
        switch (level) {
            if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
                // Release memory related to UI elements, such as bitmap caches.
            }
            if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
                // Release memory related to background processing, such as by
                // closing a database connection.
            }
        }
    }
}

ProfilingManager

Android 15 (API 수준 35)에 도입된 ProfilingManager API를 사용하면 애플리케이션이 런타임에 프로그래매틱 방식으로 정의된 스냅샷 (예: 힙 프로필, 시스템 트레이스, 자바 힙 덤프)을 직접 캡처할 수 있습니다.

개발자는 특정 장면에서 캡처를 수동으로 트리거하거나 TRIGGER_TYPE_ANOMALY와 같은 자동 트리거를 등록하여 게임 프로세스가 메모리 제한기 임계값을 위반할 때 캡처를 자동으로 실행할 수 있습니다. 하지만 게임 개발자는 최신 게임 엔진의 중요한 제한사항을 고려해야 합니다.

참고: 최신 게임 엔진 (예: Unity 또는 Unreal)은 MAP_ANONYMOUS 플래그가 있는 mmap을 사용하여 커널에서 대규모 가상 메모리 블록을 사전 할당하여 실행 성능을 관리합니다. 그런 다음 엔진은 맞춤 하위 할당자 (예: Unity의 네이티브 메모리 관리자 또는 Unreal의 BinnedAllocators)를 사용하여 메모리 블록을 내부적으로 세분화하고 할당합니다.

ApplicationExitInfo

게임이 백그라운드에서 종료되거나 개별 프로세스 메모리 한도를 위반하여 종료된 경우 표준 Java 또는 네이티브 크래시 덤프 메커니즘(예: Firebase Crashlytics)은 이벤트를 등록하지 않습니다. 이러한 종료를 프로그래매틱 방식으로 쿼리하고 로깅하려면 개발자는 게임 시작 시 ApplicationExitInfo API를 활용해야 합니다.

  • 구현: 시작 시 ActivityManager.getHistoricalProcessExitReasons()를 호출하여 최근 세션의 종료 이유를 가져옵니다.
  • 주요 메모리 종료 이유:
    • REASON_LOW_MEMORY: 프로세스가 시스템의 메모리 부족 종료 (LMK)에 의해 종료되었음을 나타냅니다. 이 종료는 기기 전체의 메모리 압력이 높고 OS에서 RAM을 회수해야 할 때 발생합니다. 이 종료 이유는 게임의 백그라운드 공간이 너무 커서 다른 애플리케이션과 공존할 수 없음을 나타냅니다.
    • REASON_MEMORY_LIMITER (Android 17 (API 수준 37) 이상): 프로세스가 플랫폼의 메모리 제한기에서 할당한 cgroup 메모리 한도 (RssAnon + VmSwap)를 초과하여 특별히 종료되었음을 나타냅니다. 이 종료는 기기에 충분한 실제 메모리가 남아 있는 경우에도 발생할 수 있으며, 이는 개별 프로세스 한도를 직접 위반했음을 나타냅니다.

사용 가능한 도구 사용

개발 및 QA 중에 다음 플랫폼 도구를 사용하여 게임의 메모리 사용량을 정확하게 측정합니다.

meminfo

이 도구는 할당된 PSS 메모리의 양과 메모리가 사용된 카테고리를 보여 주는 메모리 통계를 수집합니다.

다음 방법 중 하나를 사용하여 meminfo 통계를 출력합니다.

  • adb shell dumpsys meminfo package-name 명령어를 사용합니다.
  • Android Debug API의 MemoryInfo 호출 을 사용합니다.

PrivateDirty 통계는 디스크로 페이징할 수 없고 다른 프로세스와 공유되지 않는 프로세스 내의 RAM 양을 보여 줍니다. 이 양의 대부분은 이 프로세스가 종료될 때 시스템에서 사용할 수 있게 됩니다.

메모리 tracepoint

메모리 tracepoint는 게임에서 사용하는 RSS 메모리 양을 추적합니다. RSS 메모리 사용량을 계산하는 작업은 PSS 사용량을 계산하는 것보다 훨씬 빠릅니다. 더 빠르게 계산되므로 RSS는 최대 메모리 사용량을 더 정확하게 측정하도록 메모리 크기의 변경사항을 자세하게 보여 줍니다. 따라서 게임의 메모리가 부족해질 수 있는 최댓점을 쉽게 알 수 있습니다.

Perfetto

Perfetto는 기기에서 성능 및 메모리 정보를 수집하여 웹 기반 UI에 표시하는 도구 모음입니다. 장기 트레이스를 임의로 지원하므로 시간 경과에 따른 RSS의 변화를 확인할 수 있습니다. 오프라인 처리를 위해 생성하는 데이터에 SQL 쿼리를 실행할 수도 있습니다. 시스템 추적 앱에서 장기 트레이스를 사용 설정합니다. 트레이스에 memory:Memory 카테고리 가 사용 설정되어 있는지 확인합니다. 개발 및 테스트에서 맞춤 메모리 계측을 위해 (베타) heapprofd API를 사용할 수도 있습니다.

Perfetto에서 RssAnon 및 스왑 검사

게임의 익명 메모리 및 zRAM 스왑 영향을 확인하려면 ui.perfetto.dev의 웹 기반 UI에서 트레이스 파일을 로드하고 심층 메모리 사례 연구를 위해 설계된 분석 기법을 따르세요 (자세한 내용은 Perfetto 메모리 분석 사례 연구 참고).

1. 타임라인에서 메모리 카운터 시각화

  • 프로세스 찾기: 탐색 목록에서 게임의 패키지 또는 프로세스 이름을 검색합니다.
  • 트랙 그룹 펼치기: 프로세스 행을 클릭하여 스레드 트랙을 펼치고 메모리라는 하위 그룹을 찾습니다.
  • 트랙 분석:
    • mem.rss.anon (익명 RSS): 이 선 그래프는 게임의 관리되지 않는 메모리 풀이 차지하는 실시간 실제 RAM을 보여 줍니다. 장면 로드, UI 팝업 또는 게임플레이 전환 중에 이 타임라인을 모니터링하여 높은 할당 피크를 확인합니다.
    • mem.swap (압축된 스왑 또는 VmSwap): 이 그래프는 zRAM으로 이동된 메모리 블록의 사전 압축된 크기를 표시합니다. 게임플레이와 동시에 발생하는 높은 스왑 활동은 게임이 제한된 메모리 기기에서 실행되고 있으며 시스템에서 백그라운드 애셋을 적극적으로 압축하고 있음을 나타냅니다.

2. SQL 쿼리 실행 (트레이스 프로세서) 자세한 오프라인 분석을 위해 Perfetto UI 콘솔 내에서 직접 SQL 쿼리를 실행하거나 독립형 트레이스 프로세서 Python 라이브러리를 사용하여 통계 피크를 계산할 수 있습니다.

  • 최대 익명 RSS 할당 찾기:

    SELECT
      max(value) / 1024 / 1024 AS max_rss_anon_mb
    FROM counter
    JOIN counter_track ON counter.track_id = counter_track.id
    WHERE counter_track.name = 'mem.rss.anon'
      AND counter_track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      );
    
  • 지정된 타임스탬프에서 RssAnon 및 VmSwap 상관관계 지정:

    SELECT
      ts,
      track.name AS metric_type,
      value / 1024 / 1024 AS size_mb
    FROM counter
    JOIN counter_track track ON counter.track_id = track.id
    WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap')
      AND track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      )
    ORDER BY ts ASC;
    

Android 스튜디오를 사용하여 트레이스 파일을 검사하는 방법에 관한 자세한 내용은 시스템 트레이스 검사: 프로세스 메모리 (RSS)를 참고하세요. 메모리 프로필 스크립팅에 관한 자세한 내용은 네이티브 할당 기록을 참고하세요.

heapprofd

heapprofd 는 메모리 추적 도구로, Perfetto의 일부입니다. 이 도구는 malloc을 사용하여 메모리가 할당된 위치를 표시함으로써 메모리 누수를 찾는 데 도움이 될 수 있습니다. heapprofd 는 Python 스크립트를 사용하여 시작할 수 있고 Malloc Debug와 같은 다른 도구처럼 성능에 영향을 미치지 않습니다(오버헤드가 낮기 때문).

bugreport

bugreport 는 메모리가 부족하여 게임이 비정상 종료되었는지 확인하는 로깅 도구입니다. 도구에서는 logcat을 사용하는 것보다 훨씬 자세한 내용을 출력합니다. 게임이 메모리가 부족하여 비정상 종료되었는지 또는 LMK로 인해 종료되었는지 보여 주므로 메모리 디버깅에 유용합니다.

자세한 내용은 버그 신고 캡처 및 읽기를 참고하세요.

게임 엔진 도구

플랫폼 수준 로그와 시스템 원격 분석은 OS 임계값 및 규정 준수를 추적하는 데 중요하지만 게임 엔진별 도구를 사용하면 할당을 게임 객체, 스크립트 동작, 활성 장면 계층 구조에 직접 귀속시킬 수 있습니다.

Unity

Unity 엔진 환경에서는 Unity의 네이티브 프로파일링 도구 및 클래스를 사용하여 런타임에 Android 익명 RSS + 스왑 메모리 사용량을 높은 안정성으로 추정할 수 있습니다 (일반적으로 실제 OS 수준 값과 비교하여 10% 미만의 차이를 표시함).

구성 규칙 및 런타임 스크립트를 비롯한 단계별 튜토리얼은 Unity 도구로 메모리를 확인하는 방법을 참고하세요.

  • Unity 프로파일러 API: 핵심 엔진 측정항목을 쿼리하여 런타임에 게임의 관리되지 않는 메모리 사용량을 프로그래매틱 방식으로 대략적으로 추정할 수 있습니다.
    • 프로파일러 클래스 사용: Profiler.GetTotalReservedMemoryLong()Profiler.GetMonoHeapSizeLong()의 값을 합산하여 총 메모리 할당을 추적합니다.
    • ProfilerRecorder 클래스 사용: 메모리 카테고리를 동적으로 모니터링합니다. 신뢰할 수 있는 기준 근사치를 설정하려면 총 예약 메모리(출시 빌드)를 가져오거나 개발 빌드에서 Gfx 예약 메모리를 빼서 파일 지원 그래픽 메모리 구성요소를 삭제합니다.
  • Unity 메모리 프로파일러: 오프라인에서 메모리 누수를 식별하고 디버그하려면 메모리 스냅샷을 캡처하고 기기의 상주 메모리 차트를 검사합니다 . 모든 메모리 섹션에 있습니다. 대략적인 공간을 계산하려면 추적되지 않음, Android 런타임, 네이티브, 관리됨 카테고리의 합계를 더합니다.
    • zRAM 제한: 메모리 조건이 엄격한 경우 Android 커널 은 비활성 메모리 페이지를 스왑 공간 (zRAM)으로 압축할 수 있습니다. Unity 메모리 프로파일러는 OS 수준 스왑 매개변수를 감지할 수 없으므로 메모리가 많은 장면에서 약간의 공간 불일치가 발생할 수 있습니다. Perfetto와 추정치를 교차 참조하여 정확한 값을 확인합니다.