프로그래밍을 배우신 분들 중에 C언어를 배우고 JAVA를 배운 분들이라면 많이들 공감하시는게 '편하다' 일 것입니다.
왜 그럴까요? C언어에서는 malloc 함수로 메모리를 할당해주고, 작업이 끝날때는 무조건 free 함수로 메모리 할당을 해제해줘야했는데 JAVA는 그런게 없죠. 이는 JAVA를 구동하는 자바 가상 머신(JAVA Virtual Machine) 내부에서 작동하는 가비지 컬렉터가 있기 때문입니다. 이 가비지 컬렉터는 말 그대로 쓰레기 수집가 즉, 우리가 사용이 끝난 메모리 공간을 쓰레기로 인식해서 메모리를 자동으로 해제해주는 기능입니다.
오늘은 이 가비지 컬렉터의 작동 원리에 대해서 알아볼 것입니다. 어떤 데이터를 쓰레기로 인식해서 메모리를 해제하는지, 언제 작동하는지 등을 볼 것입니다.
우선 자바의 메모리 구조에 대해서 알아보겠습니다.
실제로는 메소드 영역, 스택영역 등의 더 많은 메모리 공간이 있지만 지금은 가비지 컬렉터를 중점으로 알아보기 위해 힙 메모리를 중점적으로 보겠습니다.
가비지 컬렉터는 힙 메모리에 있는 참조하지 않는 데이터의 메모리를 해제합니다. 일반적으로 가비지 컬렉터의 구조는 Young Generation과 Old Generation 으로 나뉘어집니다. 아래는 각 영역에 대한 간단한 설명입니다.
GC 동작은 크게 2가지 GC로 나뉘어집니다.
마찬가지로 Old Generation 영역이 가득 차거나 System.gc() 메소드가 실행되면 Major GC가 발생합니다. Major GC는 "Stop The World" 라는 이벤트가 발생하는데, 이는 GC가 발생한 스레드를 제외한 나머지 스레드가 정지됩니다. 나머지 스레드가 정지된다는 것은 애플리케이션이 아무런 동작을 하지 않는다는 의미입니다. 특히 System.gc()를 호출하는 것은 시스템 성능에 매우 큰 영향을 끼치기 때문에 System.gc() 메소드는 절대로 사용하면 안됩니다.
그러므로 JAVA 프로그램의 성능을 향상시키기 위해서는 이 "Stop The World" 이벤트를 적게 발생시키는 것이 관건입니다.
Major GC가 발생할 때 나머지 스레드를 정지시키는 이유는 Major GC는 존재하는 모든 데이터를 검사하기 때문입니다.
Major GC는 Minor GC와는 다르게 여러가지 방식이 있습니다. (JAVA 7 기준)
하나하나 설명하기에는 글이 너무 길어지기때문에 Major GC 방식에 대해서는 차후에 하나하나 설명하도록 하겠습니다.
아까부터 우리는 unrechable한, 도달하지 못하는 데이터가 가비지 컬렉터에 의해 메모리가 해제된다고 했습니다. 그럼 가비지 컬렉터는 unrechable한 데이터를 어떻게 찾을까요?
위는 간단하게 reachable한 데이터와 unreachable한 데이터의 구조를 나타낸 그림입니다. 가비지 컬렉션을 다른 말로 Mark and Sweep이라고 부릅니다. 그럼 위 그림의 GC Root는 무엇일까요? 눈치 빠른 분들은 눈치채셨겠지만 Garbage Collection Root 입니다. 가비지 컬렉션의 Root 라는 의미입니다. 아래에서 GC Root 에 대해서 좀더 자세하게 설명하겠습니다.
출처 : 네이버 D2
GC Root가 될 수 있는 것은 다음 3가지입니다.
추가로 설명을 하자면 JNI의 경우 자바 내장 클래스의 메소드 중 native 지시자를 가진 메소드를 의미합니다. native 지시자는 자바로 만들어진 코드가 아니고 다른 언어(주로 C언어)로 만들어진 메소드를 의미합니다.
그럼 다시 돌아와서 GC Root가 될 수 있는 저 3가지에서 Root에서 참조하는 각 데이터들을 추적하여 추적된 데이터는 reachable한 데이터이므로 mark 하고 unreachable한 데이터는 sweep, 메모리 해제를 합니다. 이것이 위에서 말한 Mark And Sweep 입니다.
우리는 위에서 힙 메모리의 Yound Generation의 Eden 영역, OldGeneration 영역이 가득 차면 GC가 발생한다는 것을 알게 되었습니다. 또한, 특정 그러면 이 힙 메모리의 크기가 커지면 가비지 컬렉터가 덜 자주 발생하고, 작아지면 더 자주 발생하지 않을까요? 이런건 어떻게 조절할까요?
우리는 JAVA를 빌드하여 jar파일로 만들고 이 파일을 실행할 때 java 명령어를 사용합니다. 이때 우리는 옵션으로 힙 메모리 크기를 조절하거나 위에서 말한 Major GC의 종류를 변경할 수 있습니다. 이를 JVM 옵션을 통한 성능 튜닝이라고 부릅니다.
아래 표에서 옵션들 중 일부를 볼 수 있습니다.
VM Option | 설명 |
---|---|
-Xms | JVM이 시작할 때 초기 heap 사이즈를 설정 |
-Xmx | heap의 최대 사이즈를 설정 |
-Xmn | Young 영역의 사이즈를 설정한다. 나머지는 Old 영역사이즈로 설정됨. |
-XX:PermGen | Permanent 영역의 초기 사이즈를 설정 |
-XX:MaxPermGen | Permanent 영역의 최대 사이즈를 설정 |
-XX:SurvivorRatio | Eden 영역과 , Survivor 영역의 비율을 설정. 예를 들어 Young 영역의 사이즈가 10m 인데, -XX:SurvivorRatio=2 로 설정하면 Eden 영역에는 5m가 설정되고, Survivor 영역은 각각 2.5m씩 할당됨. 기본값은 8이다. |
우리는 오늘 자바의 가비지 컬렉터가 어떻게 동작하는지, 어떤 종류가 있는지, JVM의 옵션으로 어떻게 성능을 향상시킬 수 있는지 알아보았습니다. 이걸 왜 알아야 하느냐고 하시는 분이 간혹 가다 있는데, 우리는 이전에 String에 대해서 공부할 때 String 객체에 빈번하게 값을 추가하면 새로 객체를 만든다고 배웠습니다. 그럼 그만큼 가비지 데이터들이 잔뜩 쌓여서 그만큼 자주 GC가 실행될것입니다. 이는 소규모 어플리케이션에서는 상관 없지만, 사용자가 수만, 수십만, 수백만인 대규모 서비스에서는 이런 것 하나하나가 성능 이슈를 야기합니다. 따라서 우리는 개발자의 좋은 발자취인 좋은 코드를 남기기 위해서 이런걸 알고 실제 코드에 녹여야 한다고 생각합니다.
긴 글 읽어주셔서 감사합니다.