TIL/Java

[Java] Effective Java 3/E item 7~9

JoJobum 2023. 6. 13.

item 7: 다 쓴 객체는 참조 해제하라

public class Stack {
	private Object[] stack;
	private int size = 0;
	public Stack() {
		stack = new Object[1000];
	}
	public void push(Object e){
		/// 구현 내용
	}
	public Object pop(){
		if(size == 0){
			throw new EmptyStackException();
		}
		return stack[--size];
	}

	// 기타 구현 내용
}

예를 들어 위와 같이 stack을 구현하였을 때, 내용에서 메모리 누수가 발생하는 곳은

pop() 메소드를 실행시켰을 때, 논리적인 개념으로는 stack의 가장 윗 부분의 객체를 꺼내서 사용했다지만 실제로는 Object[] stack에서 참조하고 있으니 GC가 회수해가지 않는다.

이를 해결하는 방법은 해당 참조를 사용완료하였을 때,

null 처리(참조 해제)를 하면 된다.

이를 통해 얻는 이점은

  • 다 쓴 객체에 대한 메모리 회수
  • null 처리한 참조를 실수로 사용하려 하면 NullPointException을 통해 에러를 조기 발견할 수 있음

다만 위의 방법을 사용하는 것은 예외적인 경우여야 함.

이상적인 방법은 참조를 담은 변수를 유효 범위(Scope) 밖으로 밀어내는 것

item 57: 지역변수의 범위를 최소화하라

메모리 누수의 주범들, 유의해야하는 것들

  • Stack같이 자기 메모리를 직접 관리하는 클래스들
  • 캐시
  • listener 또는 callback

 

item 8: finalizer와 cleaner 사용을 피해라

finalizer, cleaner == 객체 소멸자

하지만 C++의 destructor 와 다른 개념

C++의 destructor는 비메모리 자원 회수하는 용도

java에서는 비메모리 자원 회수를 try-finally와 try-with-resources로 해결

item 9: try-finally보다는 try-with-resources를 사용하라

finalizer 는 예측 불가, 오동작의 위험, 낮은 성능, 이식성 문제로 인해 사용하지 않는 것이 좋음

cleaner 는 finalizer에 비하면 낫지만 예측 불가, 낮은 성능을 보이기에 불필요하다.

위와 같은 문제가 발생하는 원인은

finalizer와 cleaner는 즉시 실행X

언제 실행될지 모름

언제 수행될지는 GC(가비지 콜렉터) 알고리즘에 달림

수행 시점 뿐만이 아니라 수행 여부 조차 보장 못함

고로 프로그램 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서 finalizer와 cleaner에 의존하면 안된다.

Ex) 데이터베이스 같은 공유 장원의 영구 락 해제가 finalizer와 cleaner에 의존하면 시스템이 서서히 멈출 것

추가적인 문제점

  • (finalizer) 동작 중 발생한 예외 무시 ⇒ 객체 훼손 가능성 존재
  • (finalizer, cleaner) 성능 문제, AutoCloseable 에 비해 50배 정도 느림
  • (finalizer) 보안 문제 존재

finalizer와 cleaner의 쓰임새

  1. 자원의 소유자가 close 메소드를 호출하지 않는 것에 대한 대비(아예 회수하지 않는 것보단 언젠가라도 회수하는 것이 낫기에)
  2. 네이티브 피어와 연결된 객체를 회수할 때 (네이티브 피어는 자바 객체가 아니기에 GC가 회수 못함 ⇒ finalizer와 cleaner 사용)

finalizer 와 cleaner의 대안은?

AutoCloseable을 구현하고 인스턴스를 다 썼을 때 close 메소드를 호출

 

item 9: try-finally보다는 try-with-resources를 사용하라

자원을 회수하는 수단으로 close로 닫아주어야 하는 자원들이 많은데, 이러한 작업은 놓치기 쉽기에 안전망으로 finalize를 활용하지만 앞서 말했듯이 finalize는 이러한 작업을 믿고 맡길 수 있는 녀석이 아니다.

그렇기에 우리는 자원을 회수하기 위해 try-finally를 사용해왔다.

try-finally 방식에도 단점은 존재하는데,

자원이 2개 이상인 경우 try문을 중첩해서 사용해야 하기에 우선 코드가 지저분해진다.

try-finally

static void copy(String src, String dst) throws IOException {
	InputStream in = new FileInputStream(src);
	try {
		OutputStream out = new FileOutputStream(dst);
		try {
			byte[] buf = new byte[BUFFER_SIZE];
			int n;
			while ((n = in.read(buf)) >= 0)
				out.write(buf, 0, n);
		} finally {
			out.close();
		}
	} finally {
		in.close();
	}	
}

위의 예시처럼 중첩된 try-finally 문에서 발생할 수 있는 문제가 있다.

예외는 try 블록과 finally 블록 모두 발생할 수 있는데, 기기에 물리적인 문제가 생기는 등의 일로 read() 메소드에서 예외가 발생한다면

두번째 예외가 첫번째 예외를 잡아먹고, stackTrace에 첫번째 예외에 대한 정보가 남지 않아 디버깅을 어렵게 만든다.

 

try-with-resources

static void copy(String src, String dst) throws IOException {
	try (InputStream in = new FileInputStream(src);
				OutputStream out = new FileOutputStream(dst)) {
			byte[] buf = new byte[BUFFER_SIZE];
			int n;
			while ((n = in.read(buf)) >= 0)
				out.write(buf, 0, n);
	}
}

try-with-resources 를 사용하게되면 위와 같은 문제로부터 자유로워진다.

숨겨진 예외들도 버려지지 않고 stackTrace 내역에 출력된다.

반응형

'TIL > Java' 카테고리의 다른 글

[Java] Effective Java 3/E 4장 item 15~20  (0) 2024.01.18
[Java] Effective Java 3/E 3장 item 10~14  (1) 2023.11.14
[JAVA] Java Version 8 vs 17  (0) 2023.10.21
[Java] Effective Java 3/E item 4~6  (0) 2023.05.28
[Java] Effective Java 3/E item 1~3  (0) 2023.05.26

댓글