[JVM] 객체 재사용 - Reference 종류, WeakHashMap

2021. 12. 16. 17:22 JAVA/JVM

객체 재사용

객체를 생성하는데 비용이 큰 객체들은 일반적인 객체 생명 주기를 따르지 않고,
한번만 생성되고 재사용해야 한다. 
재사용 하면 무조건 좋기만 할까?


객체를 무분별하게 재사용하면 안 좋은 이유

재사용 객체는 오래 사용되기 때문에 올드 제너레이션에서 공간을 차지할 것이다.
Full GC 를 수행하는데 걸리는 시간은 살아있는 객체 수에 비례한다.
1GB의 올드 제너레이션에서 많은 객체가 살아남았을 때보다
3GB의 올드 제너레이션에서 적은 객체가 살아남았을 때 GC시간이 더 빠르다.
따라서 객체를 재사용하게 되면 올드 제너레이션에 살아남은 객체가 많아져서 GC시간이 늘어난다.

더욱이,
G1을 사용하게 되면 동시 병렬 컬렉터들의 성능도 살아있는 객체 수에 비례한다.
Full GC가 발생하지 않더라도 살아있는 객체가 많으면 병렬 처리가 느려진다.
따라서 객체를 무분별하게 재사용 하는 것은 좋지 않다. 

그럼에도 불구하고 JDBC connection pool과 같이 많은 요소에서 객체를 재사용하는 이유는,
GC를 감안하더라도 객체를 재사용하는 것이 다른면에서 더 효율적이기 때문이다. 

JDBC connection pool과 같이 재사용될 객체를 관리하기 쉽게 해주는 라이브러리들이 있다. 
이러한 라이브러리들은 GC와 직접적으로 상관없이 프로그래밍 적으로 구현을 한 것이다.
이들과는 조금 다르게, GC와 직접관여해서 GC의 동작을 바꿔서 재사용될 객체를 관리할 수 있다. 

Reference Object는  GC의 동작에 직접 관여해서 재사용될 객체를 관리한다. 
Reference Object 중에 Weak Reference, Soft Reference를 살펴보자. 
이 두 레퍼런스를 Indefinite Ref라고도 부른다. 이들은 GC의 동작을 변경해서 객체를 재사용하게 한다. 

재사용하는 것에 따른 이익도 있지만, Indefinite Ref는 메모리에서 깨끗이 제거되기 위해서 최소 2번의 GC 가 필요하다.
때문에 GC time을 더 늘리는 안좋은 영향이 있다. 자세한 사항은 아래의 레퍼런스 종류를 참고하자.

 
GC & References

GC는 Root Set으로 불리는 루트 참조에서 시작해서 모든 객체에 대한 경로를 탐색하고, 객체에 대한 Reachability를 결정한다. 그리고 Unreachable 객체는 GC대상이 된다.  

 

Various Reference : 다양한 레퍼런스

Reference 종류가 많은 이유는, 레퍼런스 종류마다 GC의 동작이 다르기 때문이다.
GC 대상여부를 판별하는 부분에 개발자가 관여해서 GC대상에서 제외한다거나, Phantom Ref을 이용하면 GC때 해당 레퍼런스 말고도 추가적으로 정리해야할 리소스를 정의할 수 있다. 

레퍼런스 들을 살펴보기에 앞서, 용어들을 알아야 이해하기 쉽다.
※ Reference Object
Soft, Weak, Phantom Ref에 의해 생성된 객체를 Reference Object라고 부른다. 
Reference Object에 의해 참조되는 객체를 Referent라고 부른다. 

※ Indefinite Reference
Weak, Soft Reference 를 합쳐서 Indefinite Reference라고 한다.
Indefinite Ref는 객체를 매번 다시 생성하지 않고 재사용할 수 있게 한다. 



1. Strong Reference
객체를 참조하는 일반적인 인스턴스 변수
java.lang.ref 패키지를 사용하지 않은 Root set으로 부터 참조되는 일반적인 참조
Strong Reference에 의해 참조되는 객체는 GC의 대상이 될 수 없다.


2. Weak Reference
"내가 D라는 데이터를 사용하는 동안 누가 D를 필요로 한다면 알려줘 같이 쓰자.
하지만 내가 필요없다면 그건 버리고 다음에 필요할 때 다시 생성할꺼야"
java.lang.ref.WeakReference를 이용해서 참조를 만들면 WeakReference가 된다.
WeakReference는 Root set으로부터 참조된다.
WeakReference 자체는 Strong Reference에 의해 참조된다. ==> 최소 2번 GC가 발생해야 깨끗이 청소됨
( 1단계 : Weak Ref가 가리키는 Referent 제거(null로 지정), 2단계 : Weak Ref를 가리키는 Strong Ref제거 )
최소 2번이라고 말한 이유는, GC 가 발생한다고 해서 반드시 해당 GC때 메모리가 수거되지는 않기 때문이다.
GC는 (GC대상 찾기, GC 대상 finalize, 메모리 회수)크게 3단계로 나뉘며, GC때 어떤 작업까지 이루어질 지는 예상하기 힘들다.
Weak Reference에 의해 참조되는 객체는 GC가 동작할 때마다 회수된다. 
따라서 LRU 캐시와 같은 임시 객체들을 저장하는 구조를 만드는데 용이하다.
Weak Ref는 다음과 같이 만들 수 있다.

Weak Ref & 스레드 동시 접근 
여러 개의 스레드에 의해 동시 접근되는 참조 대상을 처리할 때 유용하다. 
예를들어, A사용자가 특정 세션 내에 D라는 데이터를 매번 조회한다. 그래서 D라는 데이터를 Strong Ref를 이용해서 구현해서 A사용자가 로그아웃 하자마자 세션은 제거되고 메모리는 반환된다. 이제 다른 사용자 B가 D라는 똑같은 데이터를 요구한다면 글로벌 캐시에 데이터에 대한 Weak Ref를 유지하는 것이 좋다. 이제 A가 로그아웃하고 세션을 제거하지 않는다면 두번째 사용자는 D라는 데이터를 A와 공유해서 쉽게 찾을 수 있다. 


3. Soft Reference
"메모리가 충분하고 누군가가 주기적으로 접근하는 한 데이터를 유지해줘"
java.lang.ref.SoftReference를 이용해서 참조를 만듬
객체가 Soft Reachable이 되려면 Strong Reference에 의해 참조되지 않으면서, 오직 Soft Reference 객체로만 참조되어야 한다. 
SoftReference 자체는 Strong Reference에 의해 참조된다. ==> 최소 2번 GC가 발생해야 깨끗이 청소됨
( 1단계 : Soft Ref가 가리키는 Referent 제거(null로 지정), 2단계 : Soft Ref를 가리키는 Strong Ref제거 )

Soft Reference로 참조되는 객체는 힙에 남아있는 메모리의 크기와 해당 객체의 사용 빈도에 따라 GC여부가 결정된다.
따라서 Weak Reference와 달리, GC가 동작할 때마다 회수되지 않으며 자주 사용될수록 더 오래 살아남는다. 


-XX:SoftRefLRUPolicyMSPerMB=N (Default : 1000)
long ms = SoftRefLRUPolicyMSPerMB(N) * 힙에 남아있는 메모리(MB);
Soft Reference에 의해 ms초 이상 사용되지 않으면 GC에 의해 회수대상이 된다. 
Ex. N=1000이고 남아있는 메모리가 100MB이면 100000ms = 100초 동안 사용되지 않은 Soft Reference는 GC 대상이 된다. 
힙에 남아있는 메모리가 작을수록 경과시간이 짧아지므로, 많은 Soft Reference가 회수되어 메모리가 부족해서 OOME에러가 발생하는 것을 막을 수 있다.
소프트 참조를 더 자주 반환하려면 N 값을 줄이면 된다.
힙이 소프트 참조로 인해 급속도로 가득 차는 현상이 발생할 수 있는데, 이 때 이 플래그를 튜닝할 필요가 있다. 
이용 가능한 힙 공간이 많고 소프트 참조가 드물게 사용된다면 N 값을 늘리는 것을 고려할 수 있다. 
소프트 참조는 객체의 수가 너무 많지 않을 때 잘 동작한다.
캐시해야되는 객체가 너무 많다면 전통적인 형태의 객체 풀을 고려하자.


4. Phantom Reference
GC 과정은 여러 단계로 나뉜다.
GC 대상 객체를 찾는 작업, 객체를 처리(finalize), 메모리를 회수하는 작업
앞서 살펴본 Soft Reference, Weak Reference는 GC 대상 객체를 찾는 작업에 개발자가 관여할 수 있게 해준다. 
하지만 Phantom Reference는 finalize , 메모리를 회수하는 것에 관여한다.
GC가 객체를 처리하는 순서는 다음과 같다.
Strong Ref > Soft Ref > Weak Ref > finalize > phantom Ref > 메모리 회수
객체가 Strong,Soft, Weak 참조에 의해 참조되는지 판단하고 모두 아니면 finalize를 진행하고 phantom 여부를 판단한다. 
따라서 finalize() 이후에 처리해야하는 리소스 정리 작업을 할 때 개발자가 관여할 수 있다. 
하지만 거의 안쓰인다.

※ Object.finalize()
객체가 GC 대상이 되면 데이터를 제거하는데 사용한다. 
이 메서드는 절대로 직접적으로 사용하지 말 것
기능적으로나 성능적으로나 좋지 않다!

ReferenceQueue

Indefinite Ref 객체가 참조하는 객체가 GC 대상이 되고, 그 이후 첫 번째 GC가 발생하면 Indefinite Ref가 참조하는 Referent는 null로 설정되고, Indefinite Ref 객체 자체는 GC에 의해 자동으로 ReferenceQueue에 enqueue된다. 
그리고 나중에 GC가 한번더 발생할 때, ReferenceQueue를 살펴보고 그 안에 담겨진 Indefinite Ref 들은 더이상 쓸모 없으므로 처리를 하게 된다. ReferenceQueue를 처리할 때, 관련된 리소스에 대한 후처리 작업을 할 수 있다. 어떤 객체가 GC에 의해 소거당할 때, 후처리 작업이 필요할 경우 유용하게 사용된다. 
Indefinite Ref는 ReferenceQueue가 선택사항이다. 하지만 PhantomReference는 필수다.
Reference를 생성할 때, ReferenceQueue를 생성자로 주고 안주고에 따라서 선택된다. 
PhantomReference는 필수이기 때문에 다음과 같이 하나의 생성자만 존재한다.

 

WeakHashMap

Reference Object들을 배웠는데, 이들을 실제로 사용해서 코딩하려고 하면.. 막막하다.
고수분들께서... 이들을 이용해서 자주 사용하는 컬렉션 클래스를 만들어서 제공해준다. 
이전에 컬렉션과 GC의 연관관계에 대해 배운적이 있다.
컬렉션 클래스에 객체를 넣으면 객체가 가려져서 사용되지 않는 객체일지라도 JVM이 GC대상으로 판단하지 못하고 컬렉션에 쌓여서 결국 메모리 누수의 원인이 된다. 

[JVM] WANR! Collections & Memory Leak


이때 WeakHashMap을 사용할 수 있다. Weak Ref를 이용해서 HashMap의 Key를 구현했다. 
즉, WeakHashMap에 있는 Key값이 더이상 사용되지 않는다고 판단되면 다음 GC때 해당 Key, Value 쌍을 제거한다. 
임의로 제거되어도 상관없는 데이터들을 위해 주로 사용된다. 


트레이드 오프
위에서 배운 대로, Weak Reference는 깨끗하게 소거되기 위해 최소한의 2번의 GC가 필요하며 이는 가비지 컬렉터에 부정적인 영향을 줄 수 있다.
WeakHashMap자체가 컬렉션 내의 미참조 데이터를 제거하는 동작을 주기적으로 수행해야 한다. 
WeakHashMap코드를 보면 ReferenceQueue를 갖고 있고, 이를 관리한다. 관리하는 것 자체가 오버헤드이다.
WeakHashMap과 같은 Indefinite Ref를 이용한 컬렉션 구현체는 여럿 있으며 이들을 사용할 때는 신중하게 사용하여야 한다. 가능하다면 객체 풀과 같이 어플리케이션에서 컬렉션을 관리하자. 



요약 
Reference Object는 자바 객체를 재사용할 필요가 있을 경우, 객체 풀이나 스레드-로컬 변수를 사용하는 방법 보다 GC에 직접적으로 관여하여 객체를 재사용되게 한다. 

Reference Object는 잘 사용하면 객체를 현명하게 재사용함으로써 많은 이익을 얻을 수 있지만, 잘 모르고 사용하면 쉽게 성능을 떨어뜨릴 수 있다. 가능하다면 객체 풀과 같이 어플리케이션에서 컬렉션을 관리하자.