반응형

자바는 가비지 컬렉션이 알아서 메모리를 관리해주기 때문에 C와 같은 언어보다 메모리에 대해 생각하지 않고 일반적으로 코딩을 한다.

 

하지만 자바 애플리케이션을 만들어 실행하다 보면 OOM이 떨어지는 경우가 있다.

즉 어디선가 가비지 컬렉션이 청소할 수 없는 객체들이 쌓여서 메모리 누수 (leak)가 발생한 것이다.

 

아래 Stack을 구현한 pop 메서드 예제를 보자.

	/**
	 * This method only decreases the size of the stack.
	 * The object at the corresponding array index becomes an obsolete reference.
	 */
	public Object pop() {
		if (size == 0) {
			throw new EmptyStackException();
		}

		return elements[--size];
	}

 

Stack의 데이터를 꺼내오면서 Stack의 허용치를 1 증가시키기 위해 인덱스를 감소시켰다.

 

위 코드의 문제점은 무엇일까?

pop을 통해 꺼내온 객체는 아직까지도 Stack의 array가 참조하고 있다. 결국 해당 array 위치에 새로운 객체가 할당되지 않는다면 프로그램이 종료될 때까지 가비지 컬렉션은 해당 객체가 가비지인지 알 수 없다.

 

따라서 위 코드는 아래와 같이 명시적으로 null로 만들어 참조를 제거해야 한다.

	public Object popDoThis() {
		if (size == 0) {
			throw new EmptyStackException();
		}
		Object result = elements[--size];
		elements[size] = null; /* to let gc do its work */

		return result;
	}

 

Stack 예시처럼 자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해야 한다.

 

java.util.WeakHashMap : key에 대한 메모리 참조가 없으면 자동으로 데이터를 삭제하는 Map

자바에서는 메모리 누수가 발생할 수 있는 자료구조에 대한 몇 가지 해결책을 제공한다. 첫 번째로 java.util.WeakHashMap이다.

 

WeakHashMap은 Map이므로 Key와 Value를 한쌍의 데이터로 관리한다. 이때 Key에 대한 참조가 더 이상 존재하지 않게 되면, Value를 가져올 수 있는 방법이 없다고 판단하여, 해당 Key-Value 쌍은 자동으로 삭제되는 Map이다.

 

아래 예제를 보자.

	/**
	 * We put object reference into a cache and forget that we put it there.
	 * To solve this problem we often implement caches using WeakHaspMap.
	 * A WeakHashMap will automatically remove value when its key is no longer referenced.
	 */
	public static void main(String[] args) {
		WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<Integer, String>();

		Integer key = new Integer(1);
		weakHashMap.put(key, "1");
		key = null;

		// If GC is generated, the output changes to {}.
		while (true) {
			System.out.println(weakHashMap);
			System.gc();
			if (weakHashMap.size() == 0) {
				break;
			}
		}

		System.out.println("End");
	}

Key가 1의 값을 가진 Integer 객체이고, Value를 "1" 로하여 WeakHashMap에 put 하였다. 이후, Key값인 Integer의 참조를 null로 만들어 더 이상 참조가 일어나지 않도록 하였다. 이후 GC를 발생시키면 Key의 대한 참조가 없다고 판단하여, 쌍이 사라진 예제이다.

 

참고로, String 클래스를 Key로 하는 WeakHashMap을 사용하면 의미가 없다. 왜냐하면 규칙 5에서 설명했듯이 String은 내부적으로 한 번 생성된 String에 대해 Constant Pool에 항상 참조가 존재하기 때문이다.

 

java.util.LinkedHashMap : 가장 오래된 데이터를 처리할 수 있는 Map

java.util.LinkedHashMap은 HashMap가 다르게 데이터를 넣은 순서를 알 수 있다. 순서를 알 수 있으므로 LinkedHashMap은 아래와 같은 특별한 메서드를 제공한다.

 

    /**
     * Returns <tt>true</tt> if this map should remove its eldest entry.
     * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
     * inserting a new entry into the map.  It provides the implementor
     * with the opportunity to remove the eldest entry each time a new one
     * is added.  This is useful if the map represents a cache: it allows
     * the map to reduce memory consumption by deleting stale entries.
     *
	 * ....
     *
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns <tt>true</tt>.  If the map was empty prior
     *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   <tt>true</tt> if the eldest entry should be removed
     *           from the map; <tt>false</tt> if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

 

put() 또는 putAll() 메서드를 호출하고 나서 자동으로 호출되는 removeEldestEntry 메서드는 가장 오래된 데이터를 삭제할지를 검사하는 메서드이다.

 

true를 리턴하게 되면 가자 오래된 데이터를 삭제하고, false를 리턴하면 삭제하지 않는다. 디폴트로는 false를 리턴하도록 되어있어 항상 삭제하지 않는다.

 

아래는 Map의 크기가 5인 LinkedHashMap의 예제이다.

	public static void main(String[] args) {
		final int MAX_ENTRIES = 5;
		LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<String, String>() {
			@Override
			public boolean removeEldestEntry(Map.Entry eldest) {
				return size() > MAX_ENTRIES;
			}
		};

		linkedHashMap.put("1", "a");
		linkedHashMap.put("2", "b");
		linkedHashMap.put("3", "c");
		linkedHashMap.put("4", "d");
		linkedHashMap.put("5", "e");
		linkedHashMap.put("6", "f"); /* {1=a} disappear and this item will be added. */

		for (Iterator<String> hashitr = linkedHashMap.values().iterator(); hashitr.hasNext();) {
			System.out.print(hashitr.next() + " ");
		}
	}

 

반응형

+ Recent posts