티스토리 뷰
(Effective Java) 규칙7. 다 쓴 객체 참조를 해제하라
-
C, C++ 처럼 손수 메모리 관리를 해야 하는 언어를 쓰다 쓰레기 수집가 포함된 언어를 사용하기 시작하면 프로그래밍이 아주 쉬워진다.
- 볼 일 없는 객체는 자동적으로 반환되기 때문에
메모리 누수가 발생하는 Stack 클래스
//"메모리 누수(memory leak)가" 어디서 생기는지 보이는가?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
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) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
-
이 프로그램에는 메모리 누수 문제가 있음
- 스택이 커졌다가 줄어들면서(
pop
메서드) 제거한 객체들을 쓰레기 수집기가 처리하지 못하는 문제가 발생
- 스택이 커졌다가 줄어들면서(
-
스택이 만기 참조(obsolete reference)를 제거하지 않고 가지고 있기 때문
- 만기 참조란, 다시 이용되지 않을 참조를 뜻함
elements
배열에서 실제로 사용되는 부분(active portion)을 제외한 나머지 지역에 보관됨 참조 들이 만기 참조임
-
자동적으로 쓰레기 객체를 수집하는 언어에서 발생하는 메모리 누수 문제(의도치 않는 객체 보유)는 찾아내기 어려움
- 실수로 객체 참조를 계속 유지하는 경우, 해당 뿐만 아니라 그 객체를 통해 참조되는 다른 객체들도 제외됨
- 만기 참조가 몇개라도 있으면 굉장히 많은 객체가 쓰레기 수집에서 제외될 수 있음
null 처리를 통한 메모리 누수 해결 방안
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; //만기 참조 제거
return result;
}
-
참조를 null로만들면 나중에 실수로 그 참조를 사용하더라도
NullPointerException
이 발생하기 때문에 프로그램이 바로 종료된다는 장점이 있음 -
위 코드의 문제는 Stack이 자체적으로 메모리를 관리 한다는 것이 문제임
- 저장 공간 풀은
elements
배열의 원소임 (각 원소는 객체가 아닌 개체에 대한 참조) - 사용되는 객체는 할당된(allowcated) 객체지만, 나머지 원소가 참조하는 객체는 반환 가능한(free) 객체
- 하지만 쓰레기 수집기에서는
elements
내의 참조들은 전부 유효(valid) 해보임
- 하지만 쓰레기 수집기에서는
- 사용하지 않는 객체를 null로 만들면 쓰레기 수집기는 반환해도 좋은 객체가 어떤 것인지 바로 알 수 있음
- 저장 공간 풀은
-
단, 객체 참조를 null 처리 하는 것은 규범(norm)이라기보단 예외적인 조치가 되어야 함
-
만기 참조를 제거하는 가장 좋은 방법은 해당 참조가 보관된 변수가 유효범위를 벗어나게 두는 것이다.
- 변수를 정의할 때 그 유효범위를 최대한 좁게 만들면 자연스럽게 해결됨
메모리 누수가 흔히 발생하는 곳
-
일반적으로, 자체적으로 관리하는 메모리가 있는 클래스를 만들때는 메모리 누수가 발생하지 않도록 주의해야 한다.
- 더 이상 사용되지 않는 원소 객체 참조는 반드시 null로 바꿔 주어야 함
캐시(cache) 메모리
-
문제
- 객체 참조를 캐시 안에 넣어 놓고 잊어버리는 일이 많기 이다.
-
해결방법
WeakHashMap
을 가지고 캐시를 구현하는 것- 키에 대한 참조가 만기 참조가 되는 순간 캐시 안에 보관된 키-값 쌍은 자동으로 삭제됨
- 단, 캐시 안에 보관되는 항목의 수명이 키에 대한 외부 참조 수명에 따라 결정되는 상황에만 적용 가능
- 간단한
WeakHashMap
예제
public class TestMain { public static void main(String[] args) { Map<Key, String> map = new WeakHashMap<>(); Key key = new Key("name"); // Map 에 새로운 엔트리 추가 map.put(key, "jayden-lee"); // Key[name]=jayden-lee mapPrint(map); // Key 객체 참조 null 처리 key = null; // 강제 GC System.gc(); // 빈 값 출력 mapPrint(map); } private static void mapPrint(Map<?, ?> map) { map.entrySet().stream().forEach(System.out::println); } } class Key { private String name; public Key(String name) { this.name = name; } @Override public String toString() { return "Key[" + name + "]"; } }
- 간단한
- 후면 스레드(background thread)를 사용하여 처리 하는 방법
Timer
,ScheduledThreaPoolExecutor
사용
- 새로운 엔트리를 츠기힐 떼 부수 작업으로 수행하는 방법
LinkedHashMap
의removeEldestEntry
메서드 사용- 간단한
LRU(leat-recently-used)
캐쉬 구현 예제
public class LRUCache { private static final float hashTableLoadFactor = 0.75f; private LinkedHashMap map; private int cacheSize; public LRUCache(int cacheSize) { this.cacheSize = cacheSize; int hashTableCapacity = (int) Math.ceil(cacheSize / hashTableLoadFactor) + 1; map = new LinkedHashMap(hashTableCapacity, hashTableLoadFactor, true) { private static final long serialVersionUID = 1; @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > LRUCache.this.cacheSize; } }; } /** * The retrieved entry becomes the MRU (most recently used) entry. */ public synchronized V get(K key) { return map.get(key); } /** * If the cache is full, the LRU (least recently used) entry is dropped. */ public synchronized void put(K key, V value) { map.put(key, value); } public synchronized void clear() { map.clear(); } public synchronized int usedEntries() { return map.size(); } public synchronized <Collection> getAll() { return new ArrayList<>(map.entrySet()); } public static void main(String[] args) { LRUCache c = new LRUCache(3); c.put("1", "one"); // 1 c.put("2", "two"); // 2 1 c.put("3", "three"); // 3 2 1 c.put("4", "four"); // 4 3 2 if (c.get("2") == null) throw new Error(); // 2 4 3 c.put("5", "five"); // 5 2 4 c.put("4", "second four"); // 4 5 2 // Verify cache content. if (c.usedEntries() != 3) throw new Error(); if (!c.get("4").equals("second four")) throw new Error(); if (!c.get("5").equals("five")) throw new Error(); if (!c.get("2").equals("two")) throw new Error(); // List cache content. for (Map.Entry e : c.getAll()) System.out.println(e.getKey() + " : " + e.getValue()); } }
- 간단한
리스너(listener) 등의 역호출자(callback)
-
callback
을 사용하는 클라이언트가callback
을 명시적으로 제거하지 않을 경우, 적절한 조치를 취하기 전까지 메모리는 점유된 상태로 남아 있게 된다. -
쓰레기 수집기가 역호출자를 즉시 처리하도록 할 가장 좋은 방법은, 역호출자에 대한 약한 참조만 저장하도록 하는 것
WeakHashMap
의 키로 저장하는 것이 예임
결론
-
메모리 누수는 보통 뚜렷한 오류로 이어지지 않기 때문에, 수년간 시스템에 남아 있는 경우도 있음
-
따라서 문제가 생길 수 있다는 것을 사전에 인지하고 방지 대책을 세우는 것이 바람직함
참고
- http://www.omnibuscode.com/board/board_java/6318
- https://github.com/junior-study/effective-java-study/blob/master/item07.md
끝으로
이 글이 도움이 되었다면, Google 광고 한번씩 클릭 부탁 드립니다. 🙏🙏🙏
광고 클릭은 많은 힘이 됩니다!
'프로그래밍 > EffectiveJava' 카테고리의 다른 글
(이펙티브 자바) 규칙16. public 클래스에서는 public필드가 아닌 접근자 메서드를 사용하라 (0) | 2020.02.26 |
---|---|
(이펙티브 자바) 규칙12. toString을 항상 재정의하라 (0) | 2020.02.25 |
(이펙티브 자바) 규칙22. 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) | 2020.02.20 |
(이펙티브 자바) 규칙20. 추상 클래스 보다는 인터페이스를 우선하라 (0) | 2020.02.18 |
(이펙티브 자바) 규칙18. 상속보다는 컴포지션을 사용하라 (0) | 2020.02.13 |
- Total
- Today
- Yesterday
- 자전거 여행
- 텐트
- Java UI
- 일본 자전거 여행
- intelij
- JavaFX 테이블뷰
- JavaFX
- git
- JavaFX 종료
- java
- 이펙티브자바
- windows
- JavaFX Table View
- springboot
- 배낭여행
- effectivejava
- 자바
- 일본 여행
- 이펙티브 자바
- 배낭 여행
- 이펙티브
- 스프링부트
- TableView
- 자전거
- 일본 배낭여행
- JavaFX Window Close
- 인텔리제이
- effective java
- 방통대 과제물
- 일본여행
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |