Effective Java

Effective Java | Item 7. 다 쓴 객체 참조를 해제하라

이진유진 2024. 5. 27. 10:02
반응형
Effective Java 3/E 판을 읽고 정리한 기록입니다.

Java에서 메모리 누수는 가비지 컬렉션(Garbage Collection) 메커니즘이 있음에도 불구하고 여전히 발생할 수 있는 문제입니다.

메모리 누수는 더 이상 필요하지 않은 객체가 계속해서 메모리에 남아 있는 경우를 말합니다. 

이는 애플리케이션의 성능 저하를 유발하고, 심각한 경우 OutOfMemoryError를 발생시킬 수 있습니다. 

메모리 누수의 원인

컬렉션에 저장된 객체를 참조 해제하지 않은 경우 

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakExample {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        
        for (int i = 0; i < 1000; i++) {
            Object obj = new Object();
            list.add(obj); // 객체를 리스트에 추가
        }
        
        // 메모리 누수 발생 가능
        // list.clear(); // 주석을 풀면 메모리 누수 방지 가능
    }
}

 

이 코드에서는 for 루프를 통해 1000개의 객체를 생성하고 리스트에 추가합니다. 

리스트에 추가된 객체들은 프로그램이 종료될 때까지 메모리에 남아 있습니다. 

따라서, List가 참조하고 있는 모든 객체는 가비지 컬렉터에 의해 회수되지 못해 메모리 누수가 발생할 수 있습니다. 

 

이때, list.clear(); 메서드를 호출해 리스트에서 모든 객체를 제거할 수 있습니다. 

이렇게 하면 리스트가 더 이상 객체를 참조하지 않게 되므로, 

가비지 컬렉터가 해당 객체들을 회수할 수 있게 됩니다. 

결과적으로 메모리 누수를 방지할 수 있습니다. 

 

캐시된 객체를 적절히 제거하지 않은 경우

import java.util.HashMap;
import java.util.Map;

public class CacheMemoryLeakExample {
    private Map<String, Object> cache = new HashMap<>();

    public void addToCache(String key, Object value) {
        cache.put(key, value);
    }

    public Object getFromCache(String key) {
        return cache.get(key);
    }

    public static void main(String[] args) {
        CacheMemoryLeakExample cacheExample = new CacheMemoryLeakExample();
        
        for (int i = 0; i < 1000; i++) {
            String key = "key" + i;
            Object value = new Object();
            cacheExample.addToCache(key, value);
        }

        // 캐시에는 1000개의 객체가 유지되며, 더 이상 필요하지 않더라도 메모리에서 제거되지 않음
    }
}

이 코드는 1000개의 객체를 HashMap에 추가합니다. 

각 객체는 키를 통해 접근할 수 있으며, 

HashMap은 객체에 대한 강한 참조(strong reference)를 유지합니다. 

따라서, 더 이상 필요하지 않더라도 캐시에 저장된 객체들은 가비지 컬렉션의 대상이 되지 않습니다. 

 

위의 코드를 밑의 코드와 같이 구성하여 약한 참조를 사용하여 메모리 누수를 방지할 수 있습니다.

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

public class CacheWithWeakReferencesExample {
    private Map<String, Object> cache = new WeakHashMap<>();

    public void addToCache(String key, Object value) {
        cache.put(key, value);
    }

    public Object getFromCache(String key) {
        return cache.get(key);
    }

    public static void main(String[] args) {
        CacheWithWeakReferencesExample cacheExample = new CacheWithWeakReferencesExample();
        
        for (int i = 0; i < 1000; i++) {
            String key = "key" + i;
            Object value = new Object();
            cacheExample.addToCache(key, value);
        }

        // 캐시의 키가 약한 참조로 유지되므로, 더 이상 필요하지 않은 객체는 가비지 컬렉션됨
    }
}

이 코드는 WeakHashMap을 사용합니다. 

WeakHashMap에서 키는 약한 참조로 유지되기 때문에, 키가 더 이상 강한 참조로 유지되지 않으면, 해당 키와 연결된 객체가 가비지 컬렉션의 대상이 됩니다. 

따라서 캐시된 객체가 더이상 필요하지 않으면 자동으로 메모리에서 제거되어 메모리 누수가 방지됩니다. 

리스너 또는 콜백을 등록한 후 해제하지 않은 경우 

import java.util.ArrayList;
import java.util.List;

interface EventListener {
    void onEvent();
}

class EventSource {
    private List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }

    public void removeListener(EventListener listener) {
        listeners.remove(listener);
    }

    public void fireEvent() {
        for (EventListener listener : listeners) {
            listener.onEvent();
        }
    }
}

public class MemoryLeakListenerExample {
    public static void main(String[] args) {
        EventSource eventSource = new EventSource();

        EventListener listener = new EventListener() {
            @Override
            public void onEvent() {
                System.out.println("Event received");
            }
        };

        eventSource.addListener(listener);

        // 이벤트 리스너가 등록된 상태로 남아 있어 메모리 누수 발생 가능
        // eventSource.removeListener(listener); // 주석을 풀면 메모리 누수 방지 가능
    }
}

이 코드에서는 EventSource 클래스가 이벤트 리스너 목록을 유지합니다. 

EventListener 인터페이스를 구현하는 익명 클래스가 리스너로 등록됩니다. 

리스너를 등록한 후 이를 제거하지 않으면, EventSource 객체가 리스너에 대한 참조를 계속 유지하여 메모리 누수가 발생할 수 있습니다. 

 

다 쓴 객체 참조 해제하기 

다 쓴 객체 참조를 해제하는 것은 메모리 누수를 방지하기 위해 매우 중요합니다. 

다음은 이를 위한 몇가지 전략입니다. 

 

컬렉션 사용 시 주의점

컬렉션을 사용 시, 다 쓴 객체를 명시적으로 제거해야 합니다. 

List<Object> list = new ArrayList<>();
Object obj = new Object();
list.add(obj);

// 더 이상 obj가 필요하지 않을 때
list.remove(obj);

 

캐시된 객체는 특정 조건에서 제거해야합니다. 

이를 위해 WeekHashMap을 사용할 수 있습니다. 

WeekHashMap은 키에 대한 약한 참조(weak reference)를 사용해 더이상 필요하지 않은 객체가 가비지 컬렉터에 의해 수집될 수 있도록 합니다. 

Map<Object, Object> cache = new WeakHashMap<>();
Object key = new Object();
Object value = new Object();
cache.put(key, value);

// key에 대한 강한 참조를 제거하면, 캐시된 값도 가비지 컬렉터에 의해 수집될 수 있음
key = null;

 

리스너와 콜백을 등록할 때는 적절한 시점에 이를 해제해야 메모리 누수를 방지할 수 있습니다. 

class MyComponent {
    private List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }

    public void removeListener(EventListener listener) {
        listeners.remove(listener);
    }

    protected void finalize() throws Throwable {
        // 객체가 가비지 컬렉션 될 때 모든 리스너 해제
        listeners.clear();
        super.finalize();
    }
}

주요 포인트

다 쓴 객체 참조를 Null로 설정하는 방법은 특히 인스턴스가 더이상 필요하지 않게 될 때 도움이 됩니다. 

컬렉션을 직접 관리시, 스택이나 큐와 같은 자료구조를 통해 명시적으로 객체를 제거하는 것이 좋습니다. 

반응형