현재 앱에서 Memory Leak이 발생하는 상황이나 기준에 대해 알기 위해 궁금했던 것들 4편(1) - Garbage Collection에서 JVM Garbage Collection에 대해 다뤘다. 필자는 왜 GC를 다뤘을까?
우선, 불필요한 메모리가 해제되지 않아 성능 저하가 발생하는 것이 Memory Leak이다. 그리고 JVM 상에서 불필요한 메모리를 해제해주는 녀석이 GC(Garbage Collector)이다.
그렇다면 이번 글에서 다룰 내용은 '안드로이드에서의 GC 수행'에 관한 내용이다. 어떤 시점에서 GC가 수행하지 않아, 불필요한 객체가 메모리 상에 남아 있게 되는지에 대해 알아볼 것 같다.
일단 공부를 하면서 생각을 해봤는데, 위의 'GC가 수행되지 않는다'라는 표현엔 오류가 있지 않나 싶다. 해당 표현은 애초에 GC가 정상적으로 동작하지 않는 상황이고, 이는 말 그대로 프로세스 내지 시스템 그 자체의 오류를 의미하기 때문이다. 필자의 'Android GC가 수행되지 않는 상황'이란 표현은 사실 아래의 표현이 정확할 것이다. 맞나?
GC에 의해 자동으로 해제될 것이라 생각했던 객체 인스턴스를 수동으로 해제해주지 않은 상황
이유를 들겠다. 필자는 지금까지 안드로이드 개발 및 공부를 진행해오면서 Memory Leak 가능성에 대한 내용을 직/간접적으로 접해왔다. 이러한 가능성이 있을 때 마다 위의 경우에 해당했다. 지금까지 접해온 'Android Memory Leak이 발생할 수 있는 상황'들은 다음과 같다.
Application Context는 기본적으로 Application LifeCycle에 종속되어 있는 싱글턴 인스턴스(인스턴스가 오직 1개만 생성되는 객체)이다. 따라서 현재 액티비티의 LifeCycle이 종료(destroy) 되더라도 GC에 의해 자동 해제되지 않는다. 이러한 특성 때문에 액티비티 수준이 아닌 애플리케이션 수준에서 생성한 싱글턴 오브젝트가 Context를 필요로 하는 경우 Application Context를 사용해야 한다. 싱글턴 인스턴스에서 Activity Context를 사용하면 어떻게 될까?
싱글턴(액티비티 컨텍스트)
싱글턴 -> 액티비티 컨텍스트 : 누수, Reachable
QnA
Q1 : "싱글턴 인스턴스에 Activity Context를 전달한 경우, 액티비티의 화면이 표시되지 않는 순간에도 해당 인스턴스가 액티비티를 참조하며, 액티비티의 메모리 누수가 발생할 수 있다"라는 내용을 읽었는데요. 이미 Activity Context를 전달해버린 이상, 해당 인스턴스는 액티비티의 수명주기를 따르게 되면서, 문제가 발생하지 않는 것 아닌가요?
A1 : 싱글턴의 인스턴스에 들어가고 액티비티가 종료되면 이제는 사용하지 않은 액티비티 이기때문에 메모리에 회수가 되어야 겠죠! 물론 메모리에 회수 되려면 해당 액티비티에 대해 참조가 모두 끊어져야하구요.
근데 사용하지도 않는 이미 종료된 액티비티 컨텍스트가 싱글턴 인스턴스에서 사용하는 것처럼 참조가 남아 있게 되어 GC 입장에서는 알 방법이 없기 때문에 해제 하지 못하고 메모리릭으로 이어지게 됩니다.
Q2 : 싱글턴 인스턴스의 특성 때문에 이런 일이 발생한건가요? "싱글턴 인스턴스가 다른 곳에서 사용되는, 즉 참조되는 상황(코드)들이 남아 있게 되어서 GC가 인지하지 못한다." 라는 말씀으로 이해해도 될까요?
A2 : 싱글턴이 회수되면 액티비티 컨텍스트의 레퍼런스 카운트가 0 이 되니 GC 에서 해제할 수 있겠지만 싱글턴 특성으로 인해 해제 되기 어렵기 때문에 특정 조건에서 null 을 명시적으로 주던가, weakreference 를 사용하면 될 것 같아요 베스트를 액티비티 컨텍스트를 직접 사용할 일이 없도록 풀어내면 좋구요.
바인딩 클래스 인스턴스는 프래그먼트의 onDestroyView 콜백 메서드 내에서 null처리를 해줘야했다. 이를 하지 않으면 fragment replace 시 해당 인스턴스가 남아 메모리 누수가 발생할 수 있다. 프래그먼트가 destroy되면 GC가 인스턴스들을 모두 메모리 해제해줘야 하지 않나? 이상함을 감지했다. 나는 곧장 "fragemnt replace memory leak"에 대해 알아보기 시작했다.
알아 본 결과, 새로운 것을 알게 됐다. fragment replace 시, 해당 프래그먼트에서 onDestroyView() 콜백까지만 호출되고 onDestory() 콜백은 호출되지 않는다. 즉, 프래그먼트의 View만 destory(파괴)되었다는 것이다. 해당 프래그먼트의 인스턴스들은 살아 있다. 그리고 프래그먼트는 정리되지 않았던 바인딩 클래스 인스턴스를 여전히 참조하고 있다. 바인딩 클래스 인스턴스는 replace됐으므로 더 이상 사용될 필요가 없는데 말이다. 따라서 GC는 바인딩 클래스 인스턴스를 Reachable하다고 판단한다. GC에 의해 메모리 해제가 되지 않고 바인딩 클래스 인스턴스로부터 메모리 누수가 발생하는 것이다.
프래그먼트(바인딩클래스)
프래그먼트 -> 바인딩클래스 : 누수, Reachable