내가 만든 자바 코드가 어떤 과정으로 실행되는지 알아보기 위한 "JVM 알아보기" 네 번째 포스팅입니다.

이번 포스팅에서는 JVM 의 GCHeap 에 대해서 알아보겠습니다.

 


 

JVM GC(Garbage Collection)

 

개발자는 직접 자바의 메모리 관리를 하지 않아도 된다.

메모리 관리를 JVM 이 알아서 하기 때문인데, JVM 내에서 메모리 관리를 해주는 것을 가비지 컬렉터 라고 부른다.

가비지 컬렉터는 힙에서 사용 가능한 모든 객체를 추적하고 사용하지 않는 객체에 할당 된 메모리를 해제한다.

이 작업을 가비지 컬렉션, GC 라고 한다.

 

 

 

GC 의 프로세스를 알아보기 이전에, 먼저 GC 와 관련 된 개념들을 정리해보자.

 

 

UnReachable Object

 

유효한 참조가 없는 객체를 UnReachable Object(도달할 수 없는 객체)라고 한다.

그리고 UnReachable 하다고 간주되는 객체는 GC 의 대상이 된다.

그렇다면 유효한 참조를 판단하는 기준은 무엇일까?

 

앞서 JVM 의 GC 는 힙에서 사용 가능한 모든 객체를 추적한다고 했다. GC 는 객체 그래프를 순회하며 모든 객체를 식별한다.

그래프를 순회하려면 시작점이 필요한데 이 시작점을 GC의 root 라고 한다.

이 GC 루트에서 직접 또는 간접적으로 참조되는 모든 객체는 Reachable 한 객체(도달할 수 있는 객체)로 구분되어 GC 되지 않는다.

반대로 GC 루트에서 참조되지 않는 UnReachable 한 객체들은 사용하지 않는다고 간주되어 GC 의 대상이 된다.

 

GC root 는 주로 다음과 같다.

  • Class, 클래스 변수
  • Java Stack, 메서드 실행 시에 사용하는 지역 변수와 파라미터들
  • 메서드 영역의 정적 변수
  • Native Stack, JNI(Java Native Interface) 에 의해 생성된 객체

 

 

GC 의 대상이 되는 객체

 

  • null 로 선언 된 참조 변수
  • 재할당 된 참조 변수
  • 메서드 내부에서 생성된 객체
  • 고립된 섬(Island of Isolation) : 객체 A 와 B 가 서로만을 참조할 때 두 객체 모두 호출할 수 있는 방법이 없어진 경우 또는 참조되지 않는 단일 객체

Island of Isolation

 

Island of Isolation 의 예시를 보자.

객체를 두 개 만들어서 서로만을 참조하게 한 뒤, 두 객체를 모두 null 로 만들고 가비지 컬렉터를 호출해보자.

 

Java

// Main class
public class GFG {
 
    GFG i;
 
    // Method 1
    // Main driver method
    public static void main(String[] args)
    {
 
        // Creating object of class inside main() method
        GFG t1 = new GFG();
        GFG t2 = new GFG();
 
        // Object of t1 gets a copy of t2
        t1.i = t2;
 
        // Object of t2 gets a copy of t1
        t2.i = t1;
 
        // Till now no object eligible
        // for garbage collection
        t1 = null;
 
        // Now two objects are eligible for
        // garbage collection
        t2 = null;
 
        // Calling garbage collector
        System.gc();
    }
 
    // Method 2
    // overriding finalize() Method
    @Override protected void finalize() throws Throwable
    {
        // Print statement
        System.out.println("Finalize method called");
    }
}

 

Output

Finalize method called
Finalize method called

 

객체를 파괴하기 전에 가비지 컬렉터는 finalize 메서드를 호출한다.

t1 만 null 로 만들고 가비지 컬렉터를 호출하면 GC 가 동작하지 않는다. t2 라는 외부 참조가 있기 때문이다.

t1 과 t2 가 모두 null 이 되면 두 객체를 호출할 수 있는 방법이 없으므로 두 객체 모두 GC 의 대상이 된다.

 

 

stop-the-world

 

GC 는 root 에서부터 객체 그래프를 순회하며 객체가 참조되고 있는지 추적한다. 그리고 참조되지 않는 객체를 제거한다.

이 작업은 애플리케이션이 실행되는 중에 병행되어야 하는데, 그러기 위해서는 GC 이외의 모든 쓰레드의 작업을 중단해야한다.

 

GC 를 실행하기 위해 JVM 이 GC 를 실행하는 쓰레드 이외의 모든 쓰레드의 작업을 멈추는 것을 stop-the-world 라고 한다.

작업이 완료될 때까지 모든 작업이 중지되기 때문에 성능 저하의 원인이 된다.

따라서 이 stop-the-world 의 작업 시간을 줄이는 것이 GC 튜닝의 주요 목적이 된다.

 

 

 

JVM GC 의 알고리즘

 

Mark and Sweep

 

기본적으로 GC 는 Mark and Sweep 이라는 두 가지 프로세스로 동작한다.

 

 

Marking
Sweep

 

Marking 단계에서 사용 중인 메모리와 그렇지 않은 메모리를 식별하고

Sweep 단계에서 사용하지 않는다고 식별 된 메모리를 제거한다.

 

compacting

 

새 메모리 할당 시의 성능을 위해 참조되지 않는 객체를 삭제한 뒤에 빈 공간을 압축하는 방법도 있다.

 

 

 

GC 와 Heap

 

 

Weak Generational Hypothesis, 약한 세대 가설 

 

GC 는 Heap 영역 내의 모든 객체들을 대상으로 동작한다. 그리고 실행 중인 애플리케이션은 시간이 지남에 따라 더 많은 객체를 생성한다.

따라서 시간이 지날수록 점점 더 객체가 많아지고 GC 작업의 시간이 증가할 것이다.

이를 해결하기 위해 JVM 은 객체의 세대를 분리했다. 다음 그래프를 보자.

 

객체 수명에 대한 일반적인 분포

X 축은 객체의 수명을, Y 축은 살아있는 객체 수(할당 된 총 바이트 수)를 나타낸다.

그래프의 분포를 보면 왼쪽에 수명이 짧은 객체가 엄청나게 많다. 대부분의 객체는 짧은 시간동안 사용되고 메모리가 해제되는 것이다.

이것을 weak generational hypothesis 라고 한다.

 

이 전제를 통해 GC 의 효율적인 수집이 가능하다. 객체의 세대를 분리하여 각각 GC 를 수행하는 것이다.

 

 

 

 

Young 영역

 

우선, Young 영역부터 알아보자. 이 영역에는 새로 생성한 객체의 대부분이 할당된다.

Young 영역의 GC 프로세스는 다음과 같다.

  • Eden 영역에 객체가 생성된다.
  • Eden 영역이 가득 차면 GC가 발생하여 Mark and Sweep 의 과정을 거쳐서 살아있는 객체만 Survivor 영역 중 하나로 이동되고, 다시 Eden 영역을 채운다.
  • 이 과정을 반복하여 하나의 Survivor 영역이 가득 차게 되면 살아있는 객체만 나머지 비어있는 Survivor 영역으로 이동된다. 이때, Eden 영역에 있는 살아있는 객체들도 비어있던 Survivor 영역으로 간다. 즉, Survivor 영역의 둘 중 하나는 반드시 비어있어야 한다.

 

이와 같이 Young 영역에서 발생하는 GC 를 Minor GC 라고 한다.

Minor GC 는 대부분의 객체가 가비지일 것을 가정하고 최적화되어 동작한다.

일반적으로 더 작은 공간이 할당되고, 많은 객체를 검사하지 않기 때문에 속도가 빠르다.

 

몇 차례의 Minor GC 가 수행되고도 살아남은 객체들은 Old 영역으로 이동하게 된다. 이를 Promotion 이라고 한다.

각 객체는 Minor GC 에서 살아남은 횟수를 기록하는 age bit 를 가지고 있고, Minor GC 가 발생할 때마다 age bit 값이 1씩 증가한다.

age bit 값이 MaxTenuringThreshold 라는 설정값을 초과하게 되는 경우 객체가 이동된다.

 

 

Old 영역

 

Old 영역에서 수행되는 GC 는 Major GC 라고 한다. 모든 라이브 객체를 수집해야 하기 때문에 훨씬 속도가 느리며, 수행 빈도도 적다.

 

Old 영역의 GC 는 GC 방식에 따라 처리 프로세스가 달라진다.

GC 방식에는 여러 유형이 있다.

 

  • Serial GC
  • Parallel GC
  • Parallel Old GC (Parallel Compacting GC)
  • Concurrent Mark and Sweep (CMS)
  • G1(Garbage First) GC
  • 그 외의 여러 유형

 

 

 

마치며

이번 포스팅에서는 JVM 의 GC 와 JVM Heap 의 구조에 대해 알아보았습니다.

다음 포스팅에는 GC 의 유형 별 프로세스와 장단점에 대해 다루도록 하겠습니다.

 

 

 

Reference

 

복사했습니다!