티스토리 뷰

난생처음 보는 메소드들이다.

현재 finalizer는 Java 9 버전 이상에서 deprecated 됐다.

어차피 쓰지도 않을 녀석들이니 어떠한 사유로 deprecated 됐는지 이유를 가볍게 알아보고 넘어가면 좋을 것 같다.

 

finalizer는 gc가 발생하기 이전 실행 돼야 할 전처리를 담당하는 메소드이다.

이는 C++의 소멸자(destructor)와 다르다.

C++은 클래스 내부에서 heap 영역에 메모리를 동적 할당하고 할당된 메모리 해제와 관련한 로직을 직접 작성해야 한다.

#include <iostream>

using namespace std;

class testClass {

public: testClass() {
		cout << "생성 ㅋㅋ\n";
	}

	~testClass() {
		// 동적할당 해제 로직
		cout << "소멸 ㅋㅋ\n";
	}
};

int main()
{
	testClass hi;
}

C++에서는 소멸자가 해당 클래스가 선언된 스코프에서 벗어나면 실행된다.

물론 소멸자를 개똥같이 구현해서 메모리 누수가 발생할 수 있다.

이는 메모리 관리를 프로그래머에게 맡기지 않는 gc의 등장배경이다.

 

 

finalizer는 어떻게 동작하는가? gc가 발생하기 이전 finalizer를 호출해 gc의 전처리 메소드를 실행하게 된다.

그러나 이 메소드가 실행되는 시점은 전혀 예측할 수 없다.

가비지 콜렉터가 메모리를 청소하는 시점은 gc 알고리즘에 따라 달려있다. 메모리가 꽉 차기 직전이라던지..

이게 어떤 문제가 발생할 수 있는지 코드를 통해 알 수 있다.

public class MyFileReader {
	private BufferedReader reader;

	public MyFileReader() throws FileNotFoundException {
		InputStream input = new FileInputStream("C:\\devlist.txt");
		this.reader = new BufferedReader(new InputStreamReader(input));
	}
	public String readFirstLine() throws IOException {
		String firstLine = reader.readLine();
		return firstLine;
	}
	@Override
	protected void finalize() throws Throwable {
		try {
			 System.out.println("Calling the finalize method...");
	        reader.close();
	       
	    } catch (IOException e) {
	       e.printStackTrace();
	    }
	}
	public static void main(String[] args) throws Throwable {
		MyFileReader reader = new MyFileReader();
		reader.readFirstLine();
		reader = null;
		System.out.println("Calling System.gc()");
		System.gc();
	}
}

MyFileReader 클래스에서 BufferedReader의 인스턴스를 생성했다.

이 친구는 자원을 반납하기 위해서 필히 close 메소드를 호출해 줘야 한다.

그러나 BufferedReader의 인스턴스 해제 로직이 finalize 메소드 안에 있어 호출될 시점을 알 수가 없다.

시스템이 열 수 있는 파일의 개수에는 한계가 있어 사용을 마치고 나선 close를 통해 자원을 반납해야 한다.

finalizer가 실행을 게을리하여 파일을 계속 열어두고 있는다면 새로운 파일을 열지 못해 프로그램이 실패할 수 있다.

 

 

그렇다면 이를 명시적으로 수행시키기 위해 위의 코드에서 처럼 System.gc()를 호출해 gc를 실행시켜줄 수도 있다.

그러나 이에도 명확한 한계가 존재한다.

 

  1. 비용이 많이 든다
    - 모든 스레드를 멈추고, 메모리 파편화를 모아주고 등등.. 상당히 코스트가 많이 드는 로직이 실행된다.
  2. 가비지 수집을 즉시 실행하지 않는다.System.gcJVM이 GC를 시작하라는 힌트일 뿐이니다.
    System.gc()를 실행한다고 finalizer가 실행된다는 보장이 없다.
  3. JVM은 GC를 호출해야 할 때를 더 잘 안다.
    똑똑한 사람들이 만든 JVM이다. 나 같은 무지렁이가 함부로 실행하다간 시스템의 성능저하를 만들 수 있다.

 

 

 

cleaner는 finalizer와 다르게 별도의 스레드에서 실행이 되어 이를 제어하기 쉽지만 이 또한 gc의 관리 아래에 실행되기 때문에 언제 실행될지 알 수 없다.

게다가 실행 시점을 알 수 없는 것은 고사하고 실행이 아예 안될 수도 있다. FileSystem에 접근하는 로직처럼 자원을 얻고 회수하지 않으면 시스템이 멈춰버리는 문제가 발생할 수 있다.

 

 

finalizer에서 발생한 Exception은 처리할 작업이 남았어도 그 즉시 종료된다고 한다. 경고 조차 하지 않고 자원 또한 회수하지 못한 채 종료되어 큰 문제를 만들어 낼 수 있다.

 

 

또 다른 문제. 정말 문제가 많은 친구다. 더럽게 느리다.

finalizer는 gc의 성능을 저하해 시스템 성능을 느려지게 만든다.

 

 

보안상의 문제가 존재할 수 있다.

클래스의 직렬화 과정에서 Exception이 발생하면 이 클래스의 하위 클래스에서 finalizer가 호출될 수 있다.

이를 이용해 gc가 이를 회수해가지 못하게 하는 등 악의적인 로직을 넣어 보안문제로 이어질 수 있다.

이를 막으려면 클래스를 final로 선언해 상속을 막거나 finalize 메소드를 final로 선언 해 Overriding을 막으면 된다.

 

 

 

 

그럼 자원 반납은 어떻게 해야 하는가?

AutoCloseable interface를 구현해 클라이언트 측에서 close 메소드를 호출하기 바라면 된다.

public class Main {
	static class test implements AutoCloseable{
		private BufferedReader reader;

		public test() throws FileNotFoundException {
			InputStream input = new FileInputStream("C:\\devlist.txt");
			this.reader = new BufferedReader(new InputStreamReader(input));
		}
		@Override
		public void close() throws Exception {
			reader.close();
		}
	}
	
	public static void main(String[] args) throws Exception {
		test t = new test();
		t.close();
	}
}

이때 class 내부 로직에서 Exception이 발생하면 자원이 회수되지 않고 프로그램이 종료될 수 있다.

이를 막기 위해 try-finally문을 통해  close 메소드를 호출하게 구현해야 한다.

더 좋은 방법이 있다고 하는데 이는 아이템 9에서 확인하도록 하자.

 

 

 

 

단점만 나열하다 보니 이런 쓰레기를 어디에 쓰지라는 생각이 머릿속을 맴돈다.

첫 번째 사용처는 클라이언트가 close 메소드를 호출하지 않았을 경우에 대비하여 구현해 두는 것이다.

사람이다 보니 까먹을 수 있다. 클라이언트가 자원을 회수하지 않았을 때 늦게라도 회수가 되게 구현하는 것이다.

물론 이 또한 주의해서 사용해야 한다.

 

두 번째로는 native 메소드를 사용하는 클래스에서 자원을 회수하는 용도로 쓰인다.

native 메소드는 자바 코드 밖에서 실행되기 때문에 gc는 여기서 생성된 자원의 존재를 알 수 없다.

앞서 설명한 것처럼 finalizer는 성능저하가 우려되고, 회수되는 시점을 예상할 수 없다.

이를 감당할 수 있는 심각하지 않은 자원을 가진 경우에 사용될 수 있다.

그렇지 않다면 close 메소드를 호출해 자원을 회수해야 한다.

 

 



 

다음은 cleaner의 사용법이다.

// 코드 8-1 cleaner를 안전망으로 활용하는 AutoCloseable 클래스 (44쪽)
public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
    private static class State implements Runnable {
        int numJunkPiles; // Number of junk piles in this room

        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }

        // close 메서드나 cleaner가 호출한다.
        @Override public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }

    // 방의 상태. cleanable과 공유한다.
    private final State state;

    // cleanable 객체. 수거 대상이 되면 방을 청소한다.
    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles) {
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override public void close() {
        cleanable.clean();
    }
}

Cleaner는 register 메소드를 통하여 어떤 객체의 자원을 수거할지를 받아 Cleanalbe 객체를 반환한다.

이때 State 클래스처럼 Runnable을 인자로 받아 새로운 스레드를 생성함이 보장된다.

 

close 메소드가 호출되는 시점은 클라이언트가 close()를 호출했을 때와

까먹고 호출 안 했지만 Room 객체의 인스턴스가 회수되는 시점에 호출될 수 있다.

그러나 호출될 것이라는 보장은 전혀 없다.

 

 

 

 

 

 

궁극적인 해결방법은 클라이언트가 close를 잘 호출하는 것이다.

그러나 이는 까먹기도 쉽고 코드 양이 많아지면 실수할 여지가 증가한다.

이때 try-with-resources로 클래스를 감싸면 이를 고민할 일이 없다.

// cleaner 안전망을 갖춘 자원을 제대로 활용하는 클라이언트 (45쪽)
public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("안녕~");
        }
    }
}

 

 

try의 () 안에는 AutoCloseable의 구현체들만이 들어갈 수 있다.

try문이 실행이 된 이후에 close 메소드를 자동으로 호출해 줘 자원을 회수할 수 있다.

이 구문에 대한 설명은 아이템 9에서 자세히 다룬다.

 

 

 

 

결론 : cleaner나 finalizer의 사용처는 성능저하에 덜 민감하고 별로 쓸모없는 native 자원을 회수할 때만 쓰자

이때 말고 쓰면 죽음뿐이다.

 

 

 

finalize -

https://medium.com/javarevisited/time-to-say-goodbye-to-the-finalize-method-in-java-a2f5b7e4f1b1

 

Time to say goodbye to the finalize method in Java

Now the finalize method is deprecated from Java. Over this article let's explore why java deprecated the finalize method, what are the…

medium.com

코드 - 

https://github.com/WegraLee/effective-java-3e-source-code/tree/master/src/effectivejava/chapter2/item8

 

GitHub - WegraLee/effective-java-3e-source-code: 『이펙티브 자바, 3판』(인사이트, 2018)

『이펙티브 자바, 3판』(인사이트, 2018). Contribute to WegraLee/effective-java-3e-source-code development by creating an account on GitHub.

github.com

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/09   »
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
글 보관함