기본 콘텐츠로 건너뛰기

JVM Garbage Collector 종류와 동작방식

시스템이 운영되다 보면 성능개선을 해야 할 때가 생긴다. 새로 코드를 구현하는 방법이 제일 좋겠지만 가비지 컬렉터(Garbage Collector)를 튜닝 하는 것도 성능개선에 많은 도움이 된다. 튜닝을 경험해볼 일은 없어보이지만 평소 모호하게 알고있었던 내용이기도 해서 JVM가비지 컬렉터 종류와 어떤 일을 하는지 정도를 정리해 둠.


GC 개요


GC는 더이상 사용되지 않는 객체를 찾는 작업과 메모리에서 제거하는작업으로 나뉘는데, JVM은 더는 참조되지 않는객체를 찾아 메모리에서 제거하는 작업을 할 때 힙(heap)을 주기적으로 검색한다. 그런 후 GC 대상이 되는 객체가 차지했던 메모리를 다른 객체가 쓸 수 있게 메모리에서 비우게 된다. 이때 Compacting이라는 작업도 함께 이루어진다. Compacting은 메모리 파편화(memory fragmentation)가 생기지 않게 남은 공간을 잘 합쳐두는것을 말한다.

Compacting (출처: dynatrace.com)

Stop-the-world

자바는 멀티 쓰레드 환경인데 쓰레드는 어플리케이션에서만 쓰는것이 아니라 GC도 특정 쓰레드에서 실행이 된다. GC 쓰레드가 동작할 때는 모든 애플리케이션 쓰레드는 동작을 멈춘다. 때문에 Full GC가 생기면 시스템이 멈추게 된다. 그렇기 때문에 GC가 동작될때 애플리케이션 쓰레드가 멈추는 시간을 최소화하는 것이 GC 튜닝의 기본이라고 한다.

Young Generation과 Old Generation

가비지 컬렉터는 Young Generation과 Old Generation으로 나눠서 작업한다.(Young Generation은 또 edan영역과 survivor영역으로 나뉜다) 이렇게 나누는 이유는 GC 시간을 최소화 하는것과 관련이 있다. 처음에 객체는 Young Generation에 할당된다. Young Generation이 가득차면 가비지 컬렉터가 애플리케이션 쓰레드를 멈추고 사용되지 않는 객체를 찾아 Young generation에서 제거한다. 이 과정을 Minor GC라 한다.


자바 코딩을 할 때 일반적으로 아래와 같이 객체를 많이 생성하게 된다.
List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

Gabage Collector는 Generation을 구분해서 동작하도록 디자인 되었다. 그래서 위처럼 임시객체가 많이 생성되는 상황에서 이런 디자인은 이점을 가질 수 있다. Generation도 힙(heap)의 한 부분이기 때문에 영역이 구분되면 GC가 힙 전체를 대상으로 실행되지 않아서 GC 시간이 짧기 때문이다. 그리고 객체는 Young Generation에서 edan이나 Old Generation으로 옮겨지게 된다. 옮기는 과정에서 Young Generation이 자동으로 정렬(compaction)되어 메모리 단편화가 해결된다.

그렇지만 Old Generation에서 GC는 다르다. Old Generation에서 쓰이지 않는 객체를 찾아서 메모리에서 제거하고 힙을 정렬(compact)하는 절차를 Full GC라고 한다. Full GC때는 애플리케이션 쓰레드가 멈추는 시간도 길다. 그래서 Concurrent 컬렉터인 CMS와 G1 컬렉터는 애플리케이션 쓰레드가 동작중에 사용되지 않는 객체를 찾는다. 그리고 Concurrent 컬렉터는 Old Generation을 정렬(compact)하는 방법도 다르며, CMS나 G1 컬렉터를 쓰면 애플리케이션 쓰레드가 멈추는 시간을 최소화 할 수 있다. 그렇지만 CPU 사용률은 높다.

GC 알고리즘

Serial Garbage Collector

32-bit Windows나 싱글 프로세스 머신에서 기본 컬렉터로 사용되며 Full GC 할 때 Old Generation 영역 전체를 정렬(compaction)한다.

Throughput Collector

서버머신로 사용되는 multi-CPU 유닉스 머신이나, 64-bit JVM 머신에 사용되는 기본 컬렉터다. Young Generation과 Old Generation 모두 멀티 쓰레드가 사용되며 JDK 7u4이후에는 기본동작된다. 그리고 멀티 쓰레드를 사용하기 때문에 Parallel Collector 라 부른다.


이 컬렉터는 Minor GC와 Full GC 모두 애플리케이션 쓰레드를 멈추며, Full GC 때는 Old Generation 영역을 전체 정렬(compaction)한다. 그리고 대부분의 상황에 사용되는 기본 컬렉터이기 때문에 기본으로 활성화되어있다.

CMS 컬렉터

CMS 컬렉터는 Serial/Throughput 컬렉터가 Full GC 때 Long Pause 되는걸 막기 위해 디자인 되었다. Full GC 동안 애플리케이션 쓰레드를 멈추는 대신 하나 이상의 백그라운드 쓰레드가 주기적으로 Old Generation을 스캔하고 사용되지 않는 객체를 제거한다. 그리고 동일하게 Minor GC 때 애플리케이션 쓰레드 전체를 멈춘다.


그렇지만 CPU 소모량이 많고, 백그라운드 쓰레드는 정렬(compaction)을 하지 못하기 때문에 메모리 단편화가 생긴다. 그리고 CMS 백그라운드 쓰레드가 작업을 완료할 만큼 충분한 CPU 얻지 못하거나 힙에 메모리 단편화가 많으면 CMS는 Serial Collector로 동작한다. 즉 싱글 쓰레드를 사용해 전체 애플리케이션 쓰레드를 멈추고 Old Generation 영역을 정리한다. 그런 후 작업이 끝나면 다시 CMS 컬렉터로 동작한다.

G1(Garbege First) 컬렉터

4GB가 넘는 큰 힙을 처리하기 위해 디자인 된 컬렉터다. 동일하게 Pause를 최소화 하는 Minimal Pause 이며, 힙을 여러 영역(region)으로 나누어 GC한다.

Young Generation을 여러개로 영역으로 나누고 다른 컬렉터와 동일하게 애플리케이션 쓰레드를 멈추고 GC한다.

Old Generation은 백그라운드 쓰레드로 애플리케이션 쓰레드를 멈추지 않고 GC하며 GC하는 방식은 나누어진 영역중 하나를 다른 영역으로 복사하는 방식을 사용한다. 이 과정에서 힙이 정렬(compaction)되어 파편화가 거의 없다. 그래서 G1은 Concurrent 컬렉터라 부른다.

CMS처럼 여러 백그라운드 쓰레드가 애플리케이션 쓰레드와 동시에 동작될 수 있게 CPU cycle이 가용해야 한다. 기본 컬렉터가 아니기 때문에 별도 -XX:+UseG1GC옵션으로 실행 시켜야 한다.

CMS vs G1

CMS알고리즘은 G1보다 간단하므로 더 빠를 수 있지만 힙크기가 4GB 이상될 때 G1이 더 나은 성능을 보인다. CMS의 백그라운드 쓰레드는 객체를 메모리에서 해제하기 위해 전체 Old Generation 영역을 스캔하므로 힙크기에 영향을 받기 때문이다. 그리고 CMS는 Concurrent 모드가 되지 않으면 싱글 쓰레드로 Full GC하는 단점도 있다. G1도 역시 Concurrent 모드가 되지 않을 수 있지만, 영역이 나뉘어져 있고 여러 쓰레드가 영역별로 작업을 분담할 수 있으므로 Concurrent 모드가 실패하는 경우는 많지 않다. 두가지 알고리즘 모두 Concurrent 모드를 유지하는게 중요한 이슈가 될 수 있다.

덧붙이면 G1은 최신 알고리즘이기도 하고 Java8 에서는 성능에 이점이 많다고 한다.

결론

GC 알고리즘은 애플리케이션의 목적에 따라 선택이 되어야 한다. 가령 Serial Collector는 적은 메모리를 사용하는 애플리케이션에 적합할 텐데, 힙이 작기 때문에 Throughput Collector와 같이 병렬 GC나 백그라운드로 GC하는 CMS나 G1은 도움되지 않는다. 대부분의 어프리케이션은 Throughput Collector나 Concurrent Collector가 적합하며 성능 목표나 상황에 맞게 선택되어야 한다. CMS와 G1의 비교에서 알 수 있듯이 힙영역이 클수록 G1이 나은 선택이 될 수 있다.

댓글