하루에 한 문제

Java Garbage Collection 본문

Dev/Java

Java Garbage Collection

dkwjdi 2020. 12. 15. 01:55

GC란?

-자바에서는 메모리를 GC라는 알고리즘을 통해 관리합니다. 그렇기 때문에 개발자가 메모리를 처리하기 위해 로직을 만들필요가 없고, 또한 만들어서는 안됩니다.

 

-하나의 객체는 메모리를 점유하는데, 객체의 수행이 완료되어 더이상 해당 객체가 필요하지 않게되면 쓰레기가 됩니다. 이 쓰레기를 효과적으로 처리하는 작업을 GC라고 합니다.

 

stop-the-world

-GC을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 작업을 멈추고, GC작업이 완료된 이후에 다시 시작한다. 하지만 대부분의 애플리케이션에서 정지되는 시간이 무시할 정도로 낮다. 대개 GC튜닝이란 이 정지되는 시간을 줄이는 것이다.

 

 

GC의 대상

1.객체가 null인 경우(ex. String str=null)

2.블럭 안에서 생성된 객체는 블럭 싱행 종료 후 대상이 된다.

3.부모 객체가 null이 되면, 포함하는 자식 객체들도 자동으로 가비지 대상이 된다.

 

Garbage Collector

-Garbage Collector는 더이상 필요없는 객체(쓰레기)를 찾아 지우는 작업을 한다. Garbage Collector는 2가지 가정하에 만들어졌다.

 1.대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.

    10,000 건의 NewObject 객체는 Loop 내에서 생성되고, 사용되지만 Loop 밖에서는 더이상 사용할 일이 없어진다. 이      런 객체들이 메모리를 계속 점유하고 있다면, 다른 코드를 실행하기 위한 메모리 자원은 지속적으로 줄어들기만 할        것이다.

    GC는 이렇게 한번쓰이고 버려지는 객체들 즉, 접근 불가능 상태가 된 객체를 주기적으로 비워줌으로써 한정된 메모      리를 효율적으로 사용할 수 있게 해준다.

 

 2.오래된 객채에서 젊은 객체로의 참조는 아주 적게 존재한다.

    Java 개발시 가장 많이 사용하는 객체는 아마도 POJO (Plain Old Java Object) 일 것이다.

   보통 어떤 값이나 상태를 저장하기 위해 POJO 객체를 생성하고, 다른 메소드나 클래스에 전달하고, 다 사용한 객체는     더이상 사용하지 않는다. 경우에 따라 오래도록 살아남아 재활용 되는 케이스가 있긴하지만, 대부분의 경우는 아닐       것 이다.

-이러한 가설을 'weak generational hypothesis'라고 한다. 이 가정의 장점을 최대화하기 위해 Young, Old영역으로 힙을 나누었다.

 

 

JVM의 메모리는 크게 클래스, 자바, 스택, PC, 힙 네이티브 메소드 스택까지 5개 영역으로 구분되어 있습니다. 

GC는 이 중 힙 메모리를 다루게 됩니다.(힙 영역은 Young, Old)로 나뉘어진다.

 

Young Generation - 객체 사용 시간이 짧은 객체들

-새롭게 생성한 객체는 여기에 위치하게 된다.

-대부분의 객체가 이 영역에서 생성되었다가 사라진다.

-Young Generation에서 객체가 사라지면 Minor GC가 발생했다고 한다.

 

Old Generation (Tenured space) - 오래 사용되는 객체들

-Young영역에서 살아남은 객체는 여기로 복사된다.

-Young영역보다 메모리가 크게 할당되어 Young영역보다 GC가 적게 발생한다.

-Old Generation에서 객체가 사라지면 Major GC(Full GC)가 발생했다고 한다.

 

Permanent Generation(Non-heap)

-이 영역에는 JVM에 의해서 사용하는 클래스와 메서드 객체 정보를 담고 있다.

-JDK8부터 Permanent Generation은 Metaspace로 교체된다.

 

JVM Heap구조

일반적으로 객체를 생성하게 되면 Young영역에 먼저 위치하고 오랫동안 사용되는 객체는 GC과정을 통해 Old영역으로 이동하게 됩니다.

 

그렇다면 Old영역에 있는 객체가 Young영역의 객체를 참조하는 경우가 있을 때에는 어떻게 처리할까?? 이러한 경우를 처리하기 위해서 Old 영역에는 512바이트의 덩어리(chunk)로 되어 있는 카드 테이블(card table)이 존재한다.

카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다. Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.

 

 

 

Young 영역의 구성

GC를 이해하기 위해서 객체가 제일 먼저 생성되는 Young 영역부터 알아보자. Young 영역은 3개의 영역으로 나뉜다.

-Eden 영역

-Survivor 영역(2개)

재 열심히 사용중인 객체를 메모리에서 제거해버린다면, 프로그램이 정상적으로 실행되지 않을 것이다.

때문에, GC를 위해서는 우선 메모리에 있는 객체가 현재 사용중인지 사용중이 아닌지를 구분할 수 있어야 한다.

위에서 살펴본 weak generational hypothesis 를 보면 ‘오래된 객체’라는 말이 나온다. 그럼 오래되었다는 기준은 무엇일까?

JVM에서는 이 오래됨 을 표현하기 위해 메모리를 여러 영역으로 나눈다.

 

처음 생성된 객체 new Model(); 는 Young Generation 영역의 일부인 Eden 영역에 위치하게된다. 그리고 Minor GC가 발생하게 되면, 사용하지 않는 다시말하면 다른 곳에서 참조되지 않는 객체는 메모리에서 제거된다.

Eden 영역에서 살아남은 객체는 Young Generation 영역의 또다른 일부인 Survivor 영역으로 이동하게된다. Survivor 영역은 Survivor1 영역과 Survivor2 영역으로 구성되어 있는데, Minor GC가 발생할 때마다 Survivor1 영역에서 Survivor2 영역으로 또는 Survivor2 영역에서 Survivor1 영역으로 객체가 이동하게되며, 이 과정에서 더이상 참조되지 않는 객체는 메모리에서 제거된다.

Minor GC가 발생하는 동안 Survivor1, Survivor2 영역을 오가며 살아남은 객체들은 최종적으로 Old Generation 영역으로 옮겨지며, Old Generation 영역에 있다가 미사용된다고 식별되는 객체들은 Full GC를 통해 메모리에서 제거된다.

 

즉 이것을 정리해보자면

 

 1.새로 생성한 대부분의 객체는 Eden 영역에 위치한다.

 2.Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.

 3.Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.

 4.하나의 Survivor 영역이 가득 차게 되면 그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다. 그리고 가득 찬    Survivor 영역은 아무 데이터도 없는 상태로 된다.

 5.이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.

 

이 절차를 확인해 보면 알겠지만 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 여러분의 시스템은 정상적인 상황이 아니라고 생각하면 된다.

이렇게 Minor GC를 통해서 Old 영역까지 데이터가 쌓인 것을 간단히 나타내면 다음과 같다.

Young Generation 영역에서 오래동안 살아남은 객체는 Old Generation 영역으로 옮겨지는데, 오래되었다는 기준은 무엇일까?

오래되었다고 하는 기준은 Young Generation 영역에서 Minor GC 가 발생하는 동안 얼마나 오래 살아남았는지로 판단한다. 각 객체는 Minor GC에서 살아남은 횟수를 기록하는 age bit 를 가지고 있으며, Minor GC가 발생할 때마다 age bit 값은 1씩 증가 하게되며, age bit 값이 MaxTenuringThreshold 라는 설정값을 초과하게 되는 경우 Old Generation 영역을 객체가 이동 되는 것이다. 또는 Age bit가 MaxTenuringThreshold 초과하기 전이라도 Survivor 영역의 메모리가 부족할 경우에는 미리 Old Generation 으로 객체가 옮겨질 수도 있다.

JVM 옵션 : -XX:MaxTenuringThreshold

 

 

 

GC의 종류

 

1.Serial GC   ( Serial GC 적용을 위한 JVM 옵션 : -XX:+UseSerialGC)

-순차적인 GC방법이다

-Serial GC 그리고 다음에 나오는 Parallel GC 를 이해하기 위해서는 Mark-Sweep-Compaction 알고리즘을 알아두어야   할 필요가 있다.

 

Mark-Sweep-Compaction

-Mark-Sweep-Compaction 이란 서로다른 다양한 GC에서 사용되는 알고리즘이다. 기본적인 GC 과정이라고 생각하면   좋을것이다.

GC가 사용되지 않는 객체를 메모리에서 제거하는 과정인만큼, GC 대상객체를 식별하고 제거하며 객체가 제거되어 파편화된 메모리 영역을 앞에서부터 채워나가는 작업을 수행하게 된다.

  • 사용되지 않는 객체를 식별하는 작업 (Mark)
  • 사용되지 않는 객체를 제거하는 작업 (Sweep)
  • 파편화된 메모리 영역을 앞에서부터 채워나가는 작업 (Compaction)

Compaction 작업의 경우 Windows의 디스크 조각 모음을 생각하면 좋을 것이다.

Serial GC가 순차적으로 동작할 수 밖에 없는 이유는 GC를 처리하는 스레드가 하나이기 때문이다. 메모리나 CPU Core 리소스가 부족할 때 사용할 수 있을 것이다. Java가 처음 등장했던 90년대 후반의 PC들을 생각해보자.

 

2.Parallel GC

-앞서 살펴본 Serial GC을 사용하던 시절보다 PC의 성능이 좋아졌다고 생각해보자. 메모리도 넉넉해졌고 CPU Core 도   좀 더 많아졌다. 이런 상황이라면 하나의 스레드로 동작했던 Serial GC를 멀티스레드로 실행하고 싶어질 것이다.

-Parallel GC는 Minor GC를 처리하는 스레드를 여러개로 늘려 좀 더 빠른 동작이 가능하게한 방식이다.

그림을 보면 Serial GC는 GC 작업을 하는 스레드(GC Thread)가 하나이며, Parallel GC에서는 이 GC Thread가 여러개 존재한다.

이는 Parallel GC 에서의 GC 프로세스가 더 빠르게 동작할 수 있게 해주며 이러한 차이는 GC를 처리하는 동안 Java의 프로세스가 모두 멈춰버리는 Stop-The-World 현상이 나타나는 시간에도 영향을 주게된다.

즉, STW(Stop-The-World) 시간이 좀 더 적게 걸리는 Parallel GC에서의 Java 애플리케이션이 좀 더 매끄럽게 동작한다는 의미이다.

 

 

3.CMS GC

앞서 살펴보았던 GC 보다 좀 더 개선된 방식이다. 개선이 된 만큼 성능은 좋아졌지만 GC의 과정은 좀 더 복잡해진 방식이다. CMS는 GC 과정에서 발생하는 STW(Stop-The-World) 시간을 최소화 하는데 초점을 맞춘 GC 방식이다.

다시 말하면 GC 대상을 최대한 자세히 파악한 후, 정리하는 시간(STW가 발생하는 시간)을 짧게 가져가겠다는 컨셉이다. 다만 GC 대상을 파악하는 과정이 복잡하한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다

CMS GC는 Initial Mark -> Concurrent Mark -> Remark -> Concurrent Sweep 과정을 거친다.

 

-Initial Mark

  -GC 과정에서 살아남은 객체를 탐색하는 시작 객체(GC Root)에서 참조 Tree상 가까운 객체만 1차적으로 찾아가며 객      체가 GC 대상(참조가 끊긴 객체)인지를 판단한다. 이 때는 STW 현상이 발생하게되지만, 탐색 깊이가 얕기 때문에          STW 발생 기간이 매우 짧다.

-Concurrent Mark

  -STW 현상없이 진행되며, Initial Mark 단계에서 GC 대상으로 판별된 객체들이 참조하는 다른 객체들을 따라가며 GC    대상인지를 추가적으로 확인한다.

 

-Remark

   -Concurrent Mark 단계의 결과를 검증한다. Concurrent Mark 단계에서 GC 대상으로 추가 확인되거나 참조가 제거되     었는지 등등의 확인을 한다. 이 검증과정은 STW 를 유발하기 때문에 STW 지속시간을 최대한 줄이기 위해 멀티스레       드로 검증 작업을 수행한다.

 

- Concurrent Sweep

  -STW 없이 Remark 단계에서 검증 완료된 GC 객체들을 메모리에서 제거한다.

CMS GC는 Compaction 작업을 필요한 경우에만 수행한다. 즉, 연속적인 메모리 할당이 어려울 정도로 메모리 단편화가 심한 경우에만 Compaction 과정을 수행하는 것이다.

 

4.G1 GC (Garbage First)

하드웨어가 발전되면서 Java 애플리케이션에 사용할 수 있는 메모리의 크기도 점차 켜저갔다. 하지만 기존의 GC 알고리즘들로는 큰 메모리에서 좋은 성능(짧은 STW)을 내기 힘들었기 때문에 이에 초점을 둔 G1 GC가 등장하게 되었다.

즉, G1 GC는 큰 힙 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.

G1 GC는 앞서 살펴본 GC와는 다른 방식으로 힙 메모리를 관리한다. 앞서 살펴보았던 Eden, Survivor, Old 영역이 존재하지만 고정된 크기로 고정된 위치에 존재하는 것이아니며, 전체 힙 메모리 영역을 Region 이라는 특정한 크기로 나눠서 각 Region의 상태에 따라 그 Region에 역할(Eden, Survivor, Old)이 동적으로 부여되는 상태이다.

JVM 힙은 2048개의 Region 으로 나뉠 수 있으며, 각 Region의 크기는 1MB ~ 32MB 사이로 지정될 수 있다. (-XX:G1HeapRegionSize 로 설정)

G1 GC가 설정된 JVM의 힙 메모리 영역의 스냅샷은 아마도 아래와 같을 것이다.

G1 GC에서는 그동안 봐왔던 Heap 영역에서 보지 못한 Humongous, Available/Unused 이 존재하며 두 Region에 대한 역할은 아래와 같다.

  • Humongous : Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간이며, 이 Region 에서는 GC 동작이 최적으로 동작하지 않는다.

  • Available/Unused : 아직 사용되지 않은 Region을 의미한다.

 

G1 GC에서 Young GC 를 수행할 때는 STW(Stop-The-World) 현상이 발생하며, STW 시간을 최대한 줄이기 위해 멀티스레드로 GC를 수행한다. Young GC는 각 Region 중 GC대상 객체가 가장 많은 Region(Eden 또는 Survivor 역할) 에서 수행 되며, 이 Region 에서 살아남은 객체를 다른 Region(Survivor 역할) 으로 옮긴 후, 비워진 Region을 사용가능한 Region으로 돌리는 형태 로 동작한다.

 

G1 GC에서 Full GC 가 수행될 때는 Initial Mark -> Root Region Scan -> Concurrent Mark -> Remark -> Cleanup -> Copy 단계를 거치게된다.

  • Initial Mark
    • Old Region 에 존재하는 객체들이 참조하는 Survivor Region 을 찾는다. 이 과정에서는 STW 현상이 발생하게 된ㄷ.
  • Root Region Scan
    • Initial Mark 에서 찾은 Survivor Region에 대한 GC 대상 객체 스캔 작업을 진행한다.
  • Concurrent Mark
    • 전체 힙의 Region에 대해 스캔 작업을 진행하며, GC 대상 객체가 발견되지 않은 Region 은 이후 단계를 처리하는데 제외되도록 한다.
  • Remark
    • 애플리케이션을 멈추고(STW) 최종적으로 GC 대상에서 제외될 객체(살아남을 객체)를 식별해낸다.
  • Cleanup
    • 애플리케이션을 멈추고(STW) 살아있는 객체가 가장 적은 Region 에 대한 미사용 객체 제거 수행한다. 이후 STW를 끝내고, 앞선 GC 과정에서 완전히 비워진 Region 을 Freelist에 추가하여 재사용될 수 있게 한다.
  • Copy
    • GC 대상 Region이었지만 Cleanup 과정에서 완전히 비워지지 않은 Region의 살아남은 객체들을 새로운(Available/Unused) Region 에 복사하여 Compaction 작업을 수행한다.

 

 

 

 

참조

blog.advenoh.pe.kr/java/%EC%9E%90%EB%B0%94-Garbage-Collection%EC%9D%B4%EB%9E%80/

d2.naver.com/helloworld/1329

mirinae312.github.io/develop/2018/06/04/jvm_gc.html

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

[Java8] 새로운 collection api  (0) 2021.04.10
Comments