JAVA/Effective Java

[아이템 7] 다 쓴 객체 참조를 해제하라

꾸준함. 2024. 1. 22. 13:21

객체 참조 해제는 기본적으로 GC가 해주지만 간혹 메모리 누수를 방지하기 위해 다 쓴 객체를 직접 참조 해제해야 하는 케이스가 있습니다.

책에서는 자기 메모리를 직접 관리하는 클래스의 경우 직접 참조를 해제해야 한다고 명시했고 대표적인 사례로 아래 세 가지 케이스를 예시로 들었습니다.

  • Object 배열을 갖는 스택
  • 캐시
  • 리스너 혹은 콜백

 

1. Object 배열을 갖는 스택

아래 Stack 클래스와 같이 자기 메모리(Object 배열)를 직접 관리하는 케이스의 경우 객체 참조를 null로 할당하면서 직접 해제해야 합니다.

객체 참조를 null로 할당할 경우 GC가 돌 때 메모리를 회수할 수 있게 됩니다.

 

 

 

만약 pop() 메서드에서 위 코드와 같이 명시적으로 객체 참조를 해제하지 않고 아래와 같이 단순히 top()에 위치한 원소를 반환하는 방식으로 구현할 경우 Object 배열에 계속해서 원소가 쌓이고 GC 입장에서는 아직 배열이 참조하고 있는 객체이기 때문에 회수하지 않아 언젠가는 메모리 누수가 발생할 것이고 이는 OOM(Out Of Memory)를 야기할 것입니다.

 

 

2. 캐시

캐시에 앞서 HashMap과 WeakHashMap 개념에 대해 간단히 정리하겠습니다.

둘 다 key-value 쌍을 저장하는 자료구조라는 공통점이 있지만 내부적으로 동작하는 방식이 다릅니다.

 

HashMap

 

HashMap은 Strong Reference(강한 참조)를 사용하여 key-value 쌍을 저장합니다.

즉, key가 HashMap에 저장하는 순간 해당 키에 대한 강한 참조가 유지됩니다.

따라서 HashMap에 저장된 객체는 명시적으로 제거하지 않는 한 계속 메모리에 남게 되기 때문에 캐시 사용에 적합하지 않습니다.

 

WeakHashMap

 

반면, WeakHashMap은 Weak Reference(약한 참조)를 사용하여 key-value 쌍을 저장합니다.

이에 따라 WeakHashMap의 경우 GC가 돌 때 key-value 쌍에서 key가 더 이상 강한 참조되지 않을 경우 자동으로 메모리가 회수가 됩니다.

따라서 캐시를 구현할 때는 WeakHashMap을 사용하거나 Weak Reference를 사용하는 것을 추천합니다.

 

실제 코드를 확인하면 캐시를 HashMap으로 구현했을 때 GC가 돌더라도 메모리가 회수되지 않은 것을 확인할 수 있습니다.

여기서 캐시의 Key를 별도 CacheKey와 같은 레퍼런스로 생성한 이유는 Integer나 String 같은 클래스를 키로 사용할 경우 JVM에서 내부적으로 캐싱하기 때문에 Key 변수를 명시적으로 NULL로 만들어도 강한 참조가 남아 회수가 안되기 때문입니다.

물론, 아래 코드처럼 HashMap으로 구현할 경우 어쨌거나 강한 참조가 형성되기 때문에 메모리 회수가 되지 않아 테스트 코드가 실패하는 것을 확인할 수 있습니다.

 

 

반면 HashMap을 WeakHashMap으로 바꿀 경우 강한 참조에서 약한 참조로 바뀌어 테스트 코드가 통과하는 것을 확인할 수 있습니다.

 

 

 

마지막으로 앞서 말했다시피 Cache의 키를 String, Integer와 같은 기본 Wrapper 클래스로 사용할 경우 JVM 내부적으로 캐싱을 하기 때문에 WeakHashMap으로 구현하더라도 테스트 코드가 실패하는 것을 확인할 수 있습니다.

 

 

실제로 GuavaCache에서도 CacheBuilder 내부에 CustomConcurrentHashMap을 구현하고 약한 참조를 적용한 것을 확인할 수 있었습니다.

 

CustomConcurrentHashMap

 

책에서 캐시를 설명할 때 백그라운 쓰레드를 활용하여 사용하지 않는 엔트리를 이따금 청소해 줄 수 있다고 했는데 대표적인 예시로 Guava Cache를 들 수 있습니다.


 

3. 리스너 혹은 콜백

리스너 혹은 콜백의 경우 등록만 하고 명확히 해지하지 않는다면 계속 쌓여서 문제를 야기할 수 있습니다.

아래 예시 코드는 콜백을 예로 들었고 CustomWeakReferenceList 클래스를 별도 구현하여 약한 참조를 통해 객체가 더 이상 참조되지 않을 경우 GC를 통해 메모리 회수를 하도록 처리했습니다.


 

비고

참조 혹은 레퍼런스는 아래와 같이 네 종류가 있습니다.

  • Strong Reference
  • Soft Reference
  • Weak Reference
  • Phantom Reference

 

Strong Reference

 

대부분의 개발자가 일반적으로 사용하는 참조이며 Object를 "=" 으로 할당하는 경우 강한 참조입니다.

강한 참조가 존재하는 동안에는 가비지 컬렉터에 의해 수거되지 않습니다.

일반적인 객체 참조의 형태는 강한 참조로, 이는 객체의 생명주기 동안 계속해서 참조를 유지합니다.

따라서 해당 객체는 참조를 가진 모든 변수나 컨테이너가 null로 설정되거나 더 이상 사용되지 않을 때까지 계속해서 메모리에 남아 있게 됩니다.

 

Soft Reference

 

약한 참조(Weak Reference)와 유사하지만 가비지 컬렉터에 의해 수거되기 전에 메모리 부족 상황에서만 수거 대상이 되는 참조입니다.

해당 참조의 경우 더 이상 Strong Reference가 없고 Soft Reference만 남았을 때 메모리가 필요한 상황에서만 GC가 돌 때 회수가 되는 특징을 가집니다.

즉, OOM 일보 직전이 아니고서야 회수가 거의 안됩니다.

 

Weak Reference

 

Weak Reference는 해당 객체에 대한 참조를 가지지만 가비지 컬렉터에 의해 수거될 수 있는 참조입니다.

약한 참조는 메모리 누수를 방지하고 객체 수거의 유연성을 제공하기 위해 사용되며 주로 캐시 구현, 리소스 관리, 또는 객체의 생명주기를 더 유연하게 다루어야 하는 경우에 사용됩니다.

 

Phantom Reference

 

다른 reference들과 달리 객체에 대한 실질적인 참조를 제공하지 않습니다.

대신에, 객체가 finalize 되기 직전에 수거될 것임을 알려주는 역할을 합니다.

Phantom Reference만 남은 경우 GC가 돌면 본래 가지고 있던 object는 회수되고 팬텀 참조는 ReferenceQueue에 넣어지고 나중에 사용자가 ReferenceQueue에서 꺼내서 정리할 수 있습니다.

 

Phantom Reference의 use case는 아래와 같이 두 가지입니다.

  • 자원 정리
    • finalize보다는 나은 리소스 정리 방법이지만 item 9에서 언급할 최상의 방법인 try-resource가 있으므로 굳이 해당 방법을 사용하지는 않음

 

  • 메모리에 민감한 애플리케이션의 경우 GC에 의해 회수됨과 동시에 ReferenceQueued에 들어가기 때문에 큐를 통해 언제 무거운 객체가 메모리 해제되는지 확인할 수 있음


 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 1부 - 백기선 강사님

반응형