2011년 3월 4일 금요일

Garbage Collection

어디선가 퍼왔음...

목차
________________________________________
1. Garbage Collection이란?
2. JVM GC 설계자의 고민 - Generation
3. JVM GC 설계자의 고민 - GC Algorithm
4. JVM의 Garbage Collector
5. Serial Collector
1. Young Generation GC
2. Tenured Generation GC
6. Parallel Collector(Throughput Collector)
1. 언제 쓰면 좋지?
7. Parallel Compacting Collector -> 보강 필요
8. Concurrent Mark-Sweep(CMS) Collector
1. Tenured Generation GC
2. CMS Collector의 안좋은 점
3. Incremental Mode
4. 언제 쓰면 좋지?
9. GC와 관련된 JVM command line option들
10. 참고 문헌 및 Web Site

Garbage Collection이란?#
program이 점유하여 쓰고 있는 memory 공간 중 더 이상 쓰지 않는 memory 영역을 Garbage라 합니다. 예전의 programming 언어(이하 PL)들은 programmer가 이 garbage를 OS에 반납하는 code를 일일히 작성해야 했습니다(C 좀 하셨던 분들은 malloc()과 free() 함수를 기억하실 것입니다). 그런데 사람이 원래 '화장실 가기 전과 화장실 가고 난 후가 다르다'고, 반납하는 code 작성을 빼먹고 그랬습니다. 그러면 그 program이 도는 machine의 momory가 야금야금 줄어드는 현상이 발생하고(전문 용어로 memory leak이라 하는데, 이런 문제는 일정 시간이 경과해야 발견할 수 있는, 참 찾기 어려운 bug입니다) 급기야는 그 machine의 memory과 꽉 차서 system이 뻗는 현상이 발생합니다.
현대적인 언어들은 이러한 성가신 작업을 자동으로 해 주고 이를 바로 Garbage Collection(이하 GC)이라고 합니다. 그리고 Java도 바로 그 현대적인 PL에 들죠! 아싸~! 바로 JVM 덕분입니다.
그런데, 그런데 말이죠, 'There's no silver bullet in the world'라고, 이 성가신 작업을 programmer들이 안해도 되긴 하나 결코 만능은 아니라는 거~! GC를 자동으로 하는 것은 좋은데, GC를 수행할 때 JVM 상의 program들 수행을 모두 멈추고 GC를 수행하므로 program 성능이 참 나쁘게 하는 부작용도 있습니다. 그래서 JVM의 GC를 잘 안 나게 하고, 나더라도 금방 되게 하는 것이 중요한 성능 향상 수단이 됩니다.



JVM GC 설계자의 고민 - Generation#

위 graph가 표시하는 것이 뭘까요? 결론을 말하면, 생긴 지 얼마 안된 객체는 대부분이 얼마지 않아 필요 없어져서 GC 대상이 되고, 오랫동안 살아남는 객체는 적다 되겠습니다. 즉 생긴 지 얼마 안 된 객체들은 GC 수행을 자주 해야 하고, 생긴 지 좀 객체들은 그래도 GC 수행을 자주 할 필요가 없단 말이 되죠.

그래서 JVM 설계자는 JVM의 Heap 영역을 두 부분으로 나누고 한 부분은 생긴 지 얼마 안 된 객체들을 살게 하고(이 부분은 자주 GC를 해 줘야겠죠?) 한 부분은 오래 산 객체들이 모여 살도록 해서(자주 GC를 할 필요는 없죠) - 그러고 보니 정말 Young, Tenured란 이름이 걸맞네요 - 앞 부분은 빠른 수행이 가능한 GC algorithm을 적용하고 뒷 부분은 오래 걸리긴 해도 꼼꼼하게 GC하는 algorithm을 적용하기로 합니다. 그리고 Java 진영에서는 이러한 것을 Generational GC라 부르죠.

그러면, 이 Generational GC는 어떤 장/단점이 있을까요?
• 장점: 위와 말한 바와 같이 각 Generation에 대해 효율적인 GC algorithm 적용이 가능합니다.
• 단점: Java Error 중 유명한 것으로 OutOfMemoryError(흔히 OOM이라고 많이 합니다)가 있는데, 바로 JVM에 더 이상 객체를 생성할 수 없을 정도로 빈 공간이 없을 때 나는 Error입니다. 그런데 이 Error가, 예를 들어 전체 64MB memory가 있는데 이 영역을 다 써서 나는 게 아니라 Young이나 Tenured 영역 중 하나가 다 차 버려도 OOM이 발생합니다. 예를 들어 Young 영역은 널럴해도 Tenured가 꽉 차면 OOM이 나 버리죠.

여하튼 이러한 이유로, JVM은 memory를 다음과 같이 영역을 구분하여 씁니다.

• Young: 생긴 지 얼마 안되는 Java 객체(Object)들이 생기고 사는 곳입니다.
• Tenured: 생긴 지 좀 된 Java 객체들이 사는 곳이죠.
• Perm: Permernemt의 줄임말로, Java 함수의 몸체 부분이나 static 변수 같은 변할 일이 없는 것들이 사는 영역입니다.


JVM GC 설계자의 고민 - GC Algorithm#

위 그림은 또 뭘까요? GC algorithm의 분류 체계라 말할 수 있을 것인데요, 각각을 설명하면 다음과 같습니다.

• Serial / Parallel: GC 대상을 하나의 Garbage Collector 혼자서 모든 garbage를 차례대로 다 치우느냐(Serial), 여러 개의 Garbage Collector가 나눠서 함께 치우느냐 되겠습니다. CPU가 여러 개인 system에서 Serial GC는, 비록 CPU가 여러 개 있어도, garbage collector가 1개만 돕니다. 그런데 Parallel GC는 여러 개가 돌죠. Parallel GC가 당연히 빠르겠지만, 아무래도 좀 더 복잡하고 잠재적으로 memory 단편화를 더 일으킬 수는 있습니다. (일반 PC 쓰는 분들은 Parallel GC는 나랑 해당 사항 없겠네 하시겠지만, 요새 dual core CPU가 일상화되었으니 Parallel GC가 해당 사항이 있을 수도 있겠죠?)
• Stop-the-world / Concurrent: Stop-the-world는 GC 수행할 때 program의 수행을 죄다 멈춘 후 하고, Concurrent는 program 수행을 안 멈추고 GC와 program 수행을 같이 합니다. 옛날 JVM은 stop-the-world였기 때문에 GC 하면 죄다 program 수행을 일단 정지 시키므로, 성능 나쁘게 한다고 악명이 높았죠. 여하튼 Concurrent가 일견 더 좋아 보일 수 있으나, GC 수행 중 같이 도는 program이 쓰레기를 만들어 놓고, 그리고 그 쓰레기를 치우지 못할 개연성은 있습니다.
• Compacting / Non-Compacting / Copying: 쓰레기를 수집하고, 그 쓰레기가 있던 자리는 빈 공간으로 남습니다. 그리고 이런 짓거리를 여러 번 하다 보면, memory는 여기 저기 구멍 난 누더기처럼, 빈 공간과 Java 객체가 쓰고 있는 공간이 여기저기 널려있게 됩니다. 이를 단편화(fragmentation)라 하는데, GC를 하는 김에 이런 단편화 제거도 싸그리 하여 연속된 빈 공간을 확보하는 작업도 할 필요가 있습니다. 왜냐하면 연속된 빈 공간이 있어야 큰 Java 객체가 생겨도 빈 공간 찾아 해메는 시간을 줄이거든요. 이러한 단편화 제거를 compacting이라 하고, compacting은 GC하여 쓰레기를 날린 후 compacting을 하는 것이고, non-compacting은 배째라 하고 안해버리는 것이죠. non-compacting도 나름 타당한 것이, compacting에도 분명 시간이 걸리므로 전체적인 GC 시간을 길게 하는 요소기는 하거든요. 대신 memory의 누더기 현상은 피할 수 없고, 큰 Java 객체라도 만들어야 하면 빈 메memory 공간 찾느라 시간 좀 걸리겠죠. copying은 GC 뒤 살아남은 객체를 옮겨서 compacting을 하기 보다는 그 객체를 특정 영역으로 복사하고, 원본 객체는 쓰레기로 간주하여 추후에 해제되게 하는 방식의 GC법입니다. 영역을 정해놓고 영역 전체를 무차별적으로 날려버리는 식의 쓰레기 수집이 가능하고, 그럼으로써 연속된 공간도 아울러 확보하는 것이 장점이나, 아무래도 복사하는데 시간이 들고, 살아남은 객체와 원본 객체라는 두 벌의 객체가 존재하므로 memory 사용량이 다른 방식에 비해 좀 큰 게 사실입니다.


JVM의 Garbage Collector#
Sun Microsystems가 배포하는 Java SE 5 update 6 이후 version의 JVM에는 4개의 Garbage Collector가 있습니다.


이 중 기본적으로 쓰이는 Collector는 Serial Collector입니다. 그럼, 각각을 살펴볼까요?


Serial Collector#
Serial Collector는 Serial, Stop-the-world 방식의 collector입니다. 그리고 java command line option으로 -XX:+UseSerialGC를 주면 JVM은 이 collector를 씁니다. 그런데 young generation과 tenured generation을 처리하는 방식이 좀 틀립니다. 이 각각을 자세히 살펴보겠습니다.

Young Generation GC#
serial collector는 young generation GC에 copying 방식을 씁니다. 즉 serial collector가 young generation을 GC하는 방식은 serial, stop-the-world, copying 방식이라 할 수 있죠.

• 자바 객체는 Eden 영역에 태어납니다. 이를 memory 할당(allocation)이라 합니다. 즉 Eden은 모든 Java 객체의 고향이죠.

• Eden 영역이 간당간당해져서 GC 필요성이 생기면 살아야 할 놈은 S0으로 복사합니다.

• Eden 영역을 다 날립니다(사실은 이때 S1 영역도 같이 날립니다)!

• 또 Eden에 Java 객체들이 태어납니다.

• 그러다 또 Eden이 간당간당하면 이번엔 살아야 할 놈들을 S1으로 복사합니다. 이 땐 S0에서도 살아야 할 놈들은 S1으로 복사합니다.

• 또 한 번 Eden과 S0를 다 날립니다(으... Java 객체의 대규모 학살...)!

음.... 정말 Survivor 영역은 이름 그대로 생존자가 사는 곳이군요.... 보시다시피 Eden 영역은 말 그대로 Java 객체가 처음으로 태어나는 고향 같은 곳(성경에 따르면 최초의 인류인 아담과 하와가 살던 곳이 Eden이죠?), Survivor 영역은 몇 번의 대학살(?) 끝에 살아 남은 Java 객체들이 사는 곳으로 용도가 구분되어 있습니다. 어쨌든 위와 같이 S0, S1을 번갈아 가며 말 그대로 생존자를 끌어모으고 나머지 영역은 다 날리는 식으로 GC를 수행합니다.

또한 이렇게 S0, S1을 왔다갔다하면서 질기게 생존하는 Java 객체들은 어느정도 와리가리를 하면 Tenured 영역으로 옮겨가는데 이를 승진(promotion)이라고 합니다. Tenured 영역으로 넘어갔다는 이야기는 어느 정도의 생존성을 보장 받았다는 이야기니 승진이라고 봐도 되겠죠?

Tenured Generation GC#
Serial Collector의 Tenured 영역을 위한 GC algorithm은 Mark - Sweep - compact란 방식을 취하는데, 이는 글자 그대로 먼저 Java 객체를 주욱 훑어 쓰레기에는 표시를 하고, 그 다음 쓰레기로 표시한 Java 객체가 차지하는 memory 영역을 해제한 다음, 살아남은 객체들을 한 곳으로 싹 몰아 연속된 큰 빈 공간을 확보하는 방식입니다.
그럼 이것도 차근차근 GC되는 과정을 살펴보도록 하겠습니다.

mark phase에서는 쓰레기를 식별하여 표시하고(mark), sweep phase에서는 표시한 쓰레기를 날린 다음, compact phase에서는 Tenured 영역의 시작으로 살아남은 객체를 좌악 이동시켜서 compaction을 합니다(이를 sliding compaction이라 합니다. 말 그대로 살아남은 객체가 스르륵 미끄러지듯 이동하죠?)


Parallel Collector(Throughput Collector)#
Serial Collector의 young generation GC 기능 개선판으로, 간단하게 말하면 collector를 하나만 돌리는 것이 아니라 한꺼번에 여러 개 돌려 시간 단축을 꾀합니다. multiprocessor system에서나 빛을 보는 방식입니다만, PC용 CPU도 dual core CPU가 나오는 상황이므로 꽤 쓸만합니다. 이 collector의 목적은, 아무래도 자주 발생하게 마련인 young generation GC 시간을 단축시켜 전체적인 program의 thoughput을 개선하기 위함입니다. 그런데 tenured generation GC는 Serial GC의 그것과 똑같으며, young generation GC의 경우도 하나하나의 collector가 수집하는 방식은 serial GC의 그것과 같습니다.

이 collector를 쓰기 위한 command line option은 -XX:+UseParallelGC입니다.



파랑 화살표가 일반 program 수행, 주황색 화살표가 collector 수행을 나타냅니다. idea는 간단합니다. multiprocessor system에서 일단 모두 program 수행을 멈추어 놓고 1개가 아닌 여러 개의 garbage collector가 돌아 쓰레기를 처리하여 수행 중단 시간 단축을 꾀합니다. 하나하나의 GC가 처리하는 Young Generation의 경우, serial collector의 young generation GC와 같습니다.

언제 쓰면 좋지?#
여러 개의 CPU를 가지고, program 수행 중단이 자주 일어나지 않아서 제약이 덜하긴 하지만, 한번 중단되면 시간이 좀 긴 상황에서 적용하면 개선 효과가 큽니다. 이런 류의 program 예는 batch 처리, 과금, 급여 처리, 과학 기술 관련 연산 등을 꼽을 수 있습니다(전부 덜 interactive한 성격의 program들이군요).


Parallel Compacting Collector -> 보강 필요#
이 collector는 Java SE 5 update 6에서부터 처음 나온 collector입니다. Parallel collector와의 차이점은 이 놈은 tenured generation을 위한 새로운 GC algorithm을 적용했다는 점이죠. 점진적으로 Parallel Collector는 이 collector로 대체할 예정이라네요. 허긴, Parallel Collector는 Tenured Generation GC는 나아진 것이 없죠. young generation GC algorithm은 Parallel Collector와 똑같으므로 넘어가겠습니다.


Concurrent Mark-Sweep(CMS) Collector#
위에서 살펴 본 collector들이 단위 시간당 최대 throughput 획득이 목적이라면, 이 collector는 빠른 응답 시간(response time)이 목적입니다. 이 목적을 달성하기 위해서 본 collector(응? bone collector?)는 Tenured Generation을 청소할 때 유발되는 Stop-The-World 시간을 최소화하는 전략을 세웁니다. 이 collector가 Young Generation을 청소하는 algorithm은 Parallel Collector와 똑같으므로 여기서는 Tenured Generation 청소 algorithm만 알아봅니다.

Tenured Generation GC#
CMS Collector의 GC는 initial mark, concurrent mark, remark, concurrent sweep의 4단계(phase)로 나뉩니다. 그러고보니 한 번 쓰레기 청소 하려고 쓰레기라고 표시하는 작업을 무려 세 번이나 하는군요. Bingo~! 표시 작업을 나눠서 함으로써 한 번 멈출 때 길게 멈추는 것이 아니고 잠깐 잠깐 멈춘 여러 번 멈춤으로써 응답 시간을 개선하는 것이 이 collector 동작 방식의 핵심입니다(전체적인 stop-the-world 시간을 짧게 하려는 것이 아닙니다! 전체 잠깐 잠깐 멈춘 시간을 모두 합산하면 다른 collector의 stop-the-world 시간보다 더 걸릴 수도 있습니다).

그러면 각각을 찬찬히 뜯어먹어 보가써~요! 먼저 아래 그림과 같은, 곧 GC를 해야 하는 JVM memory 상태가 있다고 합시다.

Thread A, B는 현재 돌고 있는 program이고 heap에는 가 ~ 파 까지의 Java 객체가 존재하는 상황입니다. 이 Java 객체 중 살 놈과 죽을 놈을 판별해서 GC를 해야 하는 거죠.

• initial mark: 일단 모든 program의 수행을 중단시키고 각 program에서 직접 참조하는 Java 객체들을 살 놈으로 표시한 다음 멈췄던 program 수행을 재개시킵니다. 여기서는 Thread A, B를 멈추고 그 둘이 직접 참조하는 가 ~ 마 객체를 살 놈으로 표시한 다음, Thread A, B를 다시 수행시키죠. 모든 살아야 할 객체를 다 식별하는 것이 아니라 program들이 직접 참조하는 객체만 살 놈으로 인식하는 작업을 수행하므로 program 중단 시간이 짧죠.


• concurrent mark: Initial Mark 후 다시 표시 작업을 수행하는데 이 때는 program을 안 멈추고 표시 작업을 합니다. 그 다음 initial mark 때 살 놈으로 표시당한 객체들을 주욱 훑으면서, 이 객체들이 참조하는 다른 객체들을 살 놈으로 표시합니다. 이 때는 program 수행을 중단하지 않지만, 부작용(side effect)이 있긴 있습니다. 위 그림에서는 바로 6번과 하 객체 같은 것들인데요, program을 중단시키지 않으므로 concurrent mark 작업 하는 사이에 GC 이후에도 살아남아야 할 객체가 생길 수가 있습니다. 이래서 remark 단계가 더 있는 거죠.


• remark: Concurrent Mark 동안 생긴, 살아남아야 할 객체들을 식별하는 단계입니다. 이 단계는 살 놈, 죽을 놈을 구별하는 최종 단계로, 이 때에는 표시하는 동안 살아남아야 할 객체가 또 생기면 안되므로, 모든 program을 중단시키고 표시 작업을 합니다. 위 그림에서 회색으로 표시한 자 ~ 파 객체는 최종적으로 쓰레기로 표시된 객체를 나타냅니다.

• concurrent sweep: 쓰레기를 싹 날리는 단계입니다. program 수행을 중단하긴 하지만 Collector가 여러 개 돌면서 날리므로 속도가 빠릅니다.

어떨 땐 collector 여러 개를 한꺼번에 돌리고, 어떨 땐 하나만 돌리고, 어떨 땐 program을 죄다 멈추고, 어떨 땐 program을 안 멈추고... 헷갈리죠? 아래 그림을 보시면 좀 명확할 겁니다.

파랑 화살표가 일반 program 수행, 주황색 화살표가 collector 수행을 나타냅니다. stop-the-world pause가 자주 발생하긴 하지만 멈추는 시간 자체는 짧죠?

CMS Collector의 안좋은 점#
세상에 공짜가 어딨나요, 이 collector의 안 좋은 점도 살펴봅시다.

1. 다른 collector는 표시를 수집 한 번 하려면 표시 작업을 한 번 하지만, 이 collector는 세 번이나 표시합니다. 짐이죠.
2. CMS Collector는 쓰레기를 수집하여 memory를 해제하지만, 다른 collector와는 달리 compaction을 하지 않습니다. 그래서 memory 단편화는 필연적입니다. 또한 tenured 영역으로 어떤 객체가 승진되어 오면, 이 객체 덩치가 들어갈만한 빈 공간을 찾느라 시간이 걸리는 짐이 더 생깁니다. 무엇보다도, 다른 collector들은 compaction을 하므로 빈 공간을 인식하려면 빈 공간의 시작 주소만 알면 되지만, 이 collector는 진짜 Free List라는 것을 유지해야 합니다. 이 또한 짐이죠.
3. 이 collector는 다른 collector보다 heap 공간을 더 먹습니다. concurrent mark 시 다른 program들이 안 멈추고 돌기 때문에 계속 객체가 생길 수 있고, 그러다보니 tenured 영역으로 객체가 승진할 수 있기 때문에 실제로 memory가 꽉 차서 GC를 수행하다가는 이런 상황이 발생할 때 대책이 안 설 수 있습니다. 또한 program이 돌면 새 객체만 생기나요? 쓰던 객체 더 이상 안 쓸 수도 있잖아요? 역시 concurrent mark 시 기껏 살 놈으로 표시했더니만, 고 사이에 실제로는 쓰레기가 되는 객체도 있긴 있습니다. 이런 놈들을 floating garbage(붕 뜬 쓰레기로 번역하면 딱이겠는걸요~)라 부르고, CMS collector는 floating garbage도 생길 개연성이 충분하다는 점입니다. 결론은, 이러한 전차로 이래저래 CMS collector는 다른 collector보다 여유 공간이 더 필요합니다.
4. 이 collector는 memory 단편화가 생긴다고 했죠? 이 단편화 뒤치다꺼리를 하려면, CMS collecotor는 객체의 평균 크기를 추적하고, 향후 memory 요구 사항을 추정하고, 간간히 free list를 주욱 훑어서 free block을 합치거나 나누거나 하는 작업들도 해 줘야 합니다.

아까도 말씀드렸듯, 이 Collecor는 다른 collector와는 달리 "memory가 꽉 찼네? 슬슬 GC해 볼까?" 했다가는 대책 안 섭니다. 만약 memory가 꽉 차 버리면, 제 아무리 CMS Collector라도 Serial Collector나 Parallel Collector 등이 GC하는 방식이자, stop-the-world 시간이 많이 걸리는 방식인 Mark-Sweep-Compact 전술을 써야 하죠(실제로 씁니다). 이런 불상사(?)를 막으려면 CMS Collector는 이전의 GC 시간 및 얼마나 오랫동안 Tenured 영역이 점유당했는지를 감안한 통계에 기초하여 GC를 개시합니다. 또한 Tenured 영역이 일정 비율 이상 차면 GC를 수행하는 방법도 있는데, JVM의 command line option인 -XX:CMSInitialOccupancyFraction=으로 조정 가능합니다. 은 점유 당한 Tenured 영역의 precentage를 나타내는 양의 정수를 쓰며 default는 68입니다(즉 68%의 tenured 영역이 차면 GC를 수행하는 것이 default라는 거죠).

Incremental Mode#
CMS Collector의 약점을 살펴보면, concurrent mark가 문제(?)라는 것을 알 수 있습니다. remark가 필요한 것도, 그리고 floating garbage가 생기는 것도 program을 안 중단시키고 표시를 하다 보니 야기되는 문제입니다. 그렇다면 concurrent mark 상태를 길게 가져가는 것보다 짧게 가져가는 것이 이러한 골아픈 문제를 덜 일으키겠죠? 그래서 JVM은 incremental mode라는 것을 도입했는데, 이 mode는 concurrent mark 상태를 간간히 멈추고 program들에게 처리를 양보합니다. CMS Collector는 시간을 잘게 나누어서, young generation GC 사이사이에 틈틈히 concurrent mark를 수행하도록 schedule합니다(음, 일종의 시분할 scheduling이군요). 이 mode는 특히 CPU 갯수가 1, 2개 정도 밖에 안되는 system에서 CMS collector를 수행할 때 좋습니다.

언제 쓰면 좋지?#
여러 개의 CPU를 가지고, client에게 빠른 응답을 줘야 할 program 구동에 적합합니다. 예를 들면 Swing으로 GUI를 구성한 Desktop Application이나 JSP나 Servlet을 처리하는 프로그램들 적격이겠네요. 이런 program들은 응답 시간이 오래 걸리면 사용자가 짜증을 내거나 program이 뻗은 줄 아니까요.


GC와 관련된 JVM command line option들#


GC 관련 command line option을 Mind Map으로 그려봤습니다. Option에 대해서는 Option 이름, 의미, 그리고 default 값을 알아야 합니다. 이러한 자세한 사항은 Java HotSpot VM Options를 참조하세요.

참고 문헌 및 Web Site#
1. Java HotSpot Garbage Collection
2. Java SE 6 HotSpot Virtual Machine Garbage Collection Tuning
3. Java HotSpot VM Options
4. developerWorks: IBM Developer Kits
5. IBM JDK 6.0 Information Center
6. Java™ technology software HP-UX 11.0 & 11i
7. JVM Garbage Collection Options
8. JVM Options

댓글 없음:

댓글 쓰기