레퍼런스 객체라는 제목의 1999년 5월 11일자 테크팁에서 레퍼런스 객체의 개념에 대해 다룬 바 있다. 이번 테크팁에서는 이 주제를 좀더 심도 있게 다루어보도록 한다. 기본적으로 레퍼런스 객체는 객체가 필요로 하는 메모리를 간접적으로 참조하는 방법을 제공하며, 레퍼런스 객체에 대해 도달능력(Reachability)을 모니터하는 레퍼런스 큐(
자바 플랫폼의 경우 객체에 대해 네 가지 타입의 레퍼런스가 존재하는데, 일반적으로 사용하는 타입으로 Direct Reference를 들 수 있다.
Soft Reference는 데이터 캐시와 그 기능이 유사한데, 시스템 메모리가 낮을 때 가비지 컬렉터는 Soft Reference가 유일한 레퍼런스인 객체를 임의로 비울 수 있다. 다시 말해서, 객체에 대한 일반 참조가 없는 경우 그 객체가 릴리즈 후보가 되는 것이다. 한편, 가비지 컬렉터는
Weak Reference는 Soft Reference보다는 다소 불완전하다. 객체에 대한 유일한 레퍼런스가 Weak Rererence뿐이라면 가비지 컬렉터는 언제라도 오브젝트가 사용하는 메모리를 이용할 수 있으며, 메모리가 낮은 상태라면 별다른 요구사항은 발생하지 않는다. 일반적으로 객체가 사용하는 메모리는 후속 가비지 컬렉터 패스에서 이용된다.
Phantom Reference는 cleanup 작업과 관련이 있다. 이들은 가비지 컬렉터가 마무리 과정을 수행하고 객체를 비우기 바로 직전에 통지를 하는데, 이는 객체 내에서 cleanup 작업을 수행하는 방법 중 한 가지라고 생각하면 된다.
프로그램을 실행하면 다음과 같은 결과가 나온다.
실제로 이를 증명해보이기 위해
임포트와는 별도로, 클래스 정의를 시작하면 클래스 및 로컬 변수 선언이 이루어진다.
리스너 리스트는
마지막 변수
ReferenceQueue 클래스) 내에서 유지된다. 정상적인 객체 레퍼런스가 릴리즈되지 않을 경우 가비지 컬렉터는 레퍼런스 객체 타입을 기초로 해서 메모리를 비우기도 한다. 자바 플랫폼의 경우 객체에 대해 네 가지 타입의 레퍼런스가 존재하는데, 일반적으로 사용하는 타입으로 Direct Reference를 들 수 있다.
Object obj = new Object()특히 Direct Reference는 객체 액세스를 위해 별도의 코딩을 필요로 하지 않는 일반 참조(strong references )라 할 수 있으며, 나머지 세 타입의 레퍼런스는
java.lang.ref 패키지에 포함되어 있는 Reference 클래스의 서브클래스들이다. Soft Reference는 SoftReference 클래스에 의해, Weak Reference는 WeakReference 클래스에 의해, 그리고 Phantom Reference는 PhantomReference 클래스에 의해 제공된다. Soft Reference는 데이터 캐시와 그 기능이 유사한데, 시스템 메모리가 낮을 때 가비지 컬렉터는 Soft Reference가 유일한 레퍼런스인 객체를 임의로 비울 수 있다. 다시 말해서, 객체에 대한 일반 참조가 없는 경우 그 객체가 릴리즈 후보가 되는 것이다. 한편, 가비지 컬렉터는
OutOfMemoryException을 throw하기 전에 임의의 Soft Reference를 릴리즈해야 한다. Weak Reference는 Soft Reference보다는 다소 불완전하다. 객체에 대한 유일한 레퍼런스가 Weak Rererence뿐이라면 가비지 컬렉터는 언제라도 오브젝트가 사용하는 메모리를 이용할 수 있으며, 메모리가 낮은 상태라면 별다른 요구사항은 발생하지 않는다. 일반적으로 객체가 사용하는 메모리는 후속 가비지 컬렉터 패스에서 이용된다.
Phantom Reference는 cleanup 작업과 관련이 있다. 이들은 가비지 컬렉터가 마무리 과정을 수행하고 객체를 비우기 바로 직전에 통지를 하는데, 이는 객체 내에서 cleanup 작업을 수행하는 방법 중 한 가지라고 생각하면 된다.
WeakHashMap 사용법에 관한 한 예로, 다음의 WeakTest 프로그램은 하나의 엘리먼트를 포함하는 WeakHashMap를 생성한다. 그런 다음 맵이 비워질 때까지 기다리는 두 번째 스레드를 생성하여 가비지 컬렉터가 1/2초마다 실행될 것을 요구한다. 메인 스레드는 이 두 번째 스레드가 종료될 때까지 대기한다. import java.util.*;
public class WeakTest {
private static Map<String, String> map;
public static void main (String args[]) {
map = new WeakHashMap<String, String>();
map.put(new String("Scott"), "McNealey");
Runnable runner = new Runnable() {
public void run() {
while (map.containsKey("Scott")) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
System.out.println("Checking for empty");
System.gc();
}
}
};
Thread t = new Thread(runner);
t.start();
System.out.println("Main joining");
try {
t.join();
} catch (InterruptedException ignored) {
}
}
}
맵에서 유일한 키에 대한 일반 참조가 없으므로 맵 엔트리는 시스템의 여건이 허락하는 가장 이른 시기에 가비지 컬렉트 되어야 한다. 프로그램을 실행하면 다음과 같은 결과가 나온다.
Main joining Checking for empty이 예제에서 지적해야 할 중요한 사실이 두 가지 있다. 첫째, 통상적으로
System.gc()에 대한 호출은 요구되지 않는다는 점. WeakTest 프로그램은 나름대로 부담이 덜하고 메모리를 많이 사용하지 않기 때문에 가비지 컬렉터에 대한 명시적 호출이 필요하다. 둘째, new String("Scott")에 대한 호출에 유의할 것. 최종 결과가 똑같이 "Scott" 스트링일 경우 사용자는 왜 new String()을 호출해야 하는지 궁금해할 수 있을 것이다. 그 이유는 new String()을 호출하지 않으면 맵 키에 대한 참조가 시스템의 스트링 상수 풀이 될 것이기 때문이다. 이 상황은 변하지 않으므로 Weak Reference가 릴리즈되는 경우는 절대로 발생하지 않을 것이다. 이러한 문제를 해결하기 위해 new String("Scott")은 스트링 상수 풀 내의 레퍼런스에 대한 레퍼런스를 생성하게 된다. 또한, 스트링 컨텐츠는 결코 복제되지 않고 상수 풀 내에 남아 있게 되며, 이 경우 단순히 풀 내의 스트링 상수에 대한 별도의 포인터가 생성된다. WeakHashMap을 이용하는데 있어서 조금 더 복잡한 방법은 Swing 컴포넌트나 데이터 모델의 리스너 리스트를 관리하는 것인데, 이는 범용 리스너 리스트라기보다는 불충분한 리스너 리스트가 될 공산이 크다. 이 경우, 일반 참조가 컴포넌트나 데이터 모델 외부에 머무는 한 해당 객체는 계속 리스너/옵저버에게 통지하게 되며, 이는 소스 객체가 가비지 컬렉트되는 것을 리스너가 (리스너를 등록한) 막지 않는다는 의미에서 가비지 컬렉션에 도움이 된다. 기본값으로 리스너 레퍼런스는 일반 참조이며 메소드가 리턴될 때 가비지 컬렉트되지 않는다. 상황 모니터링이 종료된 후에는 리스너를 제거하는 것을 잊지 말아야 한다. 실제로 이를 증명해보이기 위해
ListModel을 제공하는 WeakListModel을 생성해 보기로 하자. ListModel은 WeakHashMap을 기초로 하여 ListDataListener 객체를 저장한다. 임포트와는 별도로, 클래스 정의를 시작하면 클래스 및 로컬 변수 선언이 이루어진다.
public class WeakListModel implements ListModel,
Serializable {
private Map<ListDataListener, Object> listenerList =
Collections.synchronizedMap(
new WeakHashMap<ListDataListener, Object>());
private final Object present = new Object();
private ArrayList<Object> delegate = new ArrayList<Object>();
리스너 리스트는 WeakHashMap이고, 액세스는 동기화된다. 어떤 경우에는 리스너 리스트 구현이 사본을 만드는 방식으로 액세스 동기화를 막지만 Weak Reference는 언제라도 제거될 수 있다. 따라서 사본을 만들 경우 추가의 값 없이 Weak Reference가 드롭될 2개의 장소가 제공된다. 리스너 리스트는
Map 내에서 유지되기 때문에 키와 값이 모두 필요한데, 맵 내의 모든 키와 관련이 있는 값은 'present' 객체이다. 이론적으로는 오직 WeakHashSet만 필요하지만, 클래스는 미리 정해진 일련의 컬렉션에 속하지 않으므로 대신 WeakHashMap이 사용된다. 마지막 변수
delegate가 ListModel 컨텐츠를 관리한다. 대부분의 ListModel 인터페이스 구현의 경우 단순히 요청을 delegate에 패스하여 연산을 수행하기 때문에 delegate라는 이름을 갖게 되었다. WeakListModel의 첫 번째 세트의 메소드는 모델 구조의 사이즈 또는 쿼리와 관련이 있으며, 항시 호출을 delegate로 패스한다. public int getSize() {
return delegate.size();
}
public Object getElementAt(int index) {
return delegate.get(index);
}
public void trimToSize() {
delegate.trimToSize();
}
public void ensureCapacity(int minCapacity) {
delegate.ensureCapacity(minCapacity);
}
public int size() {
return delegate.size();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public Enumeration elements() {
return Collections.enumeration(delegate);
}
public boolean contains(Object elem) {
return delegate.contains(elem);
}
public int indexOf(Object elem) {
return delegate.indexOf(elem);
}
public int lastIndexOf(Object elem) {
return delegate.lastIndexOf(elem);
}
public Object elementAt(int index) {
return delegate.get(index);
}
public Object firstElement() {
return delegate.get(0);
}
public Object lastElement() {
return delegate.get(delegate.size()-1);
}
public String toString() {
return delegate.toString();
}
다음 세트의 메소드는 엘리먼트의 추가 및 제거와 관련이 있다. 스토리지 연산을 위해 delegate를 액세스하는 것 외에도, 일련의 리스너가 통지되어야 한다. 이 메소드들은 fireXXX() 메소드를 호출하여 곧바로 표시할 뿐 아니라 클래스 내에서 대부분의 작업을 수행하게 된다. public void setElementAt(Object obj, int index) {
delegate.set(index, obj);
fireContentsChanged(this, index, index);
}
public void removeElementAt(int index) {
delegate.remove(index);
fireIntervalRemoved(this, index, index);
}
public void insertElementAt(Object obj, int index) {
delegate.add(index, obj);
fireIntervalAdded(this, index, index);
}
public void addElement(Object obj) {
int index = delegate.size();
delegate.add(obj);
fireIntervalAdded(this, index, index);
}
public boolean removeElement(Object obj) {
int index = indexOf(obj);
boolean rv = delegate.remove(obj);
if (index >= 0) {
fireIntervalRemoved(this, index, index);
}
return rv;
}
public void removeAllElements() {
int index1 = delegate.size()-1;
delegate.clear();
if (index1 >= 0) {
fireIntervalRemoved(this, 0, index1);
}
}
리스너 리스트 자체가 리스너 추가 및 제거 호출을 처리하는데, 이번에도 present 객체는 정상적으로 사용되지 않는다. '맵'은 단지 키와 값을 필요로 할 뿐이며, 이는 TreeMap의 지원을 받는 TreeSet에 의해 사용되는 경우와 동일하다고 보면 된다. public synchronized void addListDataListener(
ListDataListener l) {
listenerList.put(l, present);
}
public synchronized void removeListDataListener(
ListDataListener l) {
listenerList.remove(l);
}
public EventListener[] getListeners(Class listenerType) {
Set<ListDataListener> set = listenerList.keySet();
return set.toArray(new EventListener[0]);
}
가장 흥미로운 메소드 세트는 fireXXX() 메소드들로, 이 세 가지 메소드 fireContentsChanged(), fireIntervalAdded(), fireIntervalRemoved() 모두 WeakHashMap의 키에 의해 새로운 Set가 생성된다. 이는 세트가 통지 도중에 변경될 경우에도 여전히 원본 세트가 통지될 수 있도록 하기 위한 것이다. protected synchronized void fireContentsChanged(
Object source, int index0, int index1) {
ListDataEvent e = null;
Set<ListDataListener> set =
new HashSet<ListDataListener>(listenerList.keySet());
Iterator<ListDataListener> iter = set.iterator();
while (iter.hasNext()) {
if (e == null) {
e = new ListDataEvent(
source, ListDataEvent.CONTENTS_CHANGED,
index0, index1);
}
ListDataListener ldl = iter.next();
ldl.contentsChanged(e);
}
}
protected synchronized void fireIntervalAdded(
Object source, int index0, int index1) {
ListDataEvent e = null;
Set<ListDataListener> set =
new HashSet<ListDataListener>(listenerList.keySet());
Iterator<ListDataListener> iter = set.iterator();
while (iter.hasNext()) {
if (e == null) {
e = new ListDataEvent(
source, ListDataEvent.INTERVAL_ADDED,
index0, index1);
}
ListDataListener ldl = iter.next();
ldl.intervalAdded(e);
}
}
protected synchronized void fireIntervalRemoved(
Object source, int index0, int index1) {
ListDataEvent e = null;
Set<ListDataListener> set =
new HashSet<ListDataListener>(listenerList.keySet());
Iterator<ListDataListener> iter = set.iterator();
while (iter.hasNext()) {
if (e == null) {
e = new ListDataEvent(
source, ListDataEvent.INTERVAL_REMOVED,
index0, index1);
}
ListDataListener ldl = iter.next();
ldl.intervalRemoved(e);
}
}
임포트를 추가하고 괄호를 닫으면 각자의 클래스 정의가 생성된다. import java.io.Serializable; import java.util.*; import java.lang.ref.*; import javax.swing.*; import javax.swing.event.*; ... }
ListModel을 테스트하기 위해 다음의 프로그램 TestListModel은 ListDataListener를 정의하고 이를 생성된 WeakListModel에 연결한다. 썬의 설립자 이름을 엘리먼트로서 모델에 추가/제거하면 리스너에 대한 통지가 이루어진다는 점에 유의할 것. 리스너에 대한 일반 참조가 사라지는 즉시 WeakListModel은 불충분한 리스너 레퍼런스를 릴리즈할 수 있으며, 그럴 경우 모델에 대한 추가 연산은 리스너에게 통지되지 않는다. import javax.swing.*;
import javax.swing.event.*;
public class TestListModel {
public static void main (String args[]) {
ListDataListener ldl = new ListDataListener() {
public void intervalAdded(ListDataEvent e) {
System.out.println("Added: " + e);
}
public void intervalRemoved(ListDataEvent e) {
System.out.println("Removed: " + e);
}
public void contentsChanged(ListDataEvent e) {
System.out.println("Changed: " + e);
}
};
WeakListModel model = new WeakListModel();
model.addListDataListener(ldl);
model.addElement("Scott McNealy");
model.addElement("Bill Joy");
model.addElement("Andy Bechtolsheim");
model.addElement("Vinod Khosla");
model.removeElement("Scott McNealy");
ldl = null;
System.gc();
model.addElement("Scott McNealy");
System.out.println(model);
}
}
TestListModel 프로그램을 컴파일하여 실행하면 다음과 같은 결과가 표시된다. > java TestListModel Added: javax.swing.event.ListDataEvent[type=1,index0=0,index1 =0] Added: javax.swing.event.ListDataEvent[type=1,index0=1,index1 =1] Added: javax.swing.event.ListDataEvent[type=1,index0=2,index1 =2] Added: javax.swing.event.ListDataEvent[type=1,index0=3,index1 =3] Removed: javax.swing.event.ListDataEvent[type=2,index0=0,inde x1=0] [Bill Joy, Andy Bechtolsheim, Vinod Khosla, Scott McNealy]레퍼런스 객체, 통지, 도달능력 등에 관한 추가 정보를 보려면
java.lang.ref 패키지 문서를 참조하기 바란다."Java SE" 카테고리의 다른 글
- 리스너 리스트를 위한 WEAKHASHMAP 사용하기 (댓글 1개 / 트랙백 0개) 2006/03/08
- J2SE 5.0의 Java 2D API 기능 강화 (댓글 2개 / 트랙백 0개) 2006/05/12
- 3D 화면(scene)에 빛 효과 주기 (댓글 1개 / 트랙백 0개) 2004/07/30
- 다이얼로그 Modality (댓글 1개 / 트랙백 0개) 2006/06/09
- 쿠키 처리 (댓글 22개 / 트랙백 3개) 2007/07/23
- 사용자 데이터그램 프로토콜의 프로그래밍 (댓글 1개 / 트랙백 0개) 2004/06/30
- 락(LOCKS) (댓글 1개 / 트랙백 0개) 2005/09/22
- Java Web Start 퍼시스턴스 (댓글 3개 / 트랙백 0개) 2006/12/24
- AFFINETRANSFORM 이해하기 (댓글 3개 / 트랙백 0개) 2003/09/09
- 사용자 인터페이스에서 Action 사용하기 (댓글 5개 / 트랙백 2개) 2007/02/22
2006/03/08 09:48
2006/03/08 09:48
댓글을 달아 주세요
좋은 정보 감사해요~
2007/09/19 04:45