아이템 48 - 더 이상 사용하지 않는 객체의 레퍼런스를 제거하라
메모리 관리를 자동으로 해준다고 해서 메모리 관리를 완전히 무시해 버리면, 메모리 누수가 발생하여 `OutOfMemoryError`가 발생할 수 있습니다. 따라서 '더 이상 사용하지 않는 객체의 레퍼런스를 유지하면 안 된다'라는 규칙을 지키는 것이 좋습니다.
안드로이드에서는 Activity를 여러 곳에서 자유롭게 접근하기 위해서 companion 프로퍼티에 이를 할당해 두는 경우가 있습니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity = this
}
companion object {
// 이렇게 하면 메모리 누수가 발생하니 하지 마라
var activity: MainActivity? = null
}
}
이렇게 객체 참조를 companion으로 유지해 버리면, 가비지 컬렉터가 해당 객체에 대한 메모리 해제를 할 수 없습니다. 그렇기 때문에 이러한 리소스를 정적으로 유지하지 않는 것이 좋습니다. 또한 객체에 대한 레퍼런스를 다른 곳에 저장할 때는 메모리 누수가 발생할 가능성을 언제나 염두에 두기 바랍니다.
아래의 코드에는 문제가 있습니다.
class Stack {
private var elements: Array<Any?> = arrayOfNulls(DEFAULT_INITIAL_CAPACITY)
private var size = 0
fun push(e: Any) {
ensureCapacity()
elements[size++] = e
}
fun pop(): Any? {
if (size == 0) {
throw EmptyStackException()
}
return elements[--size]
}
private fun ensureCapacity() {
if (elements.size == size) {
elements = elements.copyOf(2 * size + 1)
}
}
companion object {
private const val DEFAULT_INITIAL_CAPACITY = 16
}
}
문제는 pop을 할 때 size를 감소시키기만 하고, 배열 위의 요소를 해제하는 부분이 없다는 것입니다. 스택에 1000개의 요소가 있을 때, 위의 코드는 1000개의 요소를 모두 붙들고 놓아주지 않으므로, 가비지 컬렉터가 이를 해제하지 못합니다.
실질적으로 요소 하나만 사용하지만, 999의 요소가 메모리를 낭비하게 됩니다. 이를 해결하는 것은 간단합니다. 객체를 더 이상 사용하지 않을 때, 그 레퍼런스에 null을 설정하기만 하면 됩니다.
fun pop(): Any? {
if (size == 0) {
throw EmptyStackException()
}
val elem = elements[--size]
elements[size] = null
return elem
}
다른 예시로 아래의 코드를 보겠습니다. 아래의 코드는 변경 가능한 lazy 속성을 제공하는 mutableLazy 함수를 정의합니다.
fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T> = MutableLazy(initializer)
private class MutableLazy<T>(
val initializer: () -> T
): ReadWriteProperty<Any?, T> {
private var value: T? = null
private var initialized = false
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
synchronized(this) {
if (!initialized) {
value = initializer()
initialized = true
}
return value as T
}
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
synchronized(this) {
this.value = value
initialized = true
}
}
}
위의 코드에서 initializer는 사용된 후에도 해지되지 않습니다. 이는 사용 후에 initalizer를 null로 설정하기만 하면, 가비지 컬렉터가 이를 처리할 수 있습니다.
거의 사용되지 않는 개체까지 최적화 처리를 하는 것은 좋지 않을 수도 있습니다. 하지만 오브젝트에 null을 설정하는 것은 그렇게 어려운 일이 아니므로, 무조건 하는 것이 좋습니다. 특히 많은 변수를 캡처할 수 있는 Any 또는 제네릭 타입과 같은 미지의 클래스일 때는 이러한 처리가 중요합니다.
class DataHolder<T> {
private var data: T? = null
fun setData(value: T) {
data = value
}
fun clear() {
data = null // 명확하게 null 처리하여 GC가 수거할 수 있도록 함
}
}
일반적인 규칙은 상태를 유지할 때는 메모리 관리를 염두에 두어야 한다는 것입니다. 코드를 작성할 때에는 '메모리와 성능' 뿐만 아니라 '가독성과 확장성'을 항상 고려해야 합니다. 일반적으로 가독성이 좋은 코드는 메모리와 성능적으로도 좋습니다. 예외적으로 라이브러리를 구현할 때는 메모리와 성능이 더 중요합니다.
일반적으로 메모리 누수가발생하는 상황
절대 사용되지 않는 객체를 캐시해서 저장해 두는 경우
캐시를 해두는 것이 나쁜 것은 아니지만, OutOfMemoryError를 일으킬 수 있다면 도움이 되지 않습니다. 해결 방법은 Soft Reference를 사용하는 것입니다.
Soft Reference는 Weak Reference와 유사하지만 GC가 즉시 수거하지 않고 메모리가 부족할 때만 수거하는 참조 방식입니다.
아래의 예시에서 이 둘이 어떻게 다르게 작동하는지 보여줍니다. 아래는 Soft Reference입니다.
fun main() {
var obj: MyClass? = MyClass("Hello, Soft Reference!")
val softRef = SoftReference(obj) // 소프트 참조 생성
obj = null // 강한 참조 제거
System.gc() // 가비지 컬렉터 실행 요청 (소프트 참조는 바로 수거되지 않음)
Thread.sleep(100)
println(softRef.get()) // MyClass@78308db1 출력
}
아래는 Weak Reference입니다.
fun main() {
var obj: MyClass? = MyClass("Hello, Soft Reference!")
val softRef = WeakReference(obj) // 약한 참조 생성
obj = null // 강한 참조 제거
System.gc() // 가비지 컬렉터 실행 요청 (약한 참조는 바로 수거됨)
Thread.sleep(100)
println(softRef.get()) // null 출력
}
사실 객체를 수동으로 해제해야 하는 경우는 굉장히 드뭅니다. 일반적으로 스코프를 벗어나면서, 어떤 객체를 가리키는 레퍼런스가 제거될 때, 객체가 자동으로 해제됩니다. 따라서 메모리와 관련된 문제를 피하는 가장 좋은 방법은 변수를 지역 스코프에 정의하고,
fun createAndUseObject() {
val data = ByteArray(10_000_000) // 10MB 크기의 배열
println("객체 생성 및 사용 중...")
} // 함수 종료 시 data는 스코프를 벗어나면서 자동으로 해제됨
톱레벨 프로퍼티 또는 개체 선언으로 큰 데이터를 저장하지 않는 것입니다.
val globalData = ByteArray(10_000_000) // 10MB 크기의 배열 (톱레벨 프로퍼티)
fun main() {
println("톱레벨 프로퍼티를 사용하면 GC가 바로 해제하지 못할 수 있음")
}