안드로이드의 메모리 릭(Memory leak)에 대해 정리합니다. 자세히 설명하고자 하였으나, 주제에 집중하는 과정에서 부가적인 부분의 설명은 모자랄 수 있습니다. 제가 도움을 받았던 좋은 글들을 문서의 하단에 연결해두었으니, 함께 읽으신다면 도움이 될 것 같습니다.
개인적으로 메모리 릭보다 메모리 누수가 적당한 단어라고 생각하지만, 더 많이 사용하는 릭으로 표현합니다.
메모리 릭(Memory leak)이란?
어플리케이션은 동작을 위해 메모리가 필요합니다. 어플리케이션이 사용이 끝난 메모리를 반환하지 않는 것을 메모리 릭이라고 합니다. 보통 릭이 발생하면, 사용한 메모리는 반환하지 않은 채 추가로 필요한 메모리를 시스템에 요청하기 때문에 사용하는 메모리 양이 계속 증가합니다. 이 과정이 계속되면, 최악의 경우 OOM(Out Of Memory)를 발생시키고 어플리케이션이 강제 종료되면서 어플리케이션에 할당되었던 메모리가 시스템으로 회수됩니다.
자바의 메모리 릭
자바는 C/C++과 달리 사용한 메모리를 명시적으로 해제(시스템에 반환)하지 않습니다. 대신 GC(가비지 콜렉션, Garbage Collection)가 이를 대신합니다.
간혹, 자바는 GC를 하니 메모리 릭이 발생하지 않는다고 생각하는 분이 있습니다. GC가 사용하지 않는 자원을 스스로 수집하는 것은 맞지만, 이는 사용하지 않는 메모리가 성공적으로 모두 회수될 때 맞는 이야기입니다. 다시 말해, 어플리케이션이 사용하지 않는 메모리라도 GC 실행 시 시스템이 사용중인 메모리로 판단하고 회수하지 않는다면, 여전히 메모리 릭은 발생할 수 있습니다. 그래서 자바는 보통 메모리 릭이 발생하지 않지만, 한번 발생하면 찾기가 더 어렵다고 말하는 분도 있습니다.
안드로이드의 메모리 릭
안드로이드 앱의 메모리가 누수되는 과정은 대체로 자바와 같습니다. 다만, 안드로이드 플랫폼의 특징에서 오는 몇 가지 차이점이 있습니다.
먼저 시스템이 앱에 할당하는 메모리 제한이 있습니다. 이 제한 값은 단말의 램 크기에 따라 달라집니다. 그러므로, 어플리케이션이 사용할 수 있는 최대 메모리 크기를 넘어서면 OOM이 발생합니다. 자신의 앱이 어느 정도까지 쓸 수 있을지 확인하고 싶다면, getMemoryClass() 메소드를 사용할 수 있습니다.
모바일 기기들은 페이징을 하지 않으므로 안드로이드 앱에서 사용하는 모든 객체는 RAM에 상주 합니다. 이는 모바일 기기에서 페이징을 하는 비용이 비싸기 때문입니다.
안드로이드에서 발생하는 릭은 보통 액티비티나 이미지와 함께 발생합니다. 액티비티와 이미지가 가장 크게 메모리를 차지하기 때문입니다. 액티비티는 안드로이드 앱이 운용하는 객체 중에 가장 큰 객체 입니다. 동시에, 사용자와 상호 작용을 통해 동작하는 안드로이드 앱의 특성상 가장 빈번하게 생성/삭제 됩니다. 이미지의 경우 최근에 출시되는 단말들의 해상도는 집의 티비나 모니터보다 해상도가 높습니다. 해상도가 높은 만큼 이미지는 많은 메모리를 차지하므로 잘못 관리하면 메모리 문제를 만나기 쉽습니다.
릭이 있는 것 같은데 시간(수십분 ~ 수시간 내)이 지나면 정리가 되는 것 같다면, 안드로이드의 정책을 살펴볼만 합니다. 앱이 포그라운드(foreground) 상태에서 벗어나면 안드로이드 시스템은 LRU 캐시에 프로세스 정보를 저장합니다. 그리고 메모리 부족 시 최소한의 상태 정보(최근 실행 목록 정보 등)만 남긴 후 프로세스를 죽여 메모리를 회수합니다. 이처럼 사용자가 명시적으로 프앱을 종료하지 않더라도 시스템이 정리하기 때문에 OOM을 만나지 않을 수도 있습니다.
다만, 메모리 릭이 크다면 OOM까지는 아니더라도 GC가 실행되기 마련이고 이는 처리 속도가 느려지거나 화면이 버벅거리게(Sluggish)되어 사용자가 느낄 수도 있겠죠. 물론, 적은 양의 메모리 릭이라도 그래프나 수치를 보여주는 검증 전문 도구를 사용한다면 금새 발견할 수 있습니다.
메모리 릭 확인하기
메모리 릭을 수정하려면 먼저 재현을 해야 합니다. 재현을 위해서는 단순하면서 확실한 재현 경로를 먼저 확보하는 것이 중요합니다. 요즘은 검증자가 따로 있는 회사가 많고, 검증자들은 미리 테스트 플로우를 정하고 반복적인 테스트를 통해 메모리 릭 여부를 검증하기 마련이므로 메모리 릭이 발생했다면, 보통 재현이 됩니다.
메모리 릭을 재현하는 방법은 아래와 같이 몇 가지가 있습니다.
- 손
개발자나 검증자가 직접 합니다. 이미 발견된 메모리 릭을 재현해보는 목적으로는 적당하지만, 있을지 모르는 메모리 릭을 찾는 방법으로는 적당하지 않습니다. - 스크립트
안드로이드는 adb shell을 이용해, 다양한 동작을 실행할 수 있습니다. 간단한 스크립트와 adb shell 명령어를 조합하면 쉽게 테스트를 할 수 있습니다. 스크립트 예제는 하단의 ‘그 밖에’를 참고하세요. - 전문 메모리 검증 앱
서비스나 액티비티를 반복적으로 실행하면서, 메모리 증가량 등을 모니터링하는 도구들도 있습니다. 이런 도구들은 화면을 터치한 효과나, 잠시 기다리는 동작, 필요하다면 메모리 추이도 따로 저장하기 때문에 매우 유용합니다. - 몽키러너
안드로이드 테스트 툴인 몽키러너를 사용하면 스크립트보다 더 섬세하게 테스트를 할 수도 있습니다.
위의 방법들 모두 테스트를 편하게 하는 목적이지, 이것으로 재현이 안되면 저것으로 해야하는 것은 아닙니다. 빠른 툴로 테스트했을 때 발생하는 메모리 릭이 느린 손으로 할 때 재현이 안되는 경우는 없습니다. 만약, 도구를 사용하는 빠른 테스트에서만 재현된다면 아래의 메모리 릭으로 오해되는 경우에 해당하는 것은 아닌지 확인해 보세요.
메모리 변화 확인하기
저는 두 가지 방법을 사용합니다. 하나는 meminfo이고, 또 다른 하나는 안드로이드 스투디오의 메모리 모니터 입니다.
meminfo는 쉘 커맨드로 아래와 같이 사용할 수 있습니다.
>adb shell dumpsys meminfo <패키지 이름|프로세스아이디>
meminfo는 명령어를 실행할 때 당시의 메모리 상황을 알려주는 것이기 때문에, 일정 횟수(예를 들어, 10회) 테스트 후 한번 씩 meminfo를 확인하면 추이를 볼 수 있습니다. 이 때 주의할 점은, 패키지 이름으로 프로세스를 구분하면 프로세스가 죽고 다시 살아난 경우에도 meminfo가 출력됩니다. 이미 메모리가 시스템에 반환된거죠. 패키지이름 대신 프로세스 아이디(pid)를 직접 전달할 수 있으니 참고하세요. meminfo에서 출력하는 각 항목에 대한 소개는 이전 글을 meminfo 소개 부분을 참고 부탁드립니다.
안드로이드 스투디오는 메모리 모니터라는 기능을 가지고 있습니다. 안드로이드 스투디오의 “Android Monitor > Monitors 탭 > Memory 그래프”를 보면 됩니다. 현재 실행중인 앱의 정보를 실시간으로 보여줍니다. 참고로, 메모리 뿐 아니라 GPU와 Network, CPU도 동시에 볼 수 있습니다. 또한, 다양한 기능을 제공하기 때문에 사용법을 알아두면 유용합니다. 이 기능에 대한 소개는 안드로이드 스투디오에서 제공하는 이 글을 참고 하세요.
위에 언급한 것처럼 자바는 GC를 통해 메모리를 관리하고 있습니다. 그러므로, 톱니 모양의 메모리 사용량의 등락은 있을 수 있습니다. 하지만 같은 동작을 반복할 때마다 그래프의 추세가 증가한다면 이는 메모리 릭이 있다고 보면 됩니다.
메모리 릭으로 오해할 수 있는 것들
- 캐시
정확한 테스트를 위해 앱의 프로세스를 종료하고 테스트를 실행하는 경우가 있습니다. 만약, 메모리 캐시를 쓰고 있다면 캐시가 찰 때까지는 메모리가 증가할 수 있습니다. 이 때는, 캐시를 끄거나 반대로 캐시가 모두 찬 후 메모리 증가 추이를 확인하는 것이 좋습니다. - 비동기 작업
뒤로 가기 등을 통해 명시적으로 앱을 종료한 후에도 잠시동안 남은 작업이 실행될 수 있습니다. 올바르게 예외처리가 되어 있다면, 곧 작업이 마무리되고 자원도 반환도 반환될 것입니다. - 정상적인 메모리 증가
네트워크 라이브러리가 병렬 작업을 위해 스레드를 생성하는 경우처럼, 정상적으로 메모리 사용량이 증가 하기도 합니다. 물론 계속 증가한다면 수정을 해야겠죠?
수정하기
메모리 릭이 맞다고 확인되면 수정을 해야합니다. 그런데 어디를 수정해야할지 찾는 것은 어렵습니다. 안드로이드 앱이 예전처럼 화면에 뷰 몇개 얹는 정도가 아니기 때문에 정확히 어디인지 찾기가 어렵죠. 어느 액티비티에서 발생하는지는 재현을 통해 확인했지만, 코드 상 위치를 정확히 모르겠다면 액티비티 안의 프레그먼트, 레이아웃, 뷰 단위로 하나씩 뗐다 붙였다 하면서 재현을 해봅니다. 그리고, 재현이 되는 부위를 좁혀가는 것이 좋습니다.
일단 찾고 나면, 수정은 싱겁게 끝납니다. 참조를 명시적으로 끊어주거나, 리스너나 콜백을 해제시키면 됩니다. 액티비티가 onDestory() 호출을 보장하지 않는 것처럼 컴포넌트의 생명주기 때문에 예상대로 동작하지 않는 특이한 경우가 있을 수 있습니다.
그 밖에
아래와 같은 간단한 스크립트를 통해, 유투브 앱의 ‘메인 화면 진입 – 나가기’을 반복하는 테스트를 할 수 있습니다.
:Loop adb shell am start com.google.android.youtube timeout 3 adb shell input keyevent BACK GOTO Loop
참고로 나갈 때는 홈키 대신, 뒤로가기(back)을 눌러야 합니다. 홈키를 누르면 앱은 onPause() 까지만 호출되고, 뒤로가기를 눌러야 onDestroy()까지 호출됩니다.
로우레벨 개발을 하지 않는다면어플리케이션은 dalvik heap 부분만 사용하게 됩니다. 직접 메모리를 제어하지 말라는 것은 구글의 권장사항이기도 하죠. dalvik heap은 증가하지 않은 채 native heap만 증가한다면 어플리케이션이 제어할 수 없는 영역일 수 있습니다.
틀린 내용이나 부족한 부분이 있다면, 알려주세요. 업데이트 하도록 하겠습니다.
참조
- https://ko.wikipedia.org/wiki/메모리_누수
- http://www.kmshack.kr/2017/03/안드로이드의-메모리-누수-패턴/
- http://d2.naver.com/helloworld/539525
- https://developer.android.com/topic/performance/memory-overview.html
- https://developer.android.com/topic/performance/graphics/manage-memory.html
- https://developer.android.com/reference/android/app/ActivityManager.html#getMemoryClass()
- http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
- https://sunphiz.me/wp/archives/313
- https://sunphiz.me/wp/archives/1794
- https://developer.android.com/studio/profile/am-hprof.html
- https://developer.android.com/studio/test/monkeyrunner/index.html