어느덧 가비지 컬렉션 시리즈도 5번째나 되네요. 지난 포스트들에서 가비지 컬렉션의 기본 작동 방식세대별 가비지 컬렉션, LOH(Large Object Heap), 가비지 컬렉션 발생 시기를 살펴보았습니다. 이번 포스트는 CLR이 가비지 컬렉션을 수행하는 방식에 대한 내용입니다. CLR은 주어진 조건이나 설정에 따라 Workstation-GC, Server-GC 모드로 작동하게 되는데 이들에 대해 살펴보도록 하겠습니다. 특히, 닷넷 프레임워크 4.0에서 변경된 Background GC에 대해서도 살펴보도록 하겠습니다. 가비지 컬렉션에 대해 익숙하지 않으신 독자 분이나 이 글을 읽다가 이해가 안 가시는 분들은 지난 포스트들을 참고하시기 바랍니다.

참고) 이 글은 월간 마이크로소프트웨어 2011년 5월호 Inside Developer 칼럼의 일부로서 포함되었습니다.

Garbage Collection Modes

CLR(Common Langauge Runtime)은 메모리 관리를 위해 다양한 최적화 기법을 사용한다. 세대별 가비지 컬렉션이 그들 중 하나이며 오늘 설명할 가비지 컬렉션 모드도 그러한 최적화 기법 중 하나이고 다음 포스트 정도에서 설명할 관리되는 힙의 구조 역시 마찬가지이다.

가비지 컬렉션이 어떤 스레드에 의해 수행되는 가와 가비지 컬렉션이 수행되는 동안 CLR에 의해 관리되는 스레드(닷넷 어플리케이션이 사용하는 스레드들)들의 중단(suspend) 여부에 따라서 Workstation-GC, Server-GC 모드로 나누어 볼 수 있으며 Workstation-GC 모드는 다시 Non-Concurrent-GC 모드와 Concurrent-GC 모드로 나누어 볼 수 있다. 닷넷 프레임워크 4.0에 새로이 추가된 Background-GC 모드는 기존의 Concurrent-GC 모드를 대체하는 가비지 컬렉션 모드이다.

ImYourFather이 정도는 알고 있어야 어디 가서도 가비지 컬렉션에 대해 “내가 니 애비다”라고 자신 있게(응?) 말할 수 있다. 이번 포스트는 지난 포스트들에 비해 난이도도 더 높고 스크롤 압박도 상당할 것이니 시간이 뎀빌 때 천천이 음미를 하는 것이 좋을 것이다. 또, 읽다가 이해가 안 된다고 좌절할 필요도 없다. 처음엔 이해가 잘 안가는 것이 정상이다. 다만 잠시 자기 반성을 갖고 나중에 다시 읽어 보면 이해가 될 수도 있다.


가비지 컬렉션의 기본 작동 방식에서 살펴 본 바 대로 가비지 컬렉션이 수행되면 관리되는 힙은 조낸 변화가 된다. 가비지 컬렉션이 수행되는 동안 살아 남은 객체들은 컴팩션에 의해 메모리 상의 위치가 바뀌게 되고 이들 객체를 참조하는 로컬 변수, 정적 멤버, 그리고 객체 멤버의 참조 값 역시 바뀔 수도 있다. 이렇게 참조 값들이 변화되는 도중에 어플리케이션이 새로운 메모리를 할당(객체 생성)하거나 객체를 참조하는 작업을 수행하게 되면 어플리케이션은 엉망진창이 될 것이며 잘못된 참조로 인해 어플리케이션이 숏 될 수도 있다. 이런 이유에서 가비지 컬렉션이 수행되는 동안 어플리케이션은 어쩔 수 없이 수행을 중단(suspend)해야 하며 CLR은 가비지 컬렉션을 시작하기 전 닷넷 스레드들을 중단한다(CLR에 의해 관리되지 않는 스레드들은 중단되지 않는다).

Workstation-GC 모드와 Server-GC 모드는 얼마 동안 닷넷 스레드들이 중단되며 가비지 컬렉션을 수행하는 주체가 어떤 스레드인가에서 차이가 난다. Workstation-GC 모드는 Concurrent-GC 활성화 여부에 따라서 약간 달라지지만 일반적으로 가비지 컬렉션을 유발한 스레드가 가비지 컬렉션을 수행하게 된다. 반면 Server-GC는 가비지 컬렉션을 위한 별도의 스레드를 생성하고 이 스레드가 가비지 컬렉션을 수행한다. 또, Workstation GC는 Concurrent-GC가 활성화 되어 있다면, 아주 잠시 동안만 (그러나 자주) 어플리케이션의 스레드들을 중단하는 반면 Server-GC는 가비지 컬렉션을 수행하는 동안 내내 어플리케이션의 스레드들이 중단되며 가비지 컬렉션이 완전히 끝나면 어플리케이션의 스레드들은 수행을 다시 재개(resume)할 것이다.

특별히 아무런 설정도 하지 않았을 때에는 서버 운영체제에서도 Workstation-GC 모드가 기본적으로 사용된다. Server-GC 모드를 사용하고자 할 때에는 구성 파일(configuration file)의 <runtime> 섹션에서 <gcServer> 요소의 enabled 속성을 true로 설정하면 된다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <gcServer enabled="true" />
  </runtime>
</configuration>

ASP.NET이나 SQL Server와 같이 CLR을 호스팅 하는 프로세스는 구성 파일과 무관하게 CLR Hosting API를 통해 가비지 컬렉션 모드를 지정할 수도 있다. 이들은 기본적으로 Server-GC 모드를 사용하며 설정을 통해 Server-GC 모드를 사용하지 않도록 해야만 한다.

Non-Concurrent-GC

Non-Concurrent-GC 모드는 Workstation-GC 모드의 한 종류로서 가비지 컬렉션이 수행되기 전 모든 관리되는 스레드를 중단(suspend)하고 가비지 컬렉션을 유발한 스레드에서 가비지 컬렉션을 수행한다. 그림 1은 Non-Concurrent GC 모드의 작동 상황을 보여주고 있다. Thread #2가 메모리 할당을 수행하다가 0 세대 힙 부족 등의 이유로 가비지 컬렉션을 유발시켰고 이로 인해 다른 두 스레드가 정지되었다. 그리고 Thread #2 상에서 가비지 컬렉션이 수행된다. Thread #2가 0 세대이건 1세대이건 혹은 2세대 가비지 컬렉션이건 가비지 컬렉션을 완료하면 이 스레드는 원래의 코드를 계속 수행하게 되고 나머지 두 스레드 #1, #3 역시 작업을 다시 계속(resume)하게 된다.

image
그림1. Non-Concurrent-GC 모드 (만지면 커져요)

Non-Concurrent-GC 모드는 가비지 컬렉션이 수행되는 동안 다른 관리되는 스레드들이 모두 중단되기 때문에 가비지 컬렉션에 소요되는 시간이 긴 경우, 어플리케이션이 정지되는 시간 역시 길다. 많은 메모리를 사용하여 커다란 힙을 가진 어플리케이션의 경우, 2세대 가비지 컬렉션이 발생하면 사용자가 어플리케이션의 반응이 끊기는 것을 느낄 수도 있다.

Non-Concurrent-GC 모드는 일반적인 설정 상 사용되지 않는 가비지 컬렉션 모드이다. CLR의 기본 설정은 Concurrent-GC 모드를 사용하도록 구성되어 있기 때문에 명시적으로 Concurrent-GC 모드를 비 활성화 시키지 않는 한 Non-Concurrent-GC가 사용되지 않는 것이다. Non-Concurrent-GC 모드를 명시적으로 사용하기 위해서는 다음과 같이 Concurrent-GC를 구성 파일에서 비활성화 시켜야 한다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <gcConcurrent enabled="false" />
  </runtime>
</configuration>

구성 파일 설정에 상관없이 항상 Non-Concurrent-GC 모드가 활성화 되어 사용되기도 하는데 시스템이 단일 프로세서(uniprocessor; UP) 만을 가진 시스템인 경우이다. UP 시스템에서는 구성 파일이나 Hosting API를 사용하더라도 Concurrent-GC 모드가 사용되지 않으며 Server-GC 모드를 사용하도록 구성 파일 설정을 하거나 Hosting API를 호출하더라도 Non-Concurrent-GC 모드가 항상 사용된다. Non-Concurrent-GC 모드는 하나의 CPU만을 가진 단일 프로세서 시스템에 대해서 최적의 처리량(throughput)을 낼 수 있도록 구성되어 있기 때문이다.

Concurrent-GC

Workstation-GC의 한 종류인 Concurrent-GC 모드는 닷넷 프레임워크의 “디폴트 가비지 컬렉션 모드”이다. 별다른 구성 파일 설정이 없다면 CLR은 Concurrent-GC 모드를 선택하도록 되어 있다. Concurrent-GC 모드는 관리되는 스레드들의 중단 시간을 최소화하여 어플리케이션의 응답 시간(response time)을 향상시키는데 그 목적이 있다. 사용자의 응답 시간이 중요한 것은 UI를 갖는 데스크톱 어플리케이션들이기 때문에 Concurrent-GC 모드가 Workstation-GC로써 분류되는 것이다.

Concurrent-GC 모드가 활성화 된 상황(디폴트)에서 가비지 컬렉션이 발생하면 CLR은 관리되는 모든 스레드를 중단하고 가비지 컬렉션을 유발한 스레드에서 일단 가비지 컬렉션을 수행한다. 여기까지는 Non-Concurrent-GC와 다를 바가 없다. 0 세대, 1세대 힙에 대한 가비지 컬렉션을 마치고 2세대 가비지 컬렉션까지 수행해야 한다면 CLR은 별도의 가비지 컬렉션 스레드에게 2세대 가비지 컬렉션을 수행하도록 지시하고 다른 관리되는 스레드들의 작업을 다시 재개 한다. 이 점이 Non-Concurrent-GC 모드와의 차이점이며 잠시 후에 살펴볼 Server-GC 모드와도 다른 점이 되겠다. 물론, 0세대 혹은 1세대 가비지 컬렉션만을 수행하는 상황(0 세대 혹은 1 세대 GC)에서는 Non-Concurrent-GC 모드와 다를 바가 없을 것이다. Concurrent-GC 모드는 2세대 가비지 컬렉션(0, 1세대 힙에 대한 가비지 컬렉션 포함)이 발생할 때에만 유효한 기법이다.

그림 2는 Concurrent GC가 발생했을 때의 상황을 예로 보여주고 있다. 앞서 예와 마찬가지로 Thread #2가 메모리를 할당하려다가 가비지 컬렉션을 유발하였다고 가정해 보자. CLR은 관리되는 스레드들을 모두 중단하고 Thread #2로 하여금 가비지 컬렉션을 수행하도록 한다. CLR은 0 세대와 1세대 가비지 컬렉션을 마치고 2 세대 가비지 컬렉션이 필요하다고 판단되면 가비지 컬렉션 스레드(아직 생성되지 않았다면 생성을 먼저 한 후)를 깨워(wakeup) 2세대 가비지 컬렉션 작업을 수행하도록 지시하게 된다. 그리고 다른 스레드들은 모두 자신의 작업을 계속 진행하도록 설정을 한다. 이렇게 스레드들이 자신의 작업을 수행하는 동안 가비지 컬렉션도 동시에 진행되기 때문에 Concurrent-GC라는 이름을 붙여졌다고 보면 된다.

image
그림2. Concurrent-GC 모드 (만지면 커져요)

0세대와 1세대에 대한 가비지 컬렉션은 매우 빠르기 때문에 Concurrent-GC 모드를 활용하면 어플리케이션의 중단 시간은 Non-Concurrent-GC 모드에 비해 크게 줄어들 수 있다. Concurrent-GC 모드에서 0세대와 1세대를 가비지 컬렉션 하는 동안 스레드들을 중단하는 반면 2 세대 힙을 가비지 컬렉션 하는 동안 스레드들을 다시 수행할 수 있는 이유는 메모리 할당은 항상 0세대에서 이루어지기 때문이다. 커다란 객체는 LOH 힙에 할당되지만 LOH 힙은 컴팩션을 수행하지 않기 때문에 약간의 잠금이 발생하지만 메모리 정리와 할당이 동시에 진행 될 수 있다. 따라서 2세대에 대해 가비지 컬렉션을 수행하는 동안에도 메모리 할당이 가능하기 때문에 스레드들이 수행을 재개할 수 있는 것이다. 응답시간이 중요한 클라이언트 UI 어플리케이션에서 오랜 가비지 컬렉션 시간 동안 UI가 얼어버리는(freezing) 현상을 Concurrent-GC 모드를 통해 최소화 할 수 있는 것이다.

Concurrent-GC에도 한계가 있다. 가비지 컬렉션 스레드가 2세대 힙에 대한 가비지 컬렉션을 하는 동안 항상 다른 스레드들을 무작정 계속 수행할 수 있는 것은 아니다. 2세대 가비지 컬렉션이 가비지 컬렉션 스레드에 의해 진행되는 동안 스레드들은 계속 메모리 할당을 진행할 수 있지만 이 과정에서 0세대 혹은 1세대 힙이 부족한 상황이 발생할 수도 있다. 다시 말해 2세대 힙에 대한 가비지 컬렉션이 완료되지 않은 상황에서 0세대 혹은 1세대 가비지 컬렉션이 중첩되어 발생할 수도 있다는 것이다. 이와 같은 복잡한 상황을 피하기 위해서 CLR은 2세대 힙에 대한 가비지 컬렉션이 수행되는 동안 일정 이상의 메모리 할당이 수행되면 모든 관리되는 스레드들을 중단하고 가비지 컬렉션 스레드가 남은 작업을 마칠 때까지 기다린다. 비로소 가비지 컬렉션 스레드가 자신의 일을 모두 완료하면 중단되었던 스레드들의 수행을 재개하게 된다.

Concurrent-GC 모드를 “사용”하기 위한 특별한 설정은 없다. CLR의 기본 설정이 Concurrent-GC 모드이기 때문이다. 대신 Concurrent-GC 모드를 사용하지 않기 위해서는 앞서 살펴본 대로 <gcConconcurrent enabled=”false”> 설정을 사용하면 된다. 이처럼 명시적으로 Concurrent-GC 모드를 끄지 않는 한 CLR은 Concurrent-GC 모드를 사용하여 메모리를 관리하게 된다. 물론 Server-GC 모드를 명시적으로 사용하도록 설정하면 Concurrent-GC가 사용되지 않고 Server-GC 모드가 사용됨은 두 말할 것 없다(<gcConcurrent> 설정 무시).

Concurrent-GC 모드를 사용함으로써 어플리케이션의 중단은 최소화 할 수 있지만 단점도 존재한다. Concurrent-GC가 2세대 힙을 정리하는 동안 0, 1 세대에 존재하는 객체들이 2세대 객체를 “참조”하고 있을 때 2세대 힙을 컴팩션 하는데 제약이 따를 수도 있다. 때로는 Concurrent-GC가 2세대 힙을 정리하는 동안 중간 중간 잠시 동안만(그러나 생각 보다 자주) 관리되는 스레드들을 모두 중단하고 참조 값들을 업데이트 할 수도 있으며 때로는 일부 2세대 힙에 대해 객체를 옮기는 컴팩션 작업을 수행하지 않을 수도 있다. 이런 이유에서 Concurrent-GC 모드는 Non-Concurrent-GC보다 약간 더 큰 힙을 구성하기도 한다. 세상에는 공짜란 없으며 트레이드 오프는 항상 존재하기 마련이다.

Background GC

이제 Background-GC 모드에 대해 살펴보자. Background-GC는 닷넷 프레임워크 4.0에 새로이 도입된 Workstation-GC 모드로써 Concurrent-GC를 대체하는 기능이다. Concurrent-GC 모드는 닷넷 프레임워크 1.1 시절부터 지금까지 쭈욱 제공되어 왔었지만 잘 알려지지 않았었다. 따라서 닷넷 프레임워크 2.0, 3.0, 3.5 버전을 사용하는 경우에는 Concurrent-GC가, 닷넷 프레임워크 4.0을 사용하는 경우에는 Background-GC가 기본 가비지 컬렉션 모드로 간주 된다. 닷넷 프레임워크 4.0 상에서 Background-GC 모드를 사용하지 않고 Concurrent-GC 모드를 사용할 방법은 없다. Background-GC가 완전히 Concurrent-GC를 대체하기 때문이다. Background-GC가 Concurrent-GC를 대체하는 것은 Concurrent-GC의 한계를 어느 정도 극복하는 수준의 향상이 이루어 졌고 나머지 부분은 다를 바가 없다.

Concurrent-GC 모드가 2세대 힙을 정리하는 동안에는 또 다른 0, 1 세대 가비지 컬렉션이 발생할 수 없다고 했었다. 그러나 Background-GC는 가비지 컬렉션 스레드가 2세대를 정리하는 동안에도 0세대 혹은 1세대 가비지 컬렉션을 허용한다. Background-GC 모드는 2세대 힙을 정리하는 동안 0, 1 세대 가비지 컬렉션이 필요하면 2세대에 대한 가비지 컬렉션을 중단하고 0, 1 세대 가비지 컬렉션을 수행하게 된다. 그림 3은 이와 같은 상황을 잘 보여주고 있다.

image
그림 3. Background-GC 모드 (만지면 커져요)

그림 3에서 가비지 컬렉션 스레드가 2세대 힙을 정리하는 동안 Thread #3이 메모리를 할당하면서 가비지 컬렉션을 유발했다고 가정하면 CLR은 가비지 컬렉션 스레드를 잠시 중단하고 0 세대 혹은 1세대 가비지 컬렉션을 수행하게 된다. 0, 1세대 힙에 대한 정리는 가비지 컬렉션을 유발한 스레드(이 경우 Thread #3)에 의해 수행되므로 가비지 컬렉션 스레드에 아무런 영향을 주지 않으며 중첩된 0, 1세대 가비지 컬렉션이 끝나면 CLR은 가비지 컬렉션 스레드의 수행을 재개하여 2세대 가비지 컬렉션을 마무리 짓는다.

Background-GC의 장점은 Concurrent-GC에 비해 어플리케이션의 응답 시간이 전반적으로 좋아질 수 있다는 것이지만 Concurrent-GC의 단점인 (짧으나마) 스레드들 중단이 자주 발생할 수 있으며 좀 더 큰 힙이 구성되기도 한다.

Server-GC

Server-GC 모드는 서버 상에서 작동되는 서버 어플리케이션들을 위한 가비지 컬렉션 모드로써 Workstation-GC와는 사뭇 다른 접근 방법을 취한다. Server-GC가 활성화 되면 CLR은 시스템 상의 프로세서(멀티 코어, 하이퍼 스레드도 고려 됨) 개수만큼의 관리되는 힙을 만들며 이 힙을 정리하는 가비지 컬렉션 스레드도 프로세서 개수만큼 생성한다. 예를 들어, 필자의 노트북처럼 쿼드 코어에 하이퍼 스레드가 적용된 컴퓨터에서는 8개의 관리되는 힙이 생성되고 이들 힙을 정리하기 위해 별도의 가비지 컬렉션 스레드가 8개 생성된다는 말이다. Server-GC 모드 상에서 가비지 컬렉션이 발생하면 모든 관리되는 스레드들을 중단한 후 가비지 컬렉션 스레드들이 각기 자신이 맡은 힙을 정리하게 된다. 가비지 컬렉션 스레드들이 모든 작업을 마치면 그 때서야 중단 되었던 스레드들의 작업을 재개하게 된다.

Server-GC 모드가 프로세서마다 고유의 힙을 생성하는 이유는 메모리 할당의 병행성(concurrency)을 높이기 위해서이다. Workstation-GC 모드에서는 2개의 스레드가 동시에 객체를 생성하고자 했을 때(메모리 할당 발생) 힙이 1개만 존재하므로 어느 한 스레드가 메모리를 할당하는 동안 다른 스레드는 이를 기다려야만 한다. 반면 Server-GC는 프로세서마다 고유의 힙이 존재하므로 메모리 할당에 중단이 발생하지 않으며 두 스레드는 병렬적으로 동시에 메모리 할당을 수행할 수 있다. 프로세서마다 존재하는 힙은 서로 다른 힙에 대한 참조를 가질 수 있으며 별다른 제약을 가지고 있지 않다. 다시 말해 논리적으로는 하나의 힙과 다를 바가 없다는 것이다. 또, 가비지 컬렉션이 발생하면 힙 마다 하나씩 할당된 고유의 가비지 컬렉션 스레드가 힙을 정리하기 때문에 병렬적으로 가비지 컬렉션을 수행할 수도 있다. 이는 동일한 크기의 힙을 Workstation-GC 모드에서 정리하는 것 보다 Server-GC 모드에서 좀 더 빠르게 힙을 정리할 수 있다는 말과 같다.

그림 4는 4개의 논리 프로세서를 가진 시스템에서 수행되는 Server-GC 상황을 보여주고 있다. 4개의 가비지 컬렉션 스레드가 존재함에 주목하자. 가비지 컬렉션이 수행됨에 따라 CLR은 관리되는 스레드들을 모두 중단하고 고이 잠들어 있던 가비지 컬렉션 스레드 4개를 모두 뚜드려 깨우며 이들 가비지 컬렉션 스레드들은 자신에게 할당된 힙에 대해 가비지 컬렉션 작업을 수행하게 된다. 가비지 컬렉션 스레드들은 실시간(real-time) 우선 순위를 제외하고 가장 높은 우선 순위를 갖기 때문에 작업 중간에 프로세서를 다른 스레드에게 뺏길 가능성은 낮지만 100% 보장할 수는 없다. 따라서 가비지 컬렉션 스레드들이 자신의 힙을 정리하는데 소요되는 시간은 약간씩 차이를 가질 수도 있다. 이 경우, 마지막 가비지 컬렉션 스레드의 작업이 완료되어야만 어플리케이션 스레드들의 수행이 재개 된다. 그림 4에서는 3번째 가비지 컬렉션 스레드가 가장 늦게 메모리 정리 작업을 마쳤기 때문에 이 이후에 어플리케이션 스레드들이 작업을 재개하고 있다.

image
그림 4. Server-GC 모드 (만지면 커져요)

Server-GC 모드는 커다란 힙을 상대적으로 빠르게 정리할 수 있다는 장점이 있지만 반대 급부도 존재한다는 점을 잊어서는 안 된다. 논리 프로세서 당 힙이 하나씩 생성되므로 32비트 운영체제에서는 물리 메모리가 충분함에도 불구하고 가상 메모리 공간이 부족할 수도 있다. 이는 어플리케이션의 메모리 할당 패턴이 잘못된 경우 충분히 발생할 수 있는 상황이다. 또, 논리 프로세서가 많다면 가비지 컬렉션 스레드가 많이 생성되므로 Server-GC 모드를 사용하는 프로세스가 많다면 가비지 컬렉션 스레드 역시 크게 늘어날 수 있다는 점을 명심하자. 이러한 Server-GC 모드의 작동 방식은 특정한 환경에서 문제를 유발할 수도 있다. 이 문제점에 대해서는 다음 포스트에서 상세히 살펴보도록 하겠다.

다음 포스트에서는……

다음 포스트에서는 Server-GC 모드를 사용할 때의 주의할 사항과 특정 상황에서 Server-GC가 전체적인 성능을 떨어뜨릴 수 있다는 사실에 대해서도 살펴보도록 하겠다. 또, 프로세스가 Workstation-GC 모드로 작동 중인가 아니면 Server-GC 모드로 작동중인 가를 알아보는 방법도 살펴볼 것이다.

원래 다음 포스트와 이 포스트는 연관성이 깊기 때문에 하나로 묶는 것이 좋지만 포스트가 너무 길어지는 것을 막기 위해 두 개의 포스트로 분리했다. 결코 포스트 개수를 늘이기 위한 꽁수는 아니다. 블로그를 개편하면서 글 쓰는 스타일을 바꾸었을 뿐이다. 나중에 시간이 되면 스타일이 어케 바꼈는지에 대해서도 노가리를 풀어볼까 한다. 어찌되었건, 다음 포스트는 이미 써 놨지만 다음 포스트가 언제 올라 올지는 필자도 모른다(필자가 x릴 때?).


경고 : 이 글을 무단으로 복제/스크랩하여 타 게시판, 블로그에 게시하는 것은 허용하지 않습니다.