가비지컬렉터 의 동작을 이해하는 건 Java 개발자의 가장 필수적인 요건이다. 나름 Java 를 할 줄 안다고 자만하고 있었는데, 이 부분을 공부하면서 깊이 반성하였다.


 C/C++ 개발자의 경우 객체를 관리하는데 있어서 메모리는 순전히 개발자의 몫이다. 필요한 시점에 생성하고 불필요한 시점에 반환해야 하며 올바른 메모리 주소를 참조할 수 있도록 관리해주어야 한다. 

반면 Java 의 경우 개발자는 이 부분에 있어서 신경을 덜 써도 되며 이는 Java 언어가 제공하는 Garbage Collector 가 이 일을 해주기 때문이다. Garbage Collector 는 항상 background 에서 데몬 쓰레드로 돌아가면서 접근 불가능한(Unreachable) 상태가 된 객체들의 메모리를 정리해준다.



 먼저 Garbage Collectors (이하 GC) 의 동작을 이해하기 위해서는 JVM 의 메모리 관리에 대해 알아야 한다. JVM에는 일반적으로 Young Generation / Old Generation 이라는 두가지의 물리적 공간이 존재한다.


 GC는 2가지 전제를 갖고 있다. 대부분의 객체가 금방 Unreachable 한 상태가 된다는 것과 Old 객체에서 Young 객체로의 참조가 적다는 점이다. 다음은 각 물리 공간에 GC 가 어떻게 동작하는지를 설명한다.


- Young Generation 영역 : 새롭게 생성한 객체가 위치한다. 많은 객체가 이 영역에 생성되었다 사라지며 이를 Minor GC라고 한다.


- Old Generation 영역 : 접근불가능한 상태가 되지않아 Young 영역에서 살아남은 객체가 이 영역으로 복사된다. Young 영역보다 크게 할당되며 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC 또는 Full GC 가 발생한다. 

Old 영역에서는 Card table이라는 512 byte의 Chunk 가 존재하며 Old영역의 객체 중 Young 영역의 객체를 참조하는 객체의 정보들을 저장한다.


 Young 영역은 Eden 영역과 2개의 Survivor 영역으로나뉜다. New 를 이용해서 객체를 생성하면 이는 Eden 영역에 위치하게 된다. Eden 영역에서 GC가 한번 발생 후 살아남은 객체는 Survivor 영역 중 하나로 이동한다. 이 때 객체가 Eden 영역에서 Survivor1, Survivor2 영역으로 이동할 때 Minor GC 가 수행된다.


지속적인 Eden 영역에서의 GC 이후 Survivor 영역으로 객체가 계속 쌓이고 Survivor 영역 내 빈곳으로 살아남은 객체들이 이동한다. 이러한 과정을 계속 반복 후 Survivor 영역들이 가득차게 되면, 남은 객체가 Old 영역으로 이동한다.


 2개의 Survivor 영역에 모두 데이터가 존재하거나 모두 사용량이 0이면 시스템은 비정상이다. Old 영역은 기본적으로 데이터가 가득차면 GC를 수행하며 GC 방식은 JDK 7 기준으로 5가지가 있다.


(1) Serial GC : Mark-sweep-compact 알고리즘이 사용된다. Old 영역에 살아있는 객체를 Mark 하고 Heap의 앞부분부터 살아있는 객체를 Sweep한 뒤, 힙의 앞부분부터 객체를 쌓는다.(Compact) 메모리와 CPU 코어수가 적을 때 좋다.


(2) Parallel GC : Serial GC와 기본적인 알고리즘은 같지만, GC를 처리하는 Thread의 개수가 여러 개다. 메모리와 CPU 코어 수가 많을수록 좋다.


(3) Parallel Old GC : Parallel GC와 같지만 Old 영역의 GC 알고리즘만 다르다. Mark-Summary-Compact 알고리즘이며 조금 더 복잡하다.


(4) CMS GC : Stop-the-world 이후 Initial marking 시 살아있는 객체만 찾는다. 이후 concurrent mark 단계에서 참조를 따라가며 새로 추가되거나 참조가 끊긴 객체들을 remark 한다. 모든 작업이 멀티스레드 환경에서 동시진행되기 때문에 stop-the-world 시간이 매우 짧은 대신 memory와 CPU 를 많이 사용하고 compaction 단계가 제공되지 않는다.


(5) GI GC : Young 영역과 Old 영역이 없이 매우 빠르게 객체를 할당하고 GC한다.



 GC가 실행되기 전 JVM은 Application의 실행을 멈춘다. 이를 Stop-the-World 라 하며 GC 쓰레드 이외의 쓰레드들의 작업이 멈춘다. 이 때 이 Stop the world 를 줄이는 작업을 GC 튜닝이라 한다.


GC 튜닝이 모든 Java Application 에 필수적인 것은 아니지만, 크리티컬한 요청을 담당하는 서버나 코어 엔진의 경우 GC 튜닝이 필요하다. GC 튜닝을 위해 지켜야할 기본적인 원칙은 GC 튜닝이 로직에 영향을 미치지 않도록 가능한 늦게 수행하고, 객체 생성을 최소화하는 것이다.


 즉, GC 튜닝은 Java 코드 최적화와 맞물려 있는 영역이라고도 할 수 있다. 가령 String 의 append 시, + 연산으로 2개 이상의 String 을 더하는 대신, StringBuilder 등을 쓰는 것도 일종의 메모리 튜닝이라고 할 수 있다. 그 외에는 설정적인 부분으로 JVM 옵션으로 메모리 크기를 조절하고 GC 방식을 옵션으로 지정해주는 등이 있다.


(1) 메모리 관련 GC 튜닝을 위한 JVM 옵션

 -Xms : JVM 시작 시 힙 영역의 크기

 -Xmx : 최대 힙 영역 크기

 -XX:NewRatio : New 영역과 Old 영역의 비율. (New 영역의 비율을 전달한다.)

 -XX:NewSize : New 영역의 크기

 -XX:SurvivorRatio : Eden 영역과 Survivor 영역의 비율 (Survivor 영역의 비율을 전달한다.)


(2) GC 방식 관련 GC 튜닝을 위한 JVM 옵션

 - Serial GC :

-XX:+UseSerialGC 


 - Parallel GC : 

-XX:+UseParallelGC

-XX:ParallelGCThreads=value


 - Parallel Compacting GC : 

-XX:+UseParallelOldGC


 - CMS GC :   

-XX:+UseConcMarkSweepGC 

-XX:+UseParNewGC

-XX:+CMSParallelRemarkEnabled

-XX:CMSInitiatingOccupancyFraction=value 

-XX:+UseCMSInitiatingOccupancyOnly

 - G1 : 

-XX:+UnlockExperimentalVMOptions

-XX:+UseG1GC


위와 같은 옵션을 적용한다고 해도 GC 의 튜닝이 비약적인 시스템의 성능 향상을 가져오게 되리라고 보장할 수는 없다. 결국 GC 튜닝은 계속적인 모니터링을 통해서 이루어지는 것으로, 복잡한 시스템의 메모리 구조를 공식처럼 맞춰서 설정하기 보다는 지속적인 모니터링과 성능 향상을 위한 노력이 필요하다.



+ Recent posts