본문 바로가기

java/이펙티브 자바

[item 7] 다 쓴 객체 참조를 해제하라

 

OutOfMemoryError
본 적 있죠?

C, C++ 언어는 명시적으로 메모리를 할당해서 사용하고 

자원을 다 사용하고 나면 개발자가 명시적으로 해제를 한다.

 

가비지 컬렉터를 갖춘 자바와 같은 언어를 사용하면,

가비지 컬렉터가 다 쓴 객체를 알아서 회수한다.

그래서 메모리 문제를 개발자가 전혀 신경쓰지 않아도 된다고 오해할 수 있다.

 

메모리 누수 현상이 발생하니 null을 입력하라.

 

 

package com;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return this.elements[--size];
    }

    public Object pop2() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = this.elements[--size];
        this.elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (this.elements.length == size) {
            this.elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

메모리 누수가 일어나는 위치는?

 

스택을 구현한 코드는 메모리 누수 문제가 있다. 메모리 누수 문제는 어디서 일어나는 것일까?

스택을 구현한 코드를 다시 살펴보자.

 

문제는 바로 pop 메서드이다.

 elements 배열에서 꺼내진 객체는 가비지 컬렉터가 회수하지 않는다. 

스택이 다 쓴 참조(obsolete reference)를 가지고 있기 때문이다. 

다 쓴 참조라는 뜻은 더이상 쓰지 않을 참조라는 뜻이다.

 

 

다음 코드는 pop 메서드를 통해 스택에 있는 원소를 꺼내는 예제이다.

return this.elements[--size];

스택 자체에서 배열을 통해 원소를 관리하고 있다. 

pop 메서드를 호출하면 배열의 마지막 원소를 반환하면서 size 값을 변화시킨다.

size 값만 줄어든 것일뿐 elements 배열에는 객체 참조가 남아 있다.

반환된 원소에 대한 참조가 배열에 존재하더라도 개발자는 해당 원소에 접근할 수 없다.

size 값이 인덱스 역할을 하고 있는데, 해당 위치로 다시 돌아가기 위해서는 size 값을 증가 시켜야한다.

 

메모리 누수 해결방안, null 처리


메모리 누수를 해결하는 방법은 간단하다. 해당 참조를 다 쓴 경우에 null 처리(참조 해제)를 해주면 된다. 스택 클래스에서 각 원소의 참조가 더 이상 필요하지 않는 시점은 스택에서 원소가 꺼내지는 시점이다.

 

 Object result = this.elements[--size];
 this.elements[size] = null;
 return result;

 

 

메모리 누수?

S 의미로 살펴볼 때, 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상이다.

 

할당된 메모리를 사용한 다음 반환하지 않는 것이 누적되면 메모리가 낭비된다. 즉, 더 이상 불필요한 메모리가 해제되지 않으면서 메모리 할당을 잘못 관리할 때 발생한다.

 

일부 서적에서 메모리 손실이라는 용어로 뜻을 옮기기도 하지만 leak라는 표현은 단순히 잃는 것 이상의 개념이므로 누수라는 표현이 더 정확하다.

 

 

더 이상 사용되지 않는 객체들이 가비지 컬렉터에 의해 회수되지 않고 계속 누적이 되는 현상을 말한다.

Old 영역에 계속 누적된 객체로 인해 Major GC가 빈번하게 발생하게 되면서, 프로그램 응답속도가 늦어지면서 성능 저하를 불러온다. 이는 결국 OutOfMemory Error로 프로그램이 종료되게 된다.

 

https://junghyungil.tistory.com/133

 

모든 객체에 항상 null 처리를 해야하나?

그렇다면 개발자는 자바에서 사용한 모든 객체에 null 처리를 반드시 해야할까? 

매번 null 처리를 반드시 할 필요가 없다. 

이는 코드를 복잡하게 만들뿐이다. 객체 참조에 null 처리하는 일은 예외적인 경우에 해당한다. 

다 쓴 객체 참조를 해제하는 가장 좋은 방법은 참조를 유효 범위(scope) 밖으로 밀어내는 것이다. 

변수의 범위를 가능한 최소가 되게 정의한다.

 

Null 처리는 언제 해야하나?

배열의 활성 영역은 사용하고

배열으 비활성 영역은 사용하지 않는다는 걸 사람은 알지만, 가비지 컬렉터는 모른다.

그러므로 개발자는 비활성 영역이 되는 순간을 null 처리를 해서 해당 객체를 쓰지 않을 것임을

가비지 컬렉터에게 알려야 한다.


앞에서 살펴봤던 스택 클래스는 배열을 통해 원소를 관리하고 있다.

배열은 size라는 인덱스 값을 통해 원소를 접근한다.

size 값이 변동하면서 원소를 사용하지 않는더라도 이를 가비지 컬렉터가 알 수 없다.

그렇기 때문에 비활성 영역에서 참조하는 객체는 가비지 컬렉터가 해당 객체를 회수하지 않는 일이 발생한다.

이처럼 메모리를 직접 관리하는 클래스인 경우에 null 처리가 적합하며, 개발자는 메모리 누수에 주의해야 한다.

 

 

캐시 메모리 누수 주의


캐시 방식은 메모리 누수를 일으키는 주범이다. 

객체 참조를 캐시에 저장하고, 

객체를 사용하고나서 객체 참조를 해제하는 일을 잊어 버릴 수 있다. 

 

이러한 경우에 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 상황이 필요하다면 

WeakHashMap을 사용해서 캐시를 만들 수 있다. 

다 쓴 엔트리는 자동으로 제거된다.

 

WeakHashMap는 WeakReference 타입을 키 값으로 사용한다. 

Java에서 References 종류는 Strong References, Soft References, Weak References 3가지가 있다. 

각 References에 대한 설명과 예제는 Guide to WeakHashMap in Java를 참고하자.

 

다음 코드는 WeakHashMap을 사용한 예제이다.

package com;

import java.util.Map;
import java.util.WeakHashMap;

public class TestMain {
    public static void main(String[] args) {
        Map<Key, String> map = new WeakHashMap<>();
        Key key = new Key("이름");
        map.put(key, "정워니");//map 에 새로운 entry 추가
        mapPrint(map);
        //key = null;// Key 객체 참조 null 처리
        System.gc();// 강제 GC
        mapPrint(map);// 빈 값 출력
    }

    private static void mapPrint(Map<?, ?> map) {
        map.entrySet().stream().forEach(System.out::println);
    }

}

class Key {
    private String name;

    public Key(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Key[" + name + "]";
    }
}

 

 

 

주범3: 리스너, 콜백이다

 

클라이언트가 콜백을 등록만 하고 해지 하지 않는다면 콜백은 쌓인다.

이럴때 콜백을 약한 참조로 저장하면 가비지 컬렉터가 수거한다.

WeakHashMap에 키로 저장하면 된다.

 

http://blog.breakingthat.com/2018/08/26/java-collection-map-weakhashmap/

'java > 이펙티브 자바' 카테고리의 다른 글

Item29~30  (0) 2022.01.09
[item 21~22]  (0) 2022.01.01
[item 1] 생성자 대신 정적 팩터리 메서드를 고려하라  (0) 2021.12.29
[item 17~18]  (0) 2021.12.29
[item 8] finalizer와 cleaner 사용을 피하라  (0) 2021.12.26