언어/Effective Java
이펙티브 자바 Item7 - 다 쓴 객체의 참조를 해제하라
now0204
2024. 5. 28. 11:29
자바에서 사용하지 않는 객체의 경우 GC에서 알아서 회수를 해주며, 메모리 관리를 해준다.
하지만 GC가 메모리 관리를 해준다고 완전 신경을 꺼야하는 건 아니다!
GC에서는 특정 상황에서 메모리 누구가 발생하고, 우리는 그러한 상황을 인지하고 대응해야한다.
메모리 누수는 겉으로 잘 드러나지 않아 수년간 잠복하는 사례도 존재한다.
철저한 코드리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도한다.
따라서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.
메모리 누수가 발생하는 경우
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_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()하는 경우 --size하는 부분을 보면, 꺼낸 데이터를 삭제하는게 아니라 단순히 인덱스만 조정한다. (실제 객체 참조값을 스택이 가지고 있다)
- 이와 같은 경우 당연히 GC가 작동하지 못한다.
- 메모리 누수를 관리하지 않을 경우 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하된다.
해결방법
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object popObject = elements[size];
elements[size--] = null; // 참조 해제
return popObject;
}
- 다 쓴 참조를 null로 변경해주자.
- 다만 NullpointException이 발생할 수 있음으로 잘 관리해주자
null로 항상 바꿔야할까?
사용이 끝난 객체 참조를 null로 변경하는 것은 예외적인 경우에만 사용하는 것이 좋다.
다 쓴 객체 참조를 해제하는 가장 좋은 방법은 해당 객체 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.
null로 변경하는 메모리 관리는 자신의 메모리를 직접 관리하는 클래스를 사용할 때 주의 해서 사용하자
메모리 누수를 발생시키는 3개의 주범
1. Stack과 같이 자신의 메모리를 직접 관리하는 클래스
2. 캐시 메모리
- 객체 참조를 캐시에 넣고 정리를 안해주면 자원은 쌓이고, 캐시의 역할을 못하게 된다.
- 해결방법
- WeakHashMap
- 캐시에 넣어놓고 더이상 쓰지 않아도 계속 들어있을 수 있다. 일반적인 HashMap은 사용여부에 관계없이 Key와 Value 쌍을 지우지 않는다. WeakHashMap은 특정 key값이 더이상 사용되지 않는다고 판단되면 지운다.
- 캐시의 경우 시간이 지남에 따라 사용되지 않으면 그 가치를 떨어뜨리는 방법이다.
- LinkedHashMap.removeEldestEntry -> 사용된지 가장 오래된 엔트리 삭제
- WeakHashMap
3. 리스너, 콜백
- 리스너와 콜백을 등록만하고 해지 안하면 메모리 낭비이다.
- 약한 참조를 넣어서 가비지 컬렉터의 수거 대상이 되도록 하자.
public interface DatabaseManager{
ResultSet execute(final String query);
}
- 위 인터페이스는 콜백 메서드에 의해 작동하며, 콜백 메서드를 정의하는 방법에 따라 GC 대상의 여부가 결정됨
public class CustomDatabaseManager implements DatabaseManager{
// 외부에서 정의되서 참조된다. 클라이언트가 더 이상 인터페이스를 사용하지 않는다는게
// gc 대상이 됨을 의미하지 않는다.
private Connection con;
//생략
public void disconnect(){
this.con = null;
}
public ResultSet execute(final String query){
DBconnection dbCon = con.getDBConnection();
return ResultSet.createResultSet(dbCon);
}
}
public void memeoryLeak() throws InterruptedException{
Connection con = createConnection();
DatabaseManager dbManager = new CustomDatabaseManager(con);
dbManager.execute();
con = null // 참조 해제 시도
//con이 null이어도, CustomDababaseManager안에 con 참조가 남아있다.
//따라서 의존 관계 안에서도 참조를 해제 해야한다.
((CustomDatabaseManager)) dbManager.disconnect();
}
다른 방식으로 여기에 WeakHashMap을 사용할 수 있다.
public class CacheDatabaseManager implements DatabaseManager{
private static final String CAHE_KEY = "Connection Interface";
private WeakHashMap<Connection,Object> cons = new WeakHashMap<>();
public CacheDatabaseManager(Connection con) {this.cons.put(connections,CACHE_KEY);}
public Connection getConnection(){
if(cons.size() <=0){
throw new RuntimeException();
}
return (Connection).cons.get(CACHE_KEY);
}
//생략
}
public void WeakHasTest() {
Connection con = createConnection();
DatabaseManager dbm = new CacheDatabaseManager(con);
con = null; //참조 해제
//weak Hash Map에 GC작동
}
참고자료
https://javabom.tistory.com/14
https://seongwon.dev/Java/20220304-%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C%EC%9E%90%EB%B0%94-7/