본문

java.lang.ref #3. Object Lifecycle

Garbage Collection에 대해 알아보는데에는 객체의 상태들에 대해 알아보는것 또한 좋다. 따라서 Java Platform Performance: Strategies and Tactics의 "The Truth About Garbage Collection"에서의 내용을 발췌하여 한글로 옮겨보았다. 객체의 상태는 다음과 같이 총 7가지로 구분될 수 있다. (Created, In use, Invisible, Unreachable, Collected, Finalized, Deallocated)


1. Created

객체가 Created 될때에는 여러가지 작업이 수행된다. "객체를 위한 메모리 공간이 할당되고, 객체의 생성자가 호출된다. 상위클래스(superclass) 생성자가 호출되고, 인스턴스 초기화와 인스턴스 변수 초기화 작업이 이루어진다. 생성자의 나머지 부분이 수행된다." 객체가 Created 되고 변수에 할당된다면, In Use 상태로 천이한다.


2. In Use

하나이상의 강한 참조(strong reference)를 가지는 객체는 In Use 상태를 갖는다. JDK 1.1.x상에서, 참조의 종류에는 강한 참조밖에 없었다. 하지만 Java 2부터 weak, soft, phantom 참조가 도입되었다. 아래 그림상의 코드는 객체를 생성하고, 이를 변수들에 할당한다. 그리고 그림상의 도식은 makeCat 메소드가 완료되어 빠져나오기 바로 직전의 VM의 상태를  보여준다. 이 때, Cat 객체에 두개의 강한 참조가 존재한다. (Vector와 makeCat 자체에서)


[그림 1] CatTest의 코드와 Cat 객체의 참조상황


3. Invisible

프로그램 상에서 더이상 객체에 대한 강한 참조가 존재하지 않아 접근할 수 없을 때(하지만 다른 참조는 존재할 수 있다)를 가리켜 invisible 상태라고 한다. 모든 객체가 이 상태를 거쳐가지는 않으며, 이 상태는 개발자에게 혼동을 일으킬 수도 있다. 아래의 코드에서는, try 블록에서 벗어나는 경우 foo 객체에 더이상 접근할 수 없으며, foo는 임시로 사용하는 참조변수로서 스택에서 꺼내지고 따라서 해당하는 객체는 도달하지 못하게 된다(unreachable). 하지만 똑똑한 JVM 구현체의 경우 블록에서 벗어나자마자 참조 카운트를 0으로 만들지 않고, 적어도 run 메소드가 끝나기 전까지는 해당 객체에 대해 강한 참조를 유지할 수 있다. 상대적으로 짧은 시간동안이겠지만, 이 경우가 Invisible한 경우이며, 이는 GC의 대상이 아니기 때문에, 메모리 누수의 원인이 될 수 있다. 따라서 만약 이러한 경우를 접하게 될 경우 참조에 null을 대입함으로서 GC를 수행하도록 해야 한다. (특히 아래와 같이 해당 메소드의 반환이 오래 걸릴 경우를 위해)


public void run() {

    try {

        Object foo = new Object();

        foo.doSomething();

    } catch (Exception e) {

        // whatever

    }

    while (true) { // do stuff } // loop forever

}


4. Unreachable

객체에 더이상 강한 참조가 존재하지 않는 경우를 Unreachable하다고 한다. 객체가 Unreachable한 경우, 이것은 GC의 후보로 지명된다. 여기서 주목할 것은 후보로 된다고 해서 그 즉시 GC된다는 것을 의미하지는 않는다는 것이다. JVM은 해당 객체가 점유하는 메모리가 꼭 필요하지 않는 한, GC를 자유롭게 늦출 수 있다. 또한 알아둘것은, 해당 객체에 대한 강한 참조의 존재여부만이, 객체를 메모리 상에 유지하도록 하는것은 아니라는 점이다. GC root로부터 이어져오는 참조의 연쇄 또한 이에 영향을 미친다. 


GC root에서는 변수들이 관리되며, 여기에는 스택이나 스레드상에서의 임시 변수들, 그리고 사용되는 클래스 상에서의 정적 변수, 그리고 JNI상에서의 특수한 참조들을 가진다. 그리고 강한 참조가 연쇄되어 있는 경우에는 메모리 누수를 일으키지 않는다. 아래의 코드에서는 두개의 객체가 생성되고, 각각의 내부에 서로에 대한 참조가 할당된다. 아래 그림에서의 첫번째 도식은 buildDog()메소드가 반환되기 전의 참조 그래프를 나타낸다. 그리고 두번째 도식은 buildDog()메소드가 반환된 이후의 그래프이다. 첫번째에서는 buildDog()메소드 상에서의 임시 스택 변수가 존재하여 각각에 대한 강한 참조를 유지할 수 있도록 하는 반면, 두번째에서는 root로부터 분리되어 도달할 수 없게되며, 따라서 GC의 후보가 된다. 


[그림 2] 서로를 참조하는 경우에서 Unreachable하게 되는 상황


5. Collected

GC가 객체를 Unreachable하다고 판명하고, 메모리 반환을 위한 최종 준비를 하는 단계를 Collected 라고 한다. 만약 객체가 finalize 메소드를 가지고 있다면, 객체는 메모리 반환하기 전에 이를 수행해야 한다. 이는 finalizer의 수행으로 메모리의 반환이 지연될 수 있다는것을 의미한다.


6. Finalized

객체가 finalize메소드를 실행한 이후에도 (resurrection하지 않고)계속 Unreachable하다면 이는 Finalized 상태이다. Finalized된 객체는 메모리 반환을 기다린다. VM 구현에서 언제 finalizer가 실행될것인가에 대해 제어할 수 있다. 확실한 것은, finalizer를 추가하는것은 객체의 생명을 연장해줄 수 있다는 것이다. 따라서 단시간 동안만 사용될 객체에 finalizer를 추가하는것은 좋지 않다. 또한 finalizer의 처리 시간은 불분명하다는 점 등에 미루어볼때, finalizer에서 정리과정을 처리하는것보다는 직접 정리 과정을 구현하여 실행하는 것이 항상 권장된다. 특히 finalizer 스레드는 다른 스레드와의 우선순위 경쟁에서 밀려날 수 있으므로 조심해야 한다. 또한 finalize 메소드는 객체의 크기 마저 증가시킬 수 있다!

- Resurrection : finalizer 메소드에서 새로운 강한 참조를 생성하는것이 가능하며, 이를 통해 객체는 In use 상태로 전이될 수 있으며 이를 Resurrection이라 한다. 하지만 스펙에 따르면 finalizer는 객체당 단 한번만 실행되어야 한다. finalizer가 두번이상 실행될 수 없다는 점에서, Resurrection은 심각한 문제를 초래할 수 있다.


7. Deallocated

Dealoocated는 GC의 최종 단계이다. 만약 객체가 위의 모든 작업에도 불구하고 계속 Unreachable하다면, 이것은 메모리 반환의 후보가 된다. 이때도 마찬가지로 메모리 반환이 언제 이루어질것인가는 JVM 가 스스로 판단한다.

댓글

Holic Spirit :: Tistory Edition

design by tokiidesu. powerd by kakao.