ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java - 가비지 컬렉션(Garbage Collection)
    Language/Java 2021. 11. 15. 16:34

    Garbage Collection

    Garbage Collection의 개념

    Garbage Collection은 메모리 관리 기법 중 하나로, 동적으로 할당했던 메모리 영역 중 필요 없게 된 영역을 해제하는 기능이다. 여기서 동적으로 할당했던 메모리 영역은 프로그램 런타임에서 사용되는 Heap 영역을 의미하며, 필요 없게 된 영역은 어떠한 변수도 가리키지 않게 된 영역을 의미한다.

     

    Person person = new Person();
    person.setName("홍길동");
    person = null;
    
    person = new Person();
    person.setName("이순신");

    person 변수는 기존의 "홍길동" 이름이 붙은 Person 객체가 존재하는 메모리 영역을 가리키고 있었으나, 나중에 "이순신" 이름이 붙은 Person 객체가 존재하는 메모리 영역을 가리키게 된다. 이때, "홍길동" 이름이 붙었던 Person 객체의 메모리 영역은 그 어떤 변수도 가리키고 있지 않는다. 이렇게, 사용하지 않는 메모리 영역을 어느 시점에서 자동으로 해제하는 것이 바로 Garbage Collection의 역할이다.

     

    C나 C++에서는 Heap 영역의 메모리를 사용하기 위해서는 코드 레벨에서 직접 동적 메모리 영역을 할당받고 해제하는 과정을 작성해야 한다. 하지만, 이를 수동으로 직접 관리하는 것은 번거롭고 실수를 하기도 쉽다. 예를 들어, 메모리 영역을 할당받고 해제하지 않으면 메모리 누수가 발생할 수 있고, 이미 해제한 메모리 영역을 또 해제하게 된다면 에러가 발생할 것이다. 반면, Java에서는 동적 메모리 영역을 Garbage Collection이 알아서 관리해주기 때문에 개발자는 편하게 개발에 집중할 수 있다.

     

    Garbage Collection의 장점

     - 메모리 영역을 할당받고 해제하지 않는 것에서 발생한 메모리 누수를 예방할 수 있다.

     - 해제한 메모리 영역을 또 해제하게 된 경우 발생하는 에러를 예방할 수 있다.

     - 해제된 메모리에 접근하게 되는 경우 발생하는 문제점을 예방할 수 있다.

     

    Garbage Collection의 단점

     - Garbage Collection의 메모리 해제 타이밍을 개발자가 정확히 알기 어렵다.

     - 어떠한 메모리 영역이 해제의 대상이 될지 검사하고, 실제로 해제되는 일이 모두 비용이 드는 작업이다.


    Garbage Collection에서 사용하는 알고리즘

    Reference Counting

     

    위 사진의 Root Space는 Heap 영역에 대해 참조하고 있는 영역이다.

    Reference Counting은 Heap 영역의 객체들이 각각 reference count라는 숫자를 가지고 있다고 생각하면 된다. 여기서 reference count는 몇 가지 방법으로 해당 객체에 접근할 수 있는 지를 뜻한다. 만약 reference count가 0이라면 해당 객체에 접근할 수 있는 방법이 없다는 뜻이므로 메모리 해제의 대상이 되는 것이다.

    하지만, 이러한 Reference Counting 방식은 순환 참조 문제가 발생할 수 있다. 그림 속 Root Space에서 모든 Heap 영역의 참조를 끊는다고 가정한다. 이 경우 노란색 원 안의 객체는 서로가 서로를 참조하고 있기 때문에 reference count가 1로 유지된다. 결국 사용하지 않는 메모리 영역이 해제되지 못하고 메모리 누수가 발생하는 것이다.

     

    ※ Reference Count 방식은 Swift 언어에서 사용되는 방식이다.

     

    Mark and Sweep

     

    Mark and Sweep 알고리즘은 Reference Counting의 순환 참조 문제를 해결할 수 있다.

    Mark and Sweep 알고리즘은 Root Space부터 해당 객체에 접근이 가능한지 아닌지를 메모리의 해제 기준으로 삼는다. Root Space부터 그래프 순회를 통해 연결된 객체를 찾아내고(Mark) 연결이 끊어진 객체는 지우는(Sweep) 방식이다. Root Space부터 연결된 객체를 Reachable, 연결되지 않은 객체를 Unreachable이라고 부른다.

    위 사진에서는 Sweep 이후에 분산되어 있던 메모리가 정리된 것을 확인할 수 있는데, 이것은 메모리 파편화를 방지하는 Compaction 과정이다. 이러한 Compaction 과정은 Mark and Sweep 알고리즘에서 필수 사항은 아니다.

    이렇게 Mark and Sweep 방식을 사용하면 Root Space부터 연결이 끊긴 순환 참조되는 객체들도 지울 수 있다.

    하지만, Mark and Sweep 방식에도 단점이 존재한다. 객체의 reference count가 0이 되면 지워버리는 Reference Counting 방식과 달리, Mark and Sweep은 의도적으로 특정 순간에 Garbage Collection을 실행해야 한다. 즉, 어느 순간에는 실행 중인 애플리케이션이 Garbage Collection에게 컴퓨터 리소스를 내주어야 한다는 뜻이다. 따라서, 애플리케이션의 사용성을 유지하면서 효율적으로 Garbage Collection을 실행하는 것이 중요하다.

     

    ※ Mark and Sweep 방식은 Java, JavaScript 언어에서 사용되는 방식이다.


    Garbage Collection의 동작 과정

    Root Space

    Root Space는 Heap 영역에 대해 참조하고 있는 영역이다. JVM Memory 영역 중에서 Root Space에 해당되는 영역은 다음과 같다.

        1. Stack의 로컬 변수

        2. Method Area의 static 변수

        3. Native Method Stack의 JNI 참조

     

    위 사진의 노란색 박스 안의 영역이 Root Space이다.

     

    Garbage Collection의 실행 타이밍

    Mark and Sweep 방식의 첫 번째 특징은 의도적으로 Garbage Collection을 실행한다는 것이다. 그렇다면, Garbage Collection이 실행되는 순간은 언제인지 확인해 보겠다. 이를 알기 위해서는 Heap 영역에 대해서 디테일하게 알아보아야 한다.

     

    JVM Memory의 Heap 영역은 크게 Young Generation과 Old Generation으로 나뉜다. Young Generation에서 발생하는 Garbage Collection을 Minor GC, Old Generation에서 발생하는 Garbage Collection을 Major GC라고 부른다.

    Young Generation은 또다시 Eden, Survival 0, Survival 1 영역으로 나뉜다. Eden은 새롭게 생성된 객체들이 할당되는 영역이고, Survival 영역은 Minor GC에서 살아남은 객체들이 존재하는 영역이다. 이때, Survival 영역에서 Survival 0과 Survival 1 중 하나는 꼭 비어 있어야 한다는 규칙이 있다.

     

    Minor GC의 실행 타이밍은 Eden 영역이 꽉 찼을 때이다.

     

     

    위 사진에서 회색 사각형은 메모리에 할당된 객체를 의미한다. Minor GC가 발생하고 난 뒤 Reachable이라고 판단된 객체는 Survival 0 영역으로 이동된다.

     

     

    이때 살아남은 객체들의 숫자들이 0에서 1로 변한 것을 확인할 수 있다. 이 숫자는 age bit를 의미한다. Minor GC에서 살아남은 객체는 age bit가 1씩 증가한다.

     

     

    또다시 Eden 영역이 꽉 차는 순간이 온다.

     

     

    Eden 영역이 꽉 찬 경우 Minor GC가 발생하여 Reachable이라고 판단된 객체들은 Survival 1 영역으로 이동된다.

     

     

    이후, 또다시 Eden 영역이 꽉 차는 순간이 온다.

     

     

    그러면 Minor GC가 발생하여 Reachable이라고 판단된 객체들은 Survival 0 영역으로 이동한다. Survival 0 영역으로 넘어온 객체들 중 오래 살아남아 age bit가 3이 된 객체가 보인다.

    JVM에서는 일정 수준의 age bit를 넘어가면 오래도록 참조될 객체라고 판단하고, 해당 객체를 Old Generation으로 넘겨주는데, 이를 Promotion이라고 한다.

    Java 8에서는 Parallel GC 방식 기준으로 age bit가 15가 되면 Promotion이 진행된다.

     

     

    이번 예제에서는 age bit가 3이 될 경우를 Promotion의 기준으로 잡았다. Survival 0 영역의 age bit가 3인 객체가 Old Generation으로 이동한 것을 확인할 수 있다.

     

    시간이 지나게 되면 언젠가는 Old Generation도 다 채워지는 날이 올 것이다. 이때, Major GC가 발생하면서 Mark and Sweep 방식을 통해 필요 없는 메모리를 비우는데, Minor GC에 비해 시간이 오래 걸린다. 또한 Garbage Collection이 실행되면 JVM이 잠시 멈추는 Stop The World 현상이 발생한다. 이때 Minor GC보다 Major GC가 Stop The World 현상이 더 길 것이다.

     

    그렇다면 Heap 영역을 굳이 Young Generation과 Old Generation 나눈 이유는 무엇일까?

     

     

    그 이유는 바로 Garbage Collection 설계자들이 애플리케이션을 분석해 보니 대부분의 객체가 수명이 짧다는 것을 깨달았기 때문이다. Garbage Collection도 결국은 비용이 드는 작업인데, 메모리의 전체 부분이 아닌 특정 부분만을 탐색해서 해제해야 효율적이기 때문이다. 따라서, 대다수의 객체가 금방 사라지기 때문에 Young Generation 안에서 최대한 메모리를 해제하도록 설계한 것이다.


    Garbage Collection의 실행 방식

    Mark and Sweep 방식의 두 번째 특징은 애플리케이션과 Garbage Collection의 실행이 병행된다는 것이다. 즉, JVM에서는 애플리케이션과 Garbage Collection을 병행하여 실행할 수 있는 여러 가지 옵션들을 제공한다는 것을 유추해볼 수 있다.

    Garbage Collection이 어떠한 방식으로 애플리케이션의 실행과 병행되는지 살펴보기 전에, Stop The World의 개념을 알아야 한다. Stop The World란 Garbage Collection을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 것을 말한다.

     

    1. Serial GC

     

    Serial GC는 하나의 스레드로 Garbage Collection을 실행하는 방식인데, 하나의 스레드로 Garbage Collection을 실행하다 보니 Stop The World 시간이 긴 것을 확인할 수 있다.

    Serial GC 방식은 싱글 스레드 환경 및 Heap 영역이 매우 작을 때 사용되는 방식이다. 추가로, Mark and Sweep 과정 이후 메모리 파편화를 막는 Compaction 과정도 진행된다.

     

    2. Parallel GC

     

    Parallel GC의 기본적인 처리 과정은 Serial GC와 동일하다. 하지만 Parallel GC는 여러 개의 스레드로 Garbage Collection을 실행하기 때문에 Serial GC보다 Stop The World 시간이 짧다는 것을 확인할 수 있다.

    Parallel GC 방식은 멀티 코어 환경에서 애플리케이션 처리 속도를 향상시키기 위해 사용되며, Java 8에서 기본으로 쓰이는 Garbage Collection 방식이다. Parallel GC가 Garbage Collection의 오버헤드를 상당히 줄여주었지만, 애플리케이션이 멈추는 것을 피할 수 없으므로 다른 알고리즘이 등장하게 되었다.

    일반적으로 Parallel GC는 Minor GC에 대해서만 멀티 스레딩을 수행하고, Major GC에서는 싱글 스레딩으로 작업을 수행한다.

     

    3. Parallel Old GC

    Parallel Old GC는 Paralle GC의 업그레이드 버전이다. Major GC에서도 멀티 스레딩으로 작업을 수행하고 기존 Mark Sweep Compaction의 개선 버전인 Mark Summary Compaction을 사용한다.

    사실상 Java 7 Update 4 버전부터는 Parallel GC를 설정해도 Parallel Old GC가 동작한다. 엄밀히 말하면 Java 8의 디폴트 버전은 Parallel Old GC인 셈이다.

     

    4. CMS GC

     

    CMS GC에서 CMS는 Concurrent-Mark-Sweep의 줄임말로 Stop The World 시간을 최소화하기 위해 고안된 방식이다. 대부분의 메모리 해제 작업을 애플리케이션 스레드와 동시에 수행하여, Stop The World 시간을 최소화하고 있다. 하지만 메모리와 CPU를 많이 사용하고, Mark and Sweep 과정 이후 메모리 파편화를 해결하는 Compaction이 기본적으로 제공되지 않는다는 단점이 있다. 이러한 이유 때문에 시스템이 장기적으로 운영되다가 조각난 메모리들이 많아 Compaction 단계를 수동으로 수행하면 오히려 Stop The World 시간이 길어지는 것을 확인할 수 있다.

    CMS GC 방식은 Java 9 버전부터 deprecated 되었고, Java 14 버전부터는 사용이 중단되었다.

     

        1. Initial Mark : Class Loader에서 가장 가까운 객체 중 살아 있는 객체만 찾는다.

        2. Concurrent Mark : 위에서 살아 있다고 확인한 객체에서 참조되어 있는 객체를 확인한다.

        3. Remark : 위 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.

        4. Concurrent Sweep : 쓰레기를 정리한다.

     

    5. G1 GC

     

    G1 GC에서 G1은 Garbage First의 줄임말로 Heap 영역을 위에서 설명한 방식과는 다르게 사용한다. Heap 영역을 일정한 크기의 Region으로 나누어 어떤 영역은 Young Generation, 어떤 영역은 Old Generation으로 사용한다. 런타임 시 G1 GC가 필요에 따라 영역 별 Region 개수를 튜닝함으로써 Stop The World를 최소화할 수 있다.

    Java 9 이상부터는 G1 GC를 기본 GC 실행 방식으로 사용한다.


    출처

    https://papimon.tistory.com/93

    https://joel-dev.site/94

    https://mangkyu.tistory.com/118

    https://mangkyu.tistory.com/119

    https://huisam.tistory.com/entry/jvmgc

    https://steady-coding.tistory.com/584

     

    728x90

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

    Java - Primitive Type & Reference Type  (0) 2022.02.19
    Java - 인터페이스  (0) 2021.12.08
    Java - 객체지향언어  (0) 2021.12.07
    Java - 자바 가상 머신(Java Virtual Machine)  (0) 2021.11.09
    Java - 컴파일 과정  (0) 2021.10.29

    댓글

Designed by Tistory.