이전 섹션에서는 커널이 시스템 전체의 메모리를 관리하는 방법을 살펴보았습니다. 이 장에서는 Android에서 개별 앱의 메모리 사용량을 분할하고 제어하는 메커니즘인 메모리 제어 그룹 (memcg)에 관해 자세히 알아봅니다.
memcg는 시스템이 회수할 개별 앱을 타겟팅하거나 앱에 메모리 한도를 설정하여 앱 메모리 사용 공간을 사전에 관리할 수 있는 수준이므로 memcg를 이해하는 것이 중요합니다.
메모리 제어 그룹 (memcg)
통제그룹 (cgroup)은 프로세스를 계층적 그룹으로 구성하고 시스템 리소스 (예: CPU, 메모리, I/O)를 그룹 간에 분산할 수 있는 Linux 커널 기능입니다. memcg 는 메모리 전용 cgroup 컨트롤러입니다.
주요 memcg 개념
- Memcg Charge: memcg의 프로세스가 메모리 페이지 (익명 또는 파일 지원)를 할당하면 해당 페이지가 memcg에 "청구"됩니다. memcg의 총 청구액은 memcg 내의 모든 프로세스에서 사용하는 모든 페이지의 합계입니다.
- 계층적 회계: 메모리 사용량은 트리 위로 계산됩니다. 하위 cgroup의 청구액도 상위 cgroup의 사용량에 포함됩니다.
- 메모리 한도: 각 memcg에는 전역 시스템 메모리와 관계없이 회수 또는 OOM 킬러를 트리거하는 한도 (예:
memory.max또는memory.high)가 있을 수 있습니다. - memcg별 회수: memcg가 한도를 초과하거나 시스템 에 메모리가 필요한 경우 커널은 회수할 특정 memcg를 타겟팅할 수 있습니다. 즉, 파일 페이지를 삭제하거나 익명 페이지를 ZRAM으로 스왑합니다.
Android memcg 계층 구조
Android는 특정 계층 구조를 사용하여 앱 프로세스를 관리합니다. 이 구조를 사용하면 시스템에서 다양한 유형의 앱 (예: 포그라운드와 백그라운드)에 서로 다른 정책을 적용할 수 있습니다.

/sys/fs/cgroup/apps/: 모든 Android 애플리케이션의 루트입니다.uid_<UID>/: 각 앱의 사용자 ID 디렉터리입니다. 동일한 앱 패키지에 속한 모든 프로세스가 이 그룹을 공유합니다.pid_<PID>/: 각 개별 프로세스 및 포크된 모든 하위 요소 의 디렉터리입니다. 이를 통해 여러 프로세스가 있는 앱을 세밀하게 제어하고 계산할 수 있습니다.
실습: memcg 탐색
이 실습에서는 MemoryLab 의 memcg 디렉터리를 찾고 실시간으로 메모리 청구액을 관찰합니다.
1. MemoryLab 실행
기기에서 MemoryLab 이 실행 중인지 확인합니다.
2. MemoryLab의 memcg 찾기
먼저 실행 중인 MemoryLab 앱의 PID를 가져옵니다.
adb shell pidof com.android.memorylab
# Example output: 11672
이제 cgroup 디렉터리를 찾습니다. /proc/<PID>/cgroup에서 찾을 수 있습니다.
adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672
cgroup v2에서 0:: 뒤의 경로는 /sys/fs/cgroup을 기준으로 하는 memcg 계층 구조를 나타냅니다. 따라서 전체 경로는 /sys/fs/cgroup/apps/uid_10274/pid_11672/입니다.
3. memcg 통계 읽기
해당 디렉터리로 이동하여 키 파일을 살펴봅니다.
# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672
memory.current 는 현재 이 cgroup에 청구된 총 메모리 양 (바이트)을 보여줍니다.
# Detailed statistics
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.stat | head -n 10
# Example output:
# anon 1585098752
# file 741376
# kernel 5484544
# kernel_stack 393216
# pagetables 4583424
# sec_pagetables 0
# percpu 216
# sock 0
# vmalloc 4096
# shmem 20480
memory.stat는 다음과 같은 분석을 제공합니다. * anon: 익명 메모리 양
(힙, 스택) * file: 파일 지원 메모리 양 (페이지 캐시) *
swap: ZRAM으로 스왑된 메모리 양
4. 변경사항 관찰
- MemoryLab 을 엽니다.
memory.current의 값을 확인합니다.- 자바 메모리 할당 (10MB) 을 여러 번 탭합니다.
memory.current를 다시 읽습니다. 탭할 때마다 약 10MB씩 증가하는 것을 확인할 수 있습니다.- 비트맵 할당 을 탭합니다.
memory.stat를 확인하여anon이 증가하는지 확인합니다.
memory.reclaim을 사용한 사전 회수
memcg (v2)의 강력한 기능은 memory.reclaim 파일입니다. 이 파일에 값을 쓰면 커널이 이 memcg 및 그 아래의 모든 memcg 에서 해당 메모리 양을 즉시 회수하도록 지시합니다.
Android에서 memory.reclaim을 사용하는 방법
Android의 CachedAppOptimizer 는 이 기능을 사용하여 앱이 고정 된 후 앱에서 메모리를 최대한 회수합니다. Android 앱 냉동 은 캐시된 앱이 실행되지 않는 동안 가능한 한 적은 RAM을 사용하도록 합니다. 앱이 백그라운드로 이동하여 고정되면 시스템은 앱의 현재 메모리 사용량을 memory.reclaim 파일에 씁니다. 이렇게 하면 커널이 가능한 모든 파일 페이지를 삭제하고 모든 익명 페이지를 ZRAM으로 스왑하여 앱의 상주 사용 공간을 최소화합니다.
memory.current를 읽고 memory.reclaim에 다시 쓰는 방식으로 동일한 '최대 회수'를 수동으로 달성할 수 있습니다.
# Force reclaim of everything
adb shell "cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
실습: 강제 회수 및 트레이스
이제 커널이 MemoryLab에서 메모리를 회수하도록 강제하고 Perfetto 트레이스에서 활동을 캡처합니다.
- 준비: MemoryLab에 할당 (자바 및 비트맵)이 있는지 확인합니다.
트레이스 시작: 이 인라인 트레이스 구성을 사용하여 예약, 회수, 페이지 캐시 이벤트를 캡처합니다.
# Use a config that captures scheduling, reclaim and page cache events adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/reclaim.perfetto-trace <<EOF buffers: { size_kb: 131072 fill_policy: RING_BUFFER } data_sources: { config { name: "linux.ftrace" ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "sched/sched_wakeup" ftrace_events: "kmem/rss_stat" ftrace_events: "mm_filemap_add_to_page_cache" ftrace_events: "mm_filemap_delete_from_page_cache" ftrace_events: "vmscan/mm_vmscan_direct_reclaim_begin" ftrace_events: "vmscan/mm_vmscan_direct_reclaim_end" ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_begin" ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_end" symbolize_ksyms: true } } } data_sources: { config { name: "linux.process_stats" process_stats_config { scan_all_processes_on_start: true } } } EOF압력 및 회수 트리거:
- MemoryLab에서 네이티브 메모리 할당 (1GB) 을 탭합니다. 이렇게 하면 시스템 전체 압력이 트리거됩니다.
별도의 터미널에서 200MB를 회수합니다.
adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
오류 발생: MemoryLab 으로 다시 전환합니다. 페이지 캐시 스래싱(오류 재발생 테스트) 을 탭합니다.
트레이스 중지: 트레이스 터미널에서 Ctrl+C를 누릅니다.
Perfetto에서 회수 분석
트레이스를 열면 시스템 전체 회수와 앱별 회수가 모두 작동하는 것을 확인할 수 있습니다.

트레이스에서 다음을 찾습니다.
- kswapd: 커널 스레드에서
kswapd0을 검색합니다 (위 스크린샷 에서는 상단에 수동으로 고정됨). 시스템이 사용 가능한 페이지를 찾는 데 어려움을 겪으면서 절전 모드에서 깨어나 실행되는 것을 확인할 수 있습니다 (녹색 슬라이스). - 직접 회수:
com.android.memorylab프로세스 스레드를 살펴봅니다. 스레드의 예약 트랙 바로 아래에 보라색 ftrace 이벤트 슬라이스 (예:mm_vmscan_direct_reclaim_begin)가 표시됩니다. 이는 앱 스레드 자체가 커널이 페이지를 확보할 때까지 대기 중임을 나타냅니다. - RSS 및 스왑 카운터:
mem.rss.anon: 할당 버튼을 탭하면 증가합니다.mem.swap:kswapd및 앱 자체 스레드(직접 회수 대상)가 이러한 익명 페이지를 ZRAM으로 압축함에 따라 꾸준히 증가합니다.
- memcg 회수: 수동 회수를 트리거한 순간을 확대하면
rss.anon과rss.file이 모두 급격히 감소하고mm_vmscan_memcg_reclaim이벤트가 함께 표시됩니다.
← 시스템 전체 | ↑ 위로 | kswapd 및 lmkd 상호작용 →