ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 48장 - 더 이상 사용하지 않는 객체의 레퍼런스를 제거하라
    Kotlin/Effective Kotlin 요약 2023. 4. 12. 00:01

    개요

    보통 GC가 메모리 관리를 자동으로 해주기 때문에 객체의 해제를 따로 생각하지 않습니다.

    하지만 메모리 관리를 전혀 신경 쓰지 않으면 메모리 누수가 발생하여 상황에 따라 OutOfMemoryError가 발생하기도 합니다.

    따라서 ' 더 이상 사용하지 않는 객체의 레퍼런스를 유지하면 안 된다'라는 규칙 정도는 지켜 주는 것이 좋습니다.

     

    안드로이드 개발자가 많이 하는 실수

    Activity(데스크톱 애플리케이션의 창)을 여러 곳에서 자유롭게 접근하기 위해 companion 프로퍼티에 이를 할당해 두는 경우가 많습니다.

    하지만 이렇게 되면 GC가 해당 객체에 대한 메모리를 해제할 수 없습니다.

    액티비티는 굉장히 큰 객체로 큰 메모리 누수가 발생하게 됩니다.

     

    책에서 보여주는 메모리 누수의 예시로는 companion object에 var으로 필드를 설정하고 해당 필드를 계속 할당하는 경우 메모리 누수가 발생한다고 설명합니다.

     

    미묘한 문제로 발생하는 메모리 누수

    간단한 스택 구현에서 메모리 누수가 발생할 수 있습니다.

    class Stack{
        private var elements: Array<Any?> = arrayOfNulls(DEFUALT_INITIAL_CAPACITY)
        private var size = 0
        
        fun push(e: Any){
            ensureCapacity()
            elements[size++] = e
        }
    
        fun pop(e: Any): 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 DEFUALT_INITIAL_CAPACITY = 16
        }
    }

    개인적으로 생각했을 때는 companion object로 16이 설정되어 있으며, 가변 프로퍼트인 var에서 기본값으로 사용하기 때문에 메모리의 해제가 일어나지 않는가?라고 생각했습니다.

     

    문제는 pop을 할 때 size를 감소시키기만 하고, 배열 위의 요소를 해제하는 부분이 없다는 것입니다.

    스택의 1000개의 요소가 있다고 가정하고, 사이즈가 1까지 줄어들었다고 가정해보겠습니다.

    요소는 1개만 의미가 있고 나머지는 의미가 없지만 999개의 메모리가 해제되지 않아 낭비됩니다.

     

    이렇게 누수가 쌓이면 OutOfMemoryError가 발생할 수 있습니다.

     

    pop 코드를 간단하게 바꾸어 레퍼런스에 null을 할당하면 해결됩니다.

    fun pop(): Any?{
    	if (size ==0){
        	throw EmptyStackException()
        }
    	val elem = elements[--size]
    	elements[size] = null
    	return elem
    }

    위의 예제는 간단하고 확실하게 메모리 누수에 대해 이해할 수 있습니다.

    하지만 자주 접할 수 있는 예제는 아닙니다.

     

    lazy처럼 동작해야 하지만, 상태 변경도 할 수 있는 경우 발생

    val initializer: () -> T
    private var initialized = false
    
    if(!initialized){
    	value = initializer()
     	initalized = true
    }

    계속 initializer()를 호출해서 사용한 후에도 해제되지 않습니다.

     

    val initalizer = initializer
    if(initializer != null){
    	value = initalizer()
    	this.initializer = null
    }

    null으로 설정함으로써, GC가 이를 처리할 수 있습니다.

    쓸데없는 최적화는 모든 악의 근원이라는 말도 있지만 오브젝트에 null을 설정하는 것은 그렇게 어려운 일이 아니라 무조건 하는 것이 좋습니다.

     

    가독성, 확장성 vs 메모리와 성능

    일반적으로 가독성과 확장성을 더 중시하는 것이 좋습니다.

    하지만 라이브러리를 구현할 때는 메모리와 성능이 더 중요합니다.

     

    메모리 누수와 캐시

    절대 사용되지 않는 객체를 캐시 하여 저장해서 두는 경우에 메모리 누수가 발생할 수 있습니다.

    해결 방법으로는 소프트 레퍼런스를 사용하여 메모리가 필요한 경우에 GC가 알아서 해제하도록 합니다.

     

    화면 위의 대화상자와 같은 일부 객체는 약한 레퍼런스를 사용하는 것이 좋을 수 있습니다.

    대화상자가 출력되는 동안은 GC가 이를 수집하지 않으며, 대화상자를 닫은 이후에는 참조를 유지할 필요가 전혀 없습니다.

     

     

    예측하기 어려운 메모리 누수

    애플리케이션이 크래시 되기 전까지 메모리누수를 예측하기 어렵습니다.

    안드로이드 애플리케이션에서 보통 메모리 사용량에 엄격한 제한이 존재하기 때문에 더 큰 문제가 될 수 있습니다.

     

    보통 별도의 도구를 활용하여 메모리 누수를 찾는 것도 좋은 방법입니다.

     

    사실 객체를 수동으로 해제해야 하는 경우는 굉장히 드뭅니다.

    일반적으로 스코프를 벗어나면서, 어떤 객체를 가리키는 래퍼런스가 제거될 때 객체가 자동으로 해제됩니다.

     

    따라서 메모리와 관련된 문제를 피하는 가장 좋은 방법은 변수를 지역 스코프에 정의하고, 톱 레벨 프로퍼티 또는 객체선언으로 큰 데이터를 저장하지 않는 것입니다.

     

    댓글

Designed by Tistory.