1. 객체를 사용하고 나서 null을 할당해주어야 한다 vs null은 코드를 더럽히는 것이다.
2. Null을 할당하는 것은 별 의미 없다 vs 매우 중요하다
3. Null은 언제 할당해야 하나?
이러한 고민은 자바 개발자들 사이에서 많은 이슈가 되는 부분입니다. 사실상 이 논란은 자바라는 언어에서 프로그래머가 직접 메모리를 할당/해제를 하지 않기 때문에 발생합니다.
1. 작은 프로그램에서 null할당은 의미가 없다. 여기서 작은 프로그램이란 프로그램 자체의 동작시간이 짧고 객체 생성이 빈번하지 않으며 적은 메모리를 사용하여 메모리 누수가 되지 않는, 실행시 힙사이즈 절반도 사용하지 않는 프로그램(메소드)로 정의합니다.
2. 현재 작성하고 있는 코드가 큰 프로그램(메소드)의 일부라면 항상 가능한한 모든 객체의 사용 후 null을 할당해 주어야 하며 반드시 중요한 부분이다.
예제 1) null을 주지 않았을 시
public class GCTest {
private final static int ALLOC_SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.60);
public void allocate() {
System.out.println("Before first allocation");
byte[] b = new byte[ALLOC_SIZE];
System.out.println("After first allocation");
System.out.println("Before second allocation");
byte[] b2 = new byte[ALLOC_SIZE];
System.out.println("After second allocation");
}
public static void main(String[] args) {
new GCTest().allocate();
}
}
** 가용한 메모리의 60%를 allocate라는 메소드의 첫번째 지역변수 b에서 할당하고 이후 전혀 사용되지 않는다. 그리고 나서 다시 두번째 지역변수 b2에서 또 60%를 할당한다.
예제1 결과)
Before first allocation
After first allocation
Before second allocation
[GC 555205K->555136K(712576K), 0.0017096 secs]
[GC 555136K->555104K(712576K), 0.0045040 secs]
[Full GC 555104K->555088K(660160K), 0.0052565 secs]
[GC 555088K->555088K(712576K), 0.0071493 secs]
[Full GC 555088K->555066K(665280K), 0.0062704 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at GCTest.allocate(GCTest.java:12)
at GCTest.main(GCTest.java:18)
**첫째 할당은 제대로 되었지만 두번째 할당은 메모리 부족 현상이라 마이너 GC가 발생하고 연달아 Full GC가 발생합니다. 하지만 메모리는 여전히 부족합니다. 왜냐면 메소드가 반환되기 이전에는 b의 참조가 존재하므로 (사용되지 않더라도) CG대상에서 제외 되었기 때문입니다.
당연히 OOM(Out Of Memory)가 나는 상황이며 b2를 할당하는 시점에서 GC가 일어난다고 예상이 됩니다.(메모리 부족상황에서는 반드시 GC가 일어납니다)
위의 예제는 극단적인 상황을 예로 표현한 것이며 큰 규모의 프로젝트에서는 언제든 발생할 수 있는 상황입니다. 예를 들어 여러 스레드가 동작하며 각각 어떤 메소드를 실행하는데 그 메소드가 이런 구조라면
메소드 호출 -> 수많은 객체 생성 -> 수많은 객체 사용(이후 사용안함) -> 블록되는 I/O처리 -> 반환
메소드가 종료되기 전까지 참조를 물고있지만 사용되지 않는 지역변수들이 쌓여만 갈 것 입니다. 결국 메모리가 부족하여 GC가 수행되더라도 GC의 대상이 되어야 할 객체들은 참조를 물고 있기 때문에 OOM으로 직행하게 됩니다.
예제 2) null을 주면 어떻게 될까?
public class GCTest2 {
private final static int ALLOC_SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.60);
public void allocate() {
System.out.println("Before first allocation");
byte[] b = new byte[ALLOC_SIZE];
System.out.println("After first allocation");
b = null;
System.out.println("After assign null to b");
System.out.println("Before second allocation");
byte[] b2 = new byte[ALLOC_SIZE];
System.out.println("After second allocation");
}
public static void main(String[] args) {
new GCTest2().allocate();
}
}
** 첫예제와 상황은 비슷하지만 b를 사용하고난 뒤 null을 할당 합니다.
예제 2 결과)
Before first allocation
After first allocation
After assign null to b
Before second allocation
[GC 555205K->555168K(712576K), 0.0019313 secs]
[GC 555168K->555104K(712576K), 0.0027482 secs]
[Full GC 555104K->208K(105344K), 0.0509913 secs]
After second allocation
**예상대로 OOM은 발생하지 않았습니다.
메소드 호출 -> 수많은 객체 생성 -> 수많은 객체 사용(이후 사용안함) -> 블록되는 I/O처리 -> 반환
의 구조를
메소드 호출 -> 수많은 객체 생성 -> 수많은 객체 사용(이후 사용안함)-> 사용된 객체를 null할당 -> 블록되는 I/O처리 -> 반환
으로 사용하였습니다.
OOM이 될 뻔한 상황을 GC를 통해 해결할 수 있었습니다.
null할당은 GC가 일어나기만 하면 바로 회수해 가버리고 이후에 객체를 사용할 수 없지만
1. GC를 한번 해보고 메모리가 충분하다면 계속 참조하고 싶다.
2. GC가 일어나기 전까지만 계속 참조하고 싶다.
3. Finalize 이후 뭔가 더 처리하고 싶다.
의 요구사항을 통해 자바 1.2버전 이후 java.lang.ref라는 패키지를 제공해 주었습니다.
여기에 딱 5가지 클래스가 존재합니다.
Reference, PhantomReference, SoftReference, WeakReference, ReferenceQueue
여기서 주로 사용되는 Reference는
1. GC를 한번 해보고 메모리가 충분하다면 계속 참조하고 싶다. : SoftReference
2. GC가 일어나기 전까지만 계속 참조하고 싶다. : WeakReference
3. Finalize 이후 뭔가 더 처리하고 싶다. : PhantomReference
예제 1 ) SoftReference
import java.lang.ref.SoftReference;
public class GCTest3 {
private final static int ALLOC_SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.60);
public void allocate() {
System.out.println("Before first allocation");
SoftReference<byte[]> b = new SoftReference<byte[]>(new byte[ALLOC_SIZE]);
System.out.println("After first allocation");
System.out.println("b is alive ? " + b.get());
System.out.println("Before second allocation");
byte[] b2 = new byte[ALLOC_SIZE];
System.out.println("After second allocation");
System.out.println("b is alive ? " + b.get());
}
public static void main(String[] args) {
new GCTest3().allocate();
}
}
예제 1 결과) 한번 GC해보고 그래도 부족하니 null
Before first allocation
After first allocation
b is alive ? [B@7150bd4d
Before second allocation
[GC 555205K->555136K(712576K), 0.0016918 secs]
[GC 555136K->555104K(712576K), 0.0014965 secs]
[Full GC 555104K->555088K(660160K), 0.0045636 secs]
[GC 555088K->555088K(712576K), 0.0068316 secs]
[Full GC 555088K->186K(110208K), 0.0575150 secs]
After second allocation
b is alive ? null
예제 2 ) WeakReferece
import java.lang.ref.WeakReference;
public class GCTest3 {
private final static int ALLOC_SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.60);
public void allocate() {
System.out.println("Before first allocation");
WeakReference<byte[]> b = new WeakReference<byte[]>(new byte[ALLOC_SIZE]);
System.out.println("After first allocation");
System.out.println("b is alive ? " + b.get());
System.out.println("Before second allocation");
byte[] b2 = new byte[ALLOC_SIZE];
System.out.println("After second allocation");
System.out.println("b is alive ? " + b.get());
}
public static void main(String[] args) {
new GCTest3().allocate();
}
}
예제 2 결과) 꽉차면 바로 null
Before first allocation
After first allocation
b is alive ? [B@64c3c749
Before second allocation
[GC 555205K->555168K(712576K), 0.0016671 secs]
[GC 555168K->555104K(712576K), 0.0015086 secs]
[Full GC 555104K->208K(105344K), 0.0562847 secs]
After second allocation
b is alive ? null
추가 참고 자료
참조의 강한 정도 : Strong > Soft > Weak > Phantom
Strong References
일반적인 객체 참조를 말함
일반적으로는 gc에도 정리되지 않지만 항상 그렇진 않다.
예) Server svr = new Server();
Soft References
WeakReference와 비슷하나 gc에 정리될 가능성이 적다
VM이 OOM가 발생할 것같으면 정리함, 최대한 늦게 정리
Weak References
메모리에 머물지 않고, 다음 gc때 정리 대상
예) WeakReference<Cachable> weakData = new WeakReference<Cacheable>();
다시 쓸려고 할때 항상 null 체크 유의
Phantom References
참조중 가작 약한 참조, get 하면 항상 null 나옴
ReferenceQueue는 죽은 참조의 상태를 관리
WeakReference는 finalized 또는 gc 전에 ReferenceQueue에 enqueuing되는 반면(살릴수있다)
PhantomReference는 finalized 또는 gc 후에 ReferenceQueue에 enqueuing된다.
좋은 점은 개발자가 메모리로 부터 객체 제거를 확실히 결정하게 해준다.(예로 큰이미지 다룰때)
실제로는 거의 사용하지 않음
참고자료
http://www.rallydev.com/community/engineering/java-references-strong-soft-weak-phantom
https://weblogs.java.net/blog/2006/05/04/understanding-weak-references
'IT > Programming' 카테고리의 다른 글
<JAVA 메모리관리> 가비지 콜렉터 (0) | 2023.04.17 |
---|---|
<JAVA 메모리 관리> JVM (2) | 2023.04.17 |
<JAVA 메모리관리> JVM과 그 구조 (0) | 2023.04.17 |
<디자인 패턴> Singleton 패턴 (0) | 2023.04.17 |
<디자인 패턴> Factory 패턴 (0) | 2023.04.17 |