ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JVM (Java Virtual Machine), GC (Garbage Collection)
    CS/DS & OOP 2021. 6. 22. 17:40

    매번 면접 전 개념을 다시 정리하고 공부하는 두 가지입니다.

    많은 설명을 참고하고 공부하고 있지만, 눈으로 직접 볼 수 없기에 여전히 제게는 꽤나 추상적으로 느껴집니다.

     

     


    JVM

    자바 bytecode를 실행할 수 있는 주체

    자바 bytecode는 플랫폼 독립적이며 모든 자바 가상 머신은 자바 가상 머신 규격에 정의된 대로 자바 bytecode를 실행

    모든 자바 프로그램은 CPU나 운영 체제의 종류와 무관하게 동일하게 동작할 것을 보장

     

     

    '플랫폼 독립적이다'라는 특징은 자바를 사용하는 가장 큰 이유입니다.

    물론 자바도 환경 변수 설정은 해야 합니다.

    근데 그 과정이 상당히 간단하고, 이렇게만 하면 어떤 OS든 간에 코드를 따로 건드릴 필요 없이 잘 작동합니다.

    다들 아시겠지만, bytecode는 자바의 코드가 컴파일되면 생성되는 코드입니다.

     

    JVM 두 가지 주요 기능을 가집니다.

    1. Write once, run anywhere (플랫폼 독립적)

    2. 프로그램 메모리 관리 및 최적화

     

    1의 내용에서 필요성과 중요도를 느끼셨다면 2번으로 넘어가겠습니다.

     

     

    JVM Architecture

    JVM이 가진 또 하나의 장점은 프로그래머 대신 메모리를 관리해주는 점입니다.

    물론, 이것이 단점이 될 수도 있습니다만, 편리함에 중점을 뒀습니다.

     

    일반적으로 C 또는 C++ 에선 사용자가 메모리를 할당하고, free로 해제해주는 모습을 볼 수 있죠.

    이를 동적 할당한다 이야기합니다.

    자바는 대부분의 동적 할당된 메모리 해제는 GC가 알아서 처리해줍니다.

     

    우선, 메모리 구조에 대해 간략히 보겠습니다.

    그림 1. JVM Memory Architecture

    • ClassLoader Runtime data area로 클래스 파일 로드에 사용되는 하위 시스템. 로드, 연결, 초기화의 주요 기능을 수행.
    • Execution Engine ClassLoader에 의해 메모리에 적재된 bytecode를 기계어로 변경해 명령어 단위로 실행.
    • Native Method Interface 프로그래밍 프레임워크. JVM에서 실행 중인 Java 코드를 라이브러리나, Native app을 통해 호출.
    • Native Method Libraries Execution Engine에서 필요한 Native library 컬렉션.

     

    • Runtime Data Area
      • Heap모든 객체, 인스턴스 관련 변수 및 배열 저장 (동적 할당, new 생성자). GC 발생 영역.
      • Method Area
        메타데이터, 상수 풀 및 메서드 코드, 정적 변수 저장.
      • JVM (Language) Stacks 지역 변수, 파라미터, 리턴 값 등이 생성되는 영역
      • Native Method Stacks 자바 이외의 언어로 작성된 코드를 위한 영역
      • PC Registers Thread 생성 시 생성. 현재 Thread가 실행되는 부분의 주소, 명령 저장

     


    아래 포스팅에서도 언급했지만, 프로세스 구조와 JVM의 구조는 닮은 부분이 있습니다.

    작성한 코드가 담기는 방식을 아래 포스팅에 적어뒀습니다.

    각 영역에 대한 역할이 글로만 이해하기 어려우신 분들은 참고해주세요.

    깊게 연관 지으시면 오히려 헷갈릴 수 있습니다. 가볍게 보시면 좋을 것 같습니다.

    2021.05.27 - [Other CS/OS] - Process Memory Area

     

    Process Memory Area

    2021.01.17 - [Other CS/OS] - Process & Thread (OS) Process & Thread (OS) 기본기의 중요성은 항상 느끼고 있는데요. 막상 제대로 알고 있는가 생각했을 때 그렇진 않은 것 같아 하나씩 정리합니다. Process 운..

    exponential-e.tistory.com




    어쨌든 위와 같이 구성되어 있고, 이 중 Heap, Method는 모든 스레드에서 공유되는 영역입니다.

    Runtime Data Area의 나머지 세 구역은 각자 독립된 스택을 할당합니다.

    여기서 Heap Area동적 할당한 객체 등이 존재하며, 참조 여부를 확인 후 GC에서 자동으로 메모리 해제를 해줍니다.

    GC가 어떻게 메모리를 관리해주는지 그리고 종류와 구조를 알아보겠습니다.

     

     

     

    GC

    GC는 Garbage Collection을 의미합니다.

    말 그대로 쓰레기를 수집하는, 쓰레기는 치우는 그러한 역할을 합니다.

     

    프로그램을 가동하고 있는 중에 더 이상 사용하지 않는 객체는 의미 없이 메모리를 차지하겠죠?

    사용 중인 것은 아니지만, 메모리를 동적으로 할당받았던 객체를 쓰레기라고 생각하시면 됩니다.

    이와 같은 쓰레기를 GC로 처리하며 메모리를 관리 또는 최적화합니다.

     

    말씀드린 바와 같이 GC는 대부분 Heap 영역을 대상으로 발생합니다.

    GC는 두 가지 가정을 통해 운영합니다.

     

    • 대부분의 객체는 단 기간에 접근 불가능 상태(unreachable)가 된다.
    • 오래된 객체에서 젊은 객체로 참조는 아주 적게 발생한다.

     

    이 가정을 통해 Heap 영역에서 다양한 GC가 발생합니다.

     

     

    그림 2. Heap & Permanent Generation

    Heap은 Young, Old, Permanent(Java 8 이전)으로 나뉘고, Young은 다시 eden과 survivor 2개의 영역으로 나뉩니다.

    또한, GC는 Young Generation에서 발생하는 minor GC, Old Generation에서 발생하는 major GC가 있습니다.

     

    Young Generation에서 minor GC를 발생시켜 메모리를 해제하는 방법은 다음과 같습니다.

    1. 프로그램 실행 중 새로 생성된 객체들은 Eden 영역에 존재
    2. Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나(A)로 이동
    3. Eden 영역에서 GC가 발생하면 위의 Survivor 영역(A)에 객체가 계속 쌓임
    4. 쌓이던 Survivor 영역(A)이 가득 차면, 여전히 생존한(참조되고 있는) 객체를 나머지 Survivor 영역(B)으로 옮김
    5. Survivor(A)는 비움 (참조되지 않아 남아있는 객체 메모리 해제)
    6. 2 ~ 5를 계속 반복 후 일정 시간 동안 남은 객체는 Old Generation으로 이동

    Survivor 영역 두 개의 명칭은 따로 구분되는 것은 아닙니다.

    가독성을 조금 높이기 위해 A, B라고 제가 임의로 붙였습니다.

    또한, 위의 과정에서 Survivor 영역 한 곳은 무조건 비어있어야 합니다.

    양쪽 모두 객체가 들어있거나, 양쪽 모두 사용하지 않고 있다면 시스템에 문제가 발생한 것으로 여기고 확인해보시기 바랍니다.

     

    추가로 Survivor 영역 두 개를 운영하는 이유는 메모리 단편화를 방지하기 위함입니다.

    Aging을 통해 threshold 값을 넘거나 Survivor 영역의 메모리가 부족해지는 경우 Old영역으로 이동시킵니다.

     

     

    Old Generation은 영역별로 구분할 내용은 따로 없기에 다양한 GC에 대한 동작을 소개해볼까 합니다.

    일반적으로 'Old'에서 발생합니다.

     

    각 GC의 특징을 그림으로 표현하면 아래와 같습니다.

    괄호는 JVM option과 이명에 대한 소개입니다.

    그림 3. Types of Java GC

    Serial GC (-XX:+UseSerialGC)

    그림과 같이 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식입니다.

    Young 영역은 위에 설명한 내용과 동일한 방식으로 GC를 진행합니다.

    Old 영역은 Mark - Sweep - Compact이라는 알고리즘을 사용합니다.

    알고리즘 동작 방식

    1. Old 영역에 살아 있는 객체를 식별 (Mark)
    2. Heap의 앞부분부터 살아있는 것만 남김 (Sweep)
    3. 객체가 비워진 공간을 뒤에 남겨져 있는 객체를 댕겨 모두 채움 (Compaction)

     

    Parallel GC (-XX:+UseParallelGC, Thoughput GC)

    GC는 메모리가 충분하고 코어의 개수가 많을 때 유리합니다. 앞의 Serial GC는 GC를 처리하는 스레드가 하나였죠.

    Parallel GC는 그림과 같이 GC를 처리하는 스레드를 여러개 둔 방식입니다.

    동작 방식은 Serial GC와 동일하지만, 다수의 쓰레드를 사용하기 때문에 속도가 훨씬 빠릅니다.

     

    Parallel Old GC(-XX:+UseParallelOldGC)

    해당 GC는 Parallel GC와 동일하나 Old 영역의 GC 알고리즘이 다릅니다.

    이 방식은 Mark - Summary - Compaction 알고리즘을 사용합니다.

    알고리즘 동작 방식

    1. Old 영역에 살아 있는 객체를 식별 (Mark)
    2. GC를 수행한 영역에 대해 별도로 살아 있는 객체를 식별 (Summary)
    3. 객체가 비워진 공간을 뒤에 남겨져 있는 객체를 댕겨 모두 채움 (Compaction)

     

    CMS GC (-XX:+UseConcMarkSweepGC, Low Latency GC)

    CMS는 그림만 봐도 앞선 방식들보다 상당히 복잡해 보이는 것을 알 수 있습니다.

    단계별로 살펴보겠습니다.

    1. Mark 단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾아냅니다. (단시간)
    2. 앞 단계에서 살아남아 있는 것으로 확인된 객체에서 참조하는 객체를 따라가며 확인합니다.
    3. 즉, 다른 스레드가 실행 중인 상태에 동시에 진행됩니다.
    4. Remark 단계에서는 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인합니다.
    5. 참고로 Concurrent Mark는 Mark와 Remark 사이에 진행되는 2번 단계를 의미합니다.
    6. 마지막 Sweep 단계에서는 쓰레기를 정리합니다. 이 작업도 다른 쓰레드가 실행되고 있는 상황에서 진행합니다.

     

    CMS GC는 GC를 진행하기 전 JVM에 의해 나머지 스레드를 멈추는 동작(STW)의 시간이 매우 짧기에 모든 Application의 응답 속도가 중요할 때 유용하게 사용됩니다.

    하지만, 다른 방식보다 메모리와 CPU의 소모가 크며, Compaction 단계가 제공되지 않습니다.

    따라서, 조각난 메모리를 정리하기 위한 Compaction 작업이 얼마나 자주 오래 수행되어야 하는지 확인하고 사용 여부를 결정하시는 게 좋겠습니다.

     

     

    G1 GC

    G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행합니다.

    이후 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행합니다. 

    G1 GC의 가장 큰 장점은 성능인데 위에 소개한 GC 중 가장 빠릅니다.

    특히, CMS는 하나를 통으로 쓰고 지우지만, G1은 분할해서 각 영역에 대한 GC를 진행하기에 훨씬 성능이 좋습니다.

    jdk 6 ~ 8의 버전에선 불안정하다고 하니, 9 이상에서 쓰시는 것을 권장드립니다.

     

     

    추가로 Permanent 영역에 대한 이야기를 간략히 하고 마무리하겠습니다.

     

    String pool은 버전 7 이전까지 Perm에 존재했으나 이후엔 String pool은 Heap 영역에 포함되었습니다.

    이 외에도 class, method meta data, static object, variable, constant pool 등을 관리했는데 Java 8부터 Permanent 영역이 Metaspace로 변경되었고, Metaspace가 그 역할을 넘겨 받았다고 합니다.

    위치는 Native Memory 영역으로 변경되며 JVM이 아닌 OS에 의해 관리되고 있습니다.

     

     

     

     

    Permanent 영역은 앞선 JVM 구조 이야기에서 언급하는 게 좀 더 자연스러울 것 같았는데, 그러면 Heap 영역에 대한 상세 이야기를 안 꺼낼 수 없을 것 같아 이렇게 구성했습니다.

    GC 내용은 좀 더 세부적으로 파헤쳐 보면 좋을 것 같은데, 기본기를 추구하고 있는 현재로썬 아직은 시기상조인 것 같네요.

    물론 공부는 하고 싶지만, 좀 많이 뒤로 밀어야 할 것 같습니다. 하하..

     

     

    JVM 구조나 각 영역의 역할 그리고 메모리 관리까지, 눈에는 보이지 않지만 상당히 중요한 기본 내용이라 생각합니다.

    혼자만의 공부를 위해 쓰는 내용들이지만 관련 내용에 대해 계속 찾아보고 수정해야 할 것 같네요.

    내용 관련해서 첨언해주시면 큰 도움이 될 것 같습니다. 물론 질문도 해주시면 같이 의견 나눠보면 좋을 듯합니다. 감사합니다.

     

     

     

     

     

     

     

     

     

     

    참고자료

    Brunch - G1 GC 적용과 JVM Upgrade

    GeeksforGeeks - Metaspace in Java 8 with Examples

    Naver D2 - Java Garbage Collection

    Wikipedia - JVM

    GeeksforGeeks - How JVM works? JVM Architecture

    InfoWorld - What is the JVM? Introducing the Java Virtual Machine

    Java Point - JVM

    반응형

    'CS > DS & OOP' 카테고리의 다른 글

    Map 그리고 merge()  (1) 2022.01.29
    Exception (try-with-resources)  (0) 2021.01.12
    Exception (checked, unchecked)  (0) 2021.01.10
    Java 자료형과 String pool  (0) 2020.07.01
    객체지향 5원칙 (SOLID)  (0) 2020.06.24

    댓글

Designed by minchoba.