코딩관계론

Heap 메모리의 청소부 - Garbage Collection 본문

개발/Java

Heap 메모리의 청소부 - Garbage Collection

개발자_티모 2024. 6. 9. 22:41
반응형

우리는 앞서 JVM의 메모리 구조를 살펴봤다 - 2024.06.08 - [개발/Java] - JVM 메모리 구조

 

JVM 메모리 구조

이전 시간에는 JVM이 코드를 어떤 방식으로 실행하는지 알아봤다(2024.06.02 - [개발/Java] - 자바 어떻게 실행되는가?)지금부터는 JVM의 데이터 영역에 대해서 집중적으로 탐구해보겠다.JVM의 Run time dat

bjwan-career.tistory.com

그 중에 heap을 설명하면서 heap 메모리를 정리하기 위해서 Garbage Collection이 작동한다고 배웠다. 그럼 프로그래머는 자신의 application에 맞는 GC를 선택해야 한다고 한다. 그게 좋은 자바 개발자라고 네이버가 소개함 

그럼 이 Garbage Collection이 어떻게 작동될까? 이를 알기전에 Stop the world에 대해서 알아보자.

Stop-the-World

"Stop-the-World" (STW)은 자바 가비지 컬렉션에서 중요한 개념으로, 가비지 컬렉션이 실행되는 동안 애플리케이션의 모든 작업을 일시 중지시키는 현상을 말합니다. 이는 가비지 컬렉터가 메모리를 안전하게 관리하고 객체 참조를 수정할 수 있도록 하기 위한 필수적인 단계입니다. STW가 발생하면 JVM은 모든 애플리케이션 스레드를 멈추고, 가비지 컬렉션 작업을 완료한 후에야 다시 애플리케이션 스레드를 재개합니다.

왜 멈춰야 하나?

  1. 객체 참조 무결성:
    • 가비지 컬렉터가 동작하는 동안 애플리케이션 스레드가 객체를 생성하거나 참조를 수정한다면, 가비지 컬렉터가 정확한 메모리 상태를 파악하기 어려워집니다. 이는 잘못된 메모리 해제와 같은 심각한 문제를 초래할 수 있습니다.
  2. Race Condition:
    • 여러 스레드가 동시에 메모리에 접근하면서 발생할 수 있는 경합 조건(Race Condition) 문제를 방지하기 위해 STW가 필요합니다. STW 없이 가비지 컬렉션이 진행된다면, 스레드 간의 경합으로 인해 데이터의 일관성이 깨질 수 있습니다.
  3. Marking과 Compaction의 문제:
    • 가비지 컬렉션의 마킹 단계에서는 객체 그래프를 탐색하여 살아있는 객체를 식별합니다. 애플리케이션 스레드가 동작 중이면 이 과정에서 참조가 변경될 수 있어, 올바른 마킹이 어렵습니다.
    • 메모리 압축(Compaction) 과정에서도 객체를 이동시키는 동안 다른 스레드가 그 객체에 접근하면 문제가 발생할 수 있습니다.

따라서 GC의 성능 향상의 목적은 STW타임을 줄이는 것입니다.

길어지게 된다면?

Stop-the-world(STW)가 길어지면, 어플리케이션 사용자가 이상 현상을 느낄 수 있을 뿐만 아니라, 다른 문제들도 발생할 수 있습니다.

 

예를 들어 데이터베이스(DB)를 생각해봅시다. DB와 어플리케이션이 서로 살아있음을 확인하기 위해 주기적으로 ping 체크를 할 수 있습니다. 만약 STW가 길어져 ping에 응답하지 못하면, DB 커넥션이 끊어질 수 있습니다.

 

이는 커넥션을 다시 맺어야 하는 상황을 초래하며, 이로 인해 커넥션 재설정 비용이 증가하게 됩니다. 결과적으로 어플리케이션 성능이 저하될 수 있습니다.

Marking & Sweep & Compaction

Marking과 Compaction은 자바 가비지 컬렉션의 중요한 단계 중 두 가지입니다. 이들은 메모리를 관리하고, 더 이상 필요하지 않은 객체를 식별하고 제거하는 데 사용됩니다.

1. Marking (마킹)

Marking 단계는 가비지 컬렉션 과정에서 사용 중인 객체를 식별하는 단계입니다. 이 과정에서 가비지 컬렉터는 루트(root)로부터 시작하여, 참조 체인을 따라가면서 도달할 수 있는 모든 객체를 식별하고 마킹합니다. 이 과정에서 마킹된 객체들은 살아있는(live) 객체로 간주되어 가비지 컬렉션 대상이 아닙니다.

Marking 단계는 주로 다음과 같은 세 가지 알고리즘으로 구현됩니다:

  • Reachability Analysis (도달 가능성 분석): 객체가 루트(root)로부터 접근 가능한지 확인합니다. 이를 위해 주로 그래프 탐색 알고리즘을 사용합니다.
  • Tri-Color Marking Algorithm: 객체를 세 가지 색(흰색, 회색, 검은색)으로 구분하여 마킹하는 알고리즘입니다.
  • Concurrent Marking: 애플리케이션 스레드와 병행하여 객체를 마킹하는 알고리즘으로, 애플리케이션의 중단 시간을 최소화합니다.

Marking 단계를 통해 메모리에 살아있는 객체를 식별하고, 이후 단계에서 이들을 보존하고 나머지 객체를 정리할 수 있습니다.

스택이나 네이티브 메소드 스택이 힙을 참조하는 것은 이해가 가지만 method area가 힙을 참조하는 것은 이해가 잘 가지 않을 수도 있다. 왜냐하면 heap은 동적으로 생성되는 공간이고 method area는 바이트 클래스가 메모리에 올라가면서 생성되는 영역이기 때문이다.

하지만 힙의 인스턴트가 static 메소드 or 변수를 참조하고 있기 때문에 root가 된다.

2. Sweeping (스윕)

힙을 순회하면서 마킹되지 않은 객체들을 찾아 메모리에서 제거합니다. 이 과정에서 메모리의 단편화가 발생할 수 있지만, 주소 재배치가 필요 없기 때문에 알고리즘이 빠르게 동작하게 됩니다.

3. Compaction (메모리 압축)

Compaction 단계는 가비지 컬렉션 과정에서 발생하는 메모리 단편화를 해소하기 위한 단계입니다. 메모리 단편화란 메모리 공간이 작은 조각으로 나뉘어 있어서, 연속된 큰 블록을 할당하기 어려운 상태를 말합니다. 이는 메모리 사용의 효율을 떨어뜨릴 뿐만 아니라, 할당 및 해제 과정에서 오버헤드를 초래할 수 있습니다.

Compaction은 사용 중인 객체들을 메모리의 한쪽으로 모으고, 그릇된 메모리 블록을 해제하여 연속된 공간을 만듭니다. 이를 통해 할당 과정에서 메모리 단편화를 최소화하고, 메모리 사용 효율을 높일 수 있습니다.

Compaction은 일반적으로 Old Generation 영역에서 주로 발생하며, 다음과 같은 단계로 수행됩니다:

  1. Marking 후 Compaction: 객체가 마킹된 후, 메모리를 압축하여 연속된 공간을 만듭니다.
  2. 객체 이동: 살아있는 객체를 메모리의 한쪽으로 이동시키고, 그릇된 메모리 블록을 해제합니다.
  3. 주소 재배치: 객체가 이동했으므로, 이에 따라 참조된 주소를 업데이트해야 합니다.

Compaction은 애플리케이션의 일시 중단이 발생할 수 있으며, 특히 큰 힙 크기에서는 시간이 오래 걸릴 수 있습니다. 따라서, 최신 JVM 가비지 컬렉션 알고리즘은 Compaction을 최소화하고 성능을 향상시키기 위해 다양한 기술과 전략을 채택하고 있습니다.

 

4. Copying알고리즘 

Copying알고리즘은 gc의 동작 방식 중 하나입니다. 이 과정에서도 가비지 컬렉터는 루트로부터 시작하여 참조 체인을  따라가면서 도달할 수 있는 객체를 식별하고, 이 과정에서 살아있는 객체로 간주되어 가바지 컬렉션 대상이 아니게 됩니다.

  1. marking: 도달할 수 있는 객체를 식별하는 단계
  2. copying: 살아있는 객체를 다른 주소공간으로 복사 후, 이전 주소 공간에 있는 객체들을 전부 삭제(live + dead)
  3. 주소 재배치: 객체가 이동했으므로, 이에 따라 참조된 주소를 업데이트해야 합니다.

이 방법은 주로 마이너 gc에서 사용되는데 이 알고리즘이 빠르기도 하지만 메모리의 단편화 문제를 해결할 수 있습니다. 기존의 주소 공간에 있는 메모리들을 모두 삭제하기 때문에 메모리의 단편화 영향이 적고, 새로운 공간에 재배치하게 되면서 객체가 연속된 메모리 공간에 위치할 수 있게 됩니다. 하지만 추가적인 공간이 필요하고 객체의 주소가 변경되기 때문에 주소 재배치가 필요합니다.

 

참조되고 있는 객체를 판별한 후 살아있는 객체를 복사해 비어있는 영역으로 이동하고 현재 공간의 객체를 모두 소멸시키는 것을 뜻한다. 이렇게 되면 장점은 메모리의 단편화가 적어진다. 하지만 단점으로는 추가적인 공간이 필요하고 카피를 하면서 객체의 주소가 변경되기 때문에 주소 재배치가 필요하다.

 

밑에서 말씀드리겠지만 서바이벌 영역0, 1 영역 중 하나는 비어있어야 한다고 가정하는데 이는 copying알고리즘 때문이다.

 

 

그럼 HotSpot VM에서의 GC방법은?

이 방벙을 알기전에 GC는 두 가지 가정하고 메모리를 해제하는 과정을 수행한다.

  • 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다. 
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

이 가정을 성실히 수행하기 위해서 Hotspot Vm에서는 heap메모리 영역을 아래와 같이 나누었다.

heap 메모리 구조

 

Young 영역(Yong Generation 영역): 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다. 

 

Old 영역(Old Generation 영역): 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다. 그렇기 때문에 Major GC는 시간이 걸리더라도 적게 발생할 것으로 가정된다.

 

Young 영역

Young 영역의 구성 요소

Young 영역은 크게 두 가지 영역으로 구성됩니다:

  1. Eden 영역 (Eden Space):
    • Young 영역 중에서 가장 큰 영역으로, 새로운 객체가 할당되는 곳입니다.
    • 대부분의 객체는 Eden 영역에 할당되며, 초기에는 여기에서 가비지 컬렉션(GC)이 발생하지 않습니다.
  2. Survivor 영역 (Survivor Space):
    • Eden 영역에서 가비지 컬렉션 후에 살아남은 객체들이 이동하는 곳입니다.
    • Survivor 영역은 일반적으로 두 개의 영역으로 나뉘며, 한 번의 가비지 컬렉션 사이클 동안에는 하나의 Survivor 영역이 비어 있습니다.

Young 영역의 가비지 컬렉션

Young 영역은 대부분의 객체가 짧은 수명을 가지므로, 빈번한 가비지 컬렉션을 수행하여 살아남은 객체들을 제거합니다. 이를 통해 메모리 사용을 최적화하고, 오래된 객체가 Old 영역으로 이동하는 것을 지연시킵니다.

일반적으로 Young 영역의 가비지 컬렉션은 "Minor GC"라고도 불립니다. 이는 Young 영역에서만 발생하며, Eden 영역과 Survivor 영역 간의 객체 이동을 통해 수행됩니다.

  1. Eden 영역에서 Survivor 영역으로의 이동:
    • 새로운 객체는 Eden 영역에 할당됩니다.
    • Eden 영역이 가득 차면, Minor GC가 발생하고 Eden에 있는 살아남은 객체는 Survivor 영역 중 하나로 이동합니다.
  2. Survivor 영역 간의 객체 이동:
    • 가비지 컬렉션 주기마다, 살아남은 객체는 이전에 사용되던 Survivor 영역으로 이동합니다. 이 과정에서 객체의 연령(Age)이 증가합니다.
    • 일정 연령을 넘으면 Old 영역으로 이동할 가능성이 있습니다.

 

Survivor의 두 개의 영역 중 하나는 빈 공간으로 남아있어야 하는데 아래의 이유와 같다.

  1. 메모리 이동의 대상 공간 확보:
    • 가비지 컬렉션이 시작되면, Eden 영역과 현재 사용 중인 Survivor 영역(S0 또는 S1)에 있는 객체가 스캔됩니다.
    • 살아남은 객체를 다른 Survivor 영역으로 복사해야 하므로, 이 복사 대상 공간이 필요합니다. 이 때, 다른 Survivor 영역(S1 또는 S0)은 비어 있어야 합니다.
  2. 객체 정리와 단편화 방지:
    • 객체를 빈 영역으로 복사함으로써, 메모리 단편화를 방지할 수 있습니다. 복사된 객체는 연속된 메모리 공간에 배치되므로, 메모리 사용이 더 효율적입니다.
  3. GC 효율성:
    • 복사하는 과정에서 새로운 객체 할당에 필요한 공간을 쉽게 확보할 수 있습니다. 비어 있는 Survivor 영역은 다음 번 Minor GC에서 새롭게 할당된 객체를 수용할 준비가 되어 있습니다.

Old 영역

Old 영역은 Young 영역에서 살아남은 객체들이 복사되어 저장되는 곳입니다.  이 기준이 되는 것은 각 객체마다 GC count가 존제하고, 이 카운트가 임계값을 넘게되면 Old 영역으로 복사되게 됩니다. young 영역보다 크게 할당되며 보통 더 오랜 시간 동안 객체들이 유지됩니다. 따라서, Old 영역에서의 가비지 컬렉션은 Young 영역에 비해 덜 빈번하게 발생하지만, 더 많은 시간과 자원을 소비하게 됩니다. 따라서 이 영역의 GC의 이름은 Major GC라고 불리게 됩니다.

 

Old 영역의 특징과 기능:

  1. 접근 불가능 상태가 아닌 객체 보관: Young 영역에서 접근 불가능 상태가 아닌 객체들이 Old 영역으로 이동됩니다. 이는 해당 객체들이 아직 사용 중이거나 더 오랜 기간 동안 사용될 것으로 예상됨을 의미합니다.
  2. 메모리 할당 및 해제: Old 영역은 Young 영역보다 크게 할당되며, 오래된 객체들이 보관됩니다. 이 영역에서 객체가 해제되는 경우에는 Major GC(또는 Full GC)가 발생하며, 시간이 오래 걸릴 수 있습니다.
  3. 메모리 단편화: Old 영역도 메모리 단편화 문제를 겪을 수 있습니다. 메모리 할당 및 해제 과정에서 발생하는 메모리 조각화로 인해 연속된 큰 메모리 블록을 할당하기 어려울 수 있습니다. 이는 Compaction 과정에서 해소될 수 있습니다.
  4. 성능 저하 요인: Old 영역에서의 가비지 컬렉션은 Young 영역에 비해 더 많은 시간과 자원을 소비할 수 있습니다. 특히, Old 영역의 크기가 클수록 Major GC의 실행 시간이 늘어날 수 있습니다.

Old 영역의 관리:

  • Major GC 실행 조건: Old 영역에서의 가비지 컬렉션은 접근 불가능 상태가 아닌 객체들이 많아져서 더 이상 메모리를 확보할 수 없을 때 발생합니다. 이런 경우에 Major GC가 실행되어 더 이상 사용되지 않는 객체들을 해제하고 메모리를 회수합니다.
  • 튜닝 및 최적화: Old 영역의 성능을 향상시키기 위해 GC 튜닝 및 최적화 작업이 필요할 수 있습니다. 이를 통해 Major GC의 실행 시간을 줄이고 메모리 사용 힙에 할당된 객체는 금방 소멸된다 힙에 할당된 객체는 금방 소멸된다을 최적화할 수 있습니다.
  • 영역 분할: 일부 JVM 구현에서는 Old 영역을 세분화하여 크기가 작은 영역으로 나누어 관리하는 경우가 있습니다. 이를 통해 가비지 컬렉션의 효율성을 향상시킬 수 있습니다.

Old 영역은 Young 영역과 함께 JVM의 메모리 구조에서 중요한 역할을 담당하며, 영역마다 서로 다른 특징과 동작 방식을 가지고 있습니다. 종합적으로 보면, Old 영역은 오래된 객체들을 보관하고 관리함으로써 전체적인 가비지 컬렉션의 효율성과 성능을 유지하고 개선하는 데 중요한 역할을 합니다.

Young 영역과 Old 영역의 크기 비교 

young영역과 old영역의 크기를 비교하면 young영역이 크기가 작고, Old영역의 크키가 크다.

왜 Young 영역이 더 작은 것일까?

그 이유는 GC의 가정에서 알 수 있는데 GC는 "생성된 객체는 금방 접근 불가능한 상태가 된다라는 가정에 기반합니다." 따라서 메모리의 효율과 사용량을 낮추기 위해서는 이러한 객체를 빠르게 수거하는 것이 좋을 것입니다.

 

GC가 발동하기 위해서는 메모리가 꽉 차야 합니다. Young 영역이 작으면 상대적으로 빨리 가득 차기 때문에 Minor GC가 자주 발생하게 됩니다. 이를 통해 불필요한 객체를 빠르게 회수하고 메모리 사용을 최적화할 수 있습니다. 항상 Young 영역을 작게 할당하여 Minor GC가 빈번히 일어나는 것이 좋은 것은 아닙니다. 어플리케이션 특성에 맞게 메모리를 할당할 수 있어야 합니다.

 

실시간 처리가 필요한 어플리케이션에서는 작은 Young영역을 할당하여 Minor GC가 빈번하게 일어나도록 함으로써 사용자가 STW를 느끼지 못하게 하는 것이 중요합니다. 하지만 대용량 데이터를 처리하거나 배치 작업을 수행하는 어플리케이션에서는 Young 영역을 크게 할당함으로써 GC 횟수를 줄여 전체 성능을 향상시킬 수 있습니다.

 

따라서 Young 영역의 크기는 어플리케이션의 특성과 요구사항에 따라 최적화되어야 합니다. 실시간 처리가 필요한 경우에는 작은 Young 영역을 통해 빈번한 Minor GC를 유도하고, 배치 작업이나 대용량 데이터를 처리하는 경우에는 큰 Young 영역을 할당하여 GC 빈도를 줄이는 전략이 필요합니다.

 

GC를 없애면 되지 않아?

앞서 이야기했듯이, STW가 어플리케이션의 성능을 좌우한다면 GC를 실행하지 않으면 되는 것이 아닌가라는 생각이 들 수 있습니다.

 

맞는 말입니다. GC가 작동하는 조건을 생각해보면, 1. 메모리가 가득 차게 되고, 2. 메모리에 객체의 연결관계가 끊어지면 GC가 작동하게 됩니다.

 

즉, 프로그램을 시작할 때 모든 클래스를 객체화하고, 해당 객체 간의 참조 관계를 끊지 않으면 GC가 작동할 일이 없어지게 됩니다. 하지만 이는 굉장히 유지보수가 어렵기 때문에 좋은 방법이 아닙니다.

 

따라서 우리는 GC튜닝을 통해서 이를 해결해야만 합니다.

 

GC  종류 참고

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

 

 

[참고자료]

https://d2.naver.com/helloworld/1329

반응형

'개발 > Java' 카테고리의 다른 글

제네릭과 와일드카드에 대해서 알아보자  (0) 2024.06.19
Error vs Exception  (0) 2024.06.18
Thread Safety하게 만들자  (0) 2024.06.09
static, final 어디까지 알아보고 왔는가?  (0) 2024.06.09
JVM 메모리 구조  (0) 2024.06.08