앱이 크래시 없이 사라질 때 — lmkd 강제 종료와 oom_score_adj 읽기
"앱이 꺼졌는데 크래시 기록이 없다", "편성표를 다시 열면 처음부터 로딩된다", "백그라운드 음악이 끊겼다". 메모리가 넉넉하지 않은 셋톱박스·TV에서 자주 듣는 제보입니다. 대개 앱의 버그가 아니라 기기 전체 메모리가 부족해 시스템이 프로세스를 강제로 종료한 결과입니다. 이 기록은 오류(E) 레벨이 아니라서 오류만 세면 보이지 않습니다. 이 글은 그 기록을 찾고 읽는 법, 그리고 "누가 메모리를 차지했는지"까지 좁혀 가는 순서를 정리합니다.
1. 두 가지 메모리 부족은 다르다
| 앱 힙 초과 | 기기 전체 메모리 부족 | |
|---|---|---|
| 로그 | java.lang.OutOfMemoryError (E) | lowmemorykiller/lmkd의 Kill '...' (주로 I) |
| 누가 죽나 | 한도를 넘긴 그 앱(크래시로 종료) | 우선순위가 낮은 다른 프로세스들 |
| 원인 | 그 앱이 큰 이미지·버퍼를 붙들고 있음, 누수 | 기기 전체 사용량 — 범인은 죽은 앱이 아닐 수 있음 |
| 대시보드 패턴 | oom | lmk-kill + proc-died |
두 번째 경우에 강제 종료된 앱의 코드를 아무리 들여다봐도 원인은 나오지 않습니다. 그 시각 메모리를 많이 쓰던 다른 프로세스를 찾아야 합니다.
2. 강제 종료 기록 읽기
안드로이드는 원래 커널의 lowmemorykiller 드라이버가 이 일을 했고, 리눅스 커널 4.12에서 그 드라이버가 제거된 뒤로는 사용자 영역의 lmkd 데몬이 맡습니다. 그래서 기기에 따라 기록 형식이 두 가지입니다.
# 최근 형식 (lmkd, logcat 태그는 대개 lowmemorykiller)
I lowmemorykiller: Kill 'com.example.epg' (1650), uid 10031, oom_score_adj 900 to free 48212kB rss, 2048kB swap
# 옛 형식 (커널 드라이버 시절, 주로 커널 로그에 남음)
lowmemorykiller: Killing 'com.example.epg' (1650), adj 900, to free 48212kB on behalf of 'kswapd0' (29) ...
- 작은따옴표 안 — 종료된 프로세스.
- oom_score_adj(옛 형식은
adj) — 그 프로세스의 "버려도 되는 정도". 아래 3장. - to free ...kB — 종료로 확보한 메모리 양. 수십 MB짜리 프로세스가 연달아 종료된다면 부족분이 큽니다.
옛 형식은 커널 로그(dmesg)에만 남는 경우가 많아 logcat 파일에는 없을 수 있습니다. 이 대시보드는 logcat만 읽으므로, 그런 기기에서는 강제 종료가 있어도 패턴이 잡히지 않습니다.
강제 종료 직후에는 대개 시스템 서버가 ActivityManager: Process com.example.epg (pid 1650) has died: cch+5 CEM 같은 기록을 남깁니다. 반대로 has died는 있는데 앞에 크래시도 강제 종료도 없다면 다른 원인(네이티브 크래시, 앱 스스로 종료 등)을 찾아야 합니다.
3. oom_score_adj로 무엇이 죽었는지 가늠하기
값의 범위는 -1000~1000이고 클수록 먼저 종료됩니다. 안드로이드 프레임워크는 프로세스 상태에 따라 대략 다음 값을 매깁니다(정확한 값과 단계는 버전·제조사마다 다를 수 있음).
| 값(대략) | 상태 | 종료되면 사용자가 느끼는 것 |
|---|---|---|
| 음수 | 시스템·네이티브·상주 프로세스 | 거의 종료되지 않음. 종료됐다면 매우 심각 |
| 0 | 지금 화면에 보이는 앱 | 보고 있던 앱이 꺼짐 — 메모리 부족이 극심했다는 뜻 |
| 100~200 | 보이는 앱, 인지 가능한 앱(백그라운드 재생 등) | 배경 음악·라디오 끊김 |
| 500 안팎 | 서비스 프로세스 | 동기화·다운로드 같은 백그라운드 작업 중단 |
| 600~700 | 홈(런처), 직전에 쓰던 앱 | 홈으로 돌아가면 런처가 다시 그려짐, "뒤로"가 느림 |
| 900~999 | 캐시된 백그라운드 앱 | 다시 열면 처음부터 로딩 — 정상적인 동작에 가까움 |
900 이상의 캐시 앱이 가끔 종료되는 것은 안드로이드가 원래 메모리를 관리하는 방식이라 장애로 보기 어렵습니다. 문제는 값이 작은 프로세스까지 종료될 때입니다. 런처(600대)나 보이는 앱(0~100)이 종료됐다면, 그 시각 메모리 압박이 매우 컸다는 증거입니다.
4. 연쇄 종료 패턴 보기
한 건보다는 짧은 시간 안에 몇 건이 연달아 났는지가 중요합니다.
- 몇 초~수십 초 사이에 여러 건 — 무언가가 메모리를 한꺼번에 크게 요구했습니다. 고해상도 영상 재생 시작, 큰 이미지 목록 로딩, 앱 업데이트 설치가 흔한 계기입니다.
- 몇 분 간격으로 꾸준히 — 어떤 프로세스의 사용량이 계속 늘어나는 누수형일 가능성이 있습니다. 시간이 지날수록 종료되는 프로세스의 oom_score_adj가 점점 작아진다면 더 확실합니다.
- 같은 앱이 반복해서 종료됐다 다시 뜸 — 앱이 백그라운드에서 스스로 재시작(서비스 재시작 등)하며 메모리를 다시 차지하는 악순환일 수 있습니다.
대시보드의 감지된 장애 패턴 카드에서 메모리 확보용 강제 종료의 처음·마지막 시각과 대상 목록을 보면 이 패턴이 바로 드러납니다.
5. 누가 메모리를 차지했는지 확인하기
logcat만으로는 각 프로세스의 사용량을 알 수 없습니다. 증상이 재현되는 동안 다음 명령으로 상태를 함께 남겨 두세요.
adb shell dumpsys meminfo # 프로세스별 메모리 사용량(PSS) 순위
adb shell dumpsys meminfo com.example.player # 특정 앱의 자세한 내역
adb shell cat /proc/meminfo # 기기 전체: MemTotal, MemAvailable 등
adb shell getprop ro.config.low_ram # true면 저사양(low-RAM) 기기 설정
dumpsys meminfo를 평소와 증상 직전에 각각 받아 비교하면 늘어난 프로세스가 보입니다. 기준선 비교는 원인 구간 좁히기에서 설명한 방법과 같습니다.- 장시간 무인 테스트라면 일정 간격(예: 10분)으로
dumpsys meminfo를 파일로 남기는 스크립트를 함께 돌리면, 나중에 강제 종료 시각과 맞춰 볼 수 있습니다. - 영상 재생 중이라면 코덱·그래픽 메모리(앱 힙에 잡히지 않는 부분)가 큰 비중을 차지할 수 있습니다.
dumpsys meminfo의 Graphics·GL 항목을 함께 보세요.
6. 보고서에 남길 것
- 강제 종료 시각 범위와 건수 (예: 20:15:09~20:15:40, 2건)
- 종료된 프로세스와 각 oom_score_adj, 확보량(kB)
- 가장 작은 oom_score_adj (화면에 보이던 앱까지 종료됐는지)
- 같은 시각 사용자 동작 (재생 시작, 앱 설치, 목록 스크롤 등)
- 증상 전후 dumpsys meminfo 상위 5개 프로세스
- 기기 메모리 총량, low_ram 여부, 펌웨어 버전
정리
- 크래시 없이 사라진 앱은 먼저
Kill '·has died기록을 찾습니다. 오류 레벨이 아니므로 패턴으로 찾아야 합니다. - oom_score_adj 900 이상의 산발적 종료는 정상에 가깝고, 작은 값의 종료와 연쇄 종료가 문제입니다.
- 원인은 종료된 앱이 아니라 그 시각 메모리를 쓰던 쪽에 있습니다.
dumpsys meminfo를 증상 전후로 남기세요.
참고: lowmemorykiller 드라이버 제거와 lmkd 전환은 Android Open Source Project의 lmkd 문서(source.android.com의 "Low memory killer daemon")에 설명돼 있습니다. oom_score_adj 단계 값은 안드로이드 프레임워크의 프로세스 우선순위 정의를 기준으로 한 대략값입니다.
· 실습: 셋톱박스 2대의 로그를 대시보드로 읽어 보기
· 네이티브 크래시 읽는 법
· 저장된 로그에서 원인 구간 좁히기
· 아티클 목록 · 패턴 사전 · 서비스 소개
문의: dkdnj123@gmail.com