본문 바로가기

Dev.../플밍 관련 자료

[펌] 어떻게 객체를 파괴할 것인가?

어떻게 객체를 파괴할 것인가?

Java 플랫폼의 모든 배열과 객체들은 '힙(heap)'이라는 메모리 공간에 저장된다. new 키워드를 쓸 때마다 힙의 새로운 메모리가 객체에 할당된다. 하지만, Java는 C++ 같은 언어와 달리 할당한 메모리를 명시적으로 반환하는 방법이 없다. 사실, 이 작업은 가비지 컬렉터가 담당한다. 가비지 컬렉터는 아주 낮은 우선순위를 가진 백그라운드 스레드로 동작하면서 어떤 객체의 메모리를 반환해야 하는지 계속 검사한다. 만약, 메모리를 반환해야 하는 객체를 찾았고 시간도 충분하다면 가비지 컬렉터는 종료자를 수행하는 것과 같이 몇 가지 필요한 작업을 처리하고 객체를 파괴한 다음에 이 객체의 메모리를 힙으로 반환한다. 프로그래머가 무엇을 하든 가비지 컬렉터에게 이 작업을 강제로 시킬 수는 없다. 하지만, 객체 참조에 null을 대입하고 System.gc()를 호출하면 <그림 4>처럼 객체 그래프에서 더 이상 참조되지 않는 객체들이 생기고 JVM이 한가하다면 가비지 컬렉션이 일어나면서(반드시 가비지 컬렉션이 일어난다는 것을 보장할 수는 없다.) 더 이상 참조되지 않는 객체들을 파괴한다(실제로 이것보다 훨씬 복잡한 알고리즘을 쓰고 있지만, 이 정도만 알아도 충분할 것이다.)



<그림 4> 가비지 컬렉션 대상을 고르는 방법


이런 방식으로 메모리를 할당하고 반환하면 아주 안전하기는 하지만 프로그래머가 메모리를 직접 관리할 수 없어서 불편할 수 있다. 이 문제를 해결하기 위해 JDK 1.2 배포판부터 java.lang.ref 패키지를 제공해서 프로그램에서 가비지 컬렉터에 접근할 수 있는 방법을 제공하고 있다. 자세한 것은 Java 명세 문서를 참조하기 바란다.
그런데, Java에서는 가비지 컬렉터가 객체 파괴를 처리해 주므로 메모리에 대해 신경 쓰지 않아도 될 것 같지만, 몇 가지 주의할 점이 있다.

다음과 같이 구현한 간단한 스택을 한 번 살펴보자.
	public class stack {		private object [] elements;		private int size = 0;		public Stack(int initialCapacity) { 			this.elements = new Object[initialCapacity];		}			public void push(Object e) {			ensureCapacity();			elements[size++] = e;		}			public Object pop() {			if(size == 0)				throw new EmptyStackException();			return elements[--size];		}			private void ensureCapacity() {			if (elements.length == size) {				Object[] oldElements = elements;				elements = new Object[2*elements.length+1];				System.arraycopy(oldElements, 0, elements, 0, size);			}		}	}
이 스택은 별 문제가 없어 보이지만 스택에서 팝(pop)된 객체들에 대한 참조를 스택이 계속 쥐고 있기 때문에 이 객체들은 가비지 컬렉션 대상이 되지 않고 끝까지 남는다. 가비지 컬렉터가 있는 언어에서도 메모리 누수 현상('의도하지 않은 객체 유지(unintentional object retention)'가 더 적절한 표현이다.)이 일어날 수 있다! 이런 문제는 간단하게 해결할 수 있다. 쓸모 없어진 참조에 다음과 같이 null을 대입해 버리면 된다.

	public Object pop() {		if(size == 0)			throw new EmptyStackException();		Object result = elements[--size];		elements[size] = null;    // 쓸모 없는 참조를 없앤다.		return result;	}
Stack처럼 자신만의 메모리 공간을 가지는 클래스는 언제나 메모리 누수가 일어날 가능성이 있다. 어떤 객체를 보관할 필요가 없는지는 프로그래머만 알 수 있다. 따라서, 프로그래머가 직접 필요 없는 객체 참조를 null로 만들지 않으면 가비지 컬렉터는 어떤 객체를 가비지 컬렉션할지 알 수가 없다. 따라서, 자신만의 메모리 공간을 가지는 클래스를 쓸 때 보관할 필요가 없는 객체가 생기면 이 객체에 대한 참조 변수에 null을 대입하여 가비지 컬렉션 대상으로 만들어야 한다.

객체 파괴에서 또 하나 주의할 점은, finalize, System.runFinalization, System.runFinalizersOnExit, Runtime.runFinalizersOnExit와 같은 종료자는 쓰지 않는 것이 좋다는 것이다. 종료자들은 정확히 실행된다는 보장도 없고 종료자 메소드에서 처리하지 않는 예외가 발생하면 예외도 무시되고 종료자도 끝나버린다. 이런 여러가지 문제 때문에 종료자에 의존하는 코드는 되도록 만들지 않도록 한다.