개요
C언어와 달리, JAVA에서는 메모리를 직접 건드릴 이유가 없습니다.
JAVA의 Garbage Collector가 메모리 관리를 하기 때문입니다.
다만, 메모리 주소를 사용하지 않는 것은 아닙니다.
public class ReferencePrint {
public static void main(String[] args) {
String test = "HELLO";
System.out.println(test.hashCode());
}
}
위의 코드를 실행해보면 String객체의 해싱된 주소값을 가져올 수 있습니다.
이 값은 실제 메모리 주소가 아닙니다.
JAVA에서 객체의 주소는 GC가 직접 관리하고, 해당 값을 바로 출력하지 못하도록 유도하고 있습니다.
이렇게 GC는 객체가 메모리에 실리는 순간부터 메모리 할당 해제되는 순간까지 개입합니다.
메모리 관리를 GC에게 맡기는 방식은 개발자에게 장단점을 동시에 제공합니다.
이것은 C가 메모리를 직접 관리하는 것의 장단점과 대칭됩니다.
메모리를 직접 관리하지 않아도 되니 편의성과 개발 난이도가 낮아지는 대신, 메모리를 직접 하나하나 관리하며 사용하는 강력한 기능을 GC에게 맡겨야 하는 것입니다.
이렇게 우리에게 편의성을 제공해주는 GC에게는 큰 이슈가 하나 존재합니다.
STOP THE WORLD가 바로 그것입니다.
STOP THE WORLD
STOP THE WORLD라는 키워드가 있습니다.
GC가 Major GC를 진행할 때, JVM이 모든 APP의 실행을 멈춰버립니다.
JAVA입장에서는 아마 세상이 멈춘 느낌일 것입니다.
이는 어떠한 GC를 사용하더라도 발생하는 현상입니다.
GC작업 이후 중단된 스레드를 다시 실행합니다.
JVM의 모든 스레드가 작업을 중단한다면, 여기저기서 문제가 발생할 것입니다.
realtime app
STOP THE WORLD가 길어지면 치명적인 문제가 발생할 것입니다.
만약 자율주행 자동차를 JAVA로 만들었다면?
GC가 객체를 지우는 데 1초가 걸렸는데, 그 새 무언가 앞에 튀어나온다면?
상상하기 싫은 결과가 나올 것입니다.
bank app
은행 앱을 사용중인데, 송금과 같은 작업을 하다 갑자기 멈춰버린다면?
어떠한 에러가 발생할지 예측하기 힘들고 사용자 불편을 초래할 것입니다.
위의 두 이야기는 상당히 극단적인 예시입니다.
하지만 서비스의 규모가 커지고 처리해야 하는 데이터의 양이 늘어나는 현대 IT서비스의 특성상 고려해볼 만한 특징인 것은 확실합니다.
즉, GC를 개선하기 위해서 STOP THE WORLD를 줄여야 하고, 이를 위해 GC를 알아야 합니다.
HotSpotVM에서의 GC
HotSpotVM은 가장 널리 사용되는 JVM입니다.
JVM에 대한 글 - (https://nangmandeveloper.tistory.com/3)
HotSpotVM에서 GC는 Generational Collection방식을 사용합니다.
Young Generation, Old Generation 영역을 나누어 객체를 관리합니다.
Young Generation에서는 Object를 Allocation합니다.
Old Generation에서는 Young Generation에서 성숙된 Object들이 Promotion됩니다.
즉, 객체가 처음 할당될 때는 Young Generation으로. 그리고 객체가 많이 쓰인다면 Old Generation으로 넘어가는 방식입니다.
JVM은 멀티스레드 환경이기 때문에, Young Generation에 객체를 Allocation하면 문제가 발생할 것입니다.
HotSpot JVM은 메모리 할당 효율을 높이기 위해 Bump the Pointer기법을 사용합니다.
Bump the Pointer기법이란, 할당된 메모리 바로 뒤에 메모리를 할당하는 기법입니다.
멀티스레드 환경에서 사용한다면, 동기화 이슈가 발생할 것입니다. 같은 부분에 객체를 할당하는 경우가 있을 것입니다.
그래서 TLAB(Thread Local Allocation Buffer)라는 방법이 추가되었습니다.
스레드마다 할당 가능한 주소의 범위를 부여하고, Lock없이 여러 스레드가 Heap을 사용하는 방식입니다.
동기화 이슈를 완벽히 해결하지는 못했지만, 이전 방식보다 오버헤드가 대폭 줄어들었을 것입니다.
또, GC의 대전제 2가지가 있습니다.
1. 객체는 대체로 새로 생기자마자 Garbage가 된다.
2. 오래된 객체가 새 객체를 참조하는 일은 드물다.
여기에 대해서는 oracle 공식 문서를 참고하는 것이 더 좋을 것이라 생각합니다.
HotSpot Virtual Machine Garbage Collection Tuning Guide
One strength of the Java SE platform is that it shields the developer from the complexity of memory allocation and garbage collection.
docs.oracle.com
이러한 가설에 따라, GC가 메모리를 관리하는 방법이 최적화되었습니다.
HotSpot VM에서 Heap메모리는 이렇게 이뤄져 있습니다.
그림에서 오해할 만한 부분이 있습니다.
Permanent Generation은 Heap영역이 아닙니다.
Heap영역이라 주장하는 글, Heap영역이 아니라 주장하는 글들이 많이 있었습니다만
https://www.oracle.com/webfolder/technetwork/tutorials/mooc/JVM_Troubleshooting/week1/lesson1.pdf
26page, Contiguous with the Java Heap(힙에 포함된다가 아닌, 힙에 접촉해 있다로 표현)
https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html
Using JConsole - Java SE Monitoring and Management Guide
Chapter 3 Using JConsole The JConsole graphical user interface is a monitoring tool that complies to the Java Management Extensions (JMX) specification. JConsole uses the extensive instrumentation of the Java Virtual Machine (Java VM) to provide informati
docs.oracle.com
위의 두 문서에서 permanent generation은 heap이 아니라 표현되어 있습니다.
(틀렸다면 댓글에 피드백 부탁드립니다.)
Permanent Generation는 Method Area이고, 여기에 JVM class loader가 읽은 클래스, 인터페이스의 런타임 상수, 메서드 정보, static변수, 메서드의 바이트코드 등을 보관합니다.
Java 8부터는 Metaspace로 변경되었고, Heap영역에서 제외되어 Native Memory로 옮겨졌습니다.
어쨌든, HotSpot VM에서 GC가 관리하는 부분은 Heap의 Young Generation, Old Generation임을 알 수 있습니다.
그렇다면 HotSpotVM에서 GC는 어떻게 객체들을 관리할까요?
GC과정
1. Eden에 Object들이 Allocation됩니다.
2. Eden에 메모리 압박이 생기면 Minor GC를 진행합니다.
3. 참조가 살아있는 객체는 S0으로 이동하고, 참조가 없는 객체는 Eden에서 지워집니다.
4. 다음 Minor GC시, 참조가 살아있는 객체는 S1으로 이동하고, Aging됩니다.
5. S0에 남은 객체는 모두 지워집니다.
6. 다음 Minor GC가 발생하면, 참조가 살아있는 객체는 S0으로 이동하고 Aging됩니다.
7. S1에 남은 객체는 모두 지워집니다.
8. 1~7을 반복하다, 특정 Age임계에 도달한 객체는 Old Generation으로 Promotion됩니다.
9. 이렇게 Old Generation에 쌓이던 객체가 가득 차면, Major GC를 진행합니다.
10. STOP THE WORLD. GC이외의 모든 스레드가 멈춥니다.
11. Major GC 종료 이후 모든 스레드가 다시 동작합니다.
사전에 공부한 것에 비해, 상당히 간단하게 동작하는 것을 알 수 있습니다.
단순히 생각했을 때, 지금의 컴퓨터 시스템에서 메모리 크기가 증가함에 따라 문제가 발생할 것이 자명합니다.
Heap의 크기가 늘어난다면 Old Generation이 커질 것입니다.
이 때 Major GC가 발생하면 이전보다 더 많은 시간이 소모될 것입니다.
메모리 크기가 늘어났고, 프로세서 처리량이 증가했는데 STOP THE WORLD시간이 더 늘어날 수 있다니, 모순된 이야기입니다.
GC를 수행함에 있어, 이러한 부분을 해결하기 위해 많은 전략들이 존재합니다.
모든 리소스를 투입해 GC를 빨리 끝내는 방법, Suspend를 분산시키는 방법 등이 있습니다.
이런 방법을 가진 여러 GC도 존재합니다.
Serial GC, Parallel GC, Pareallel Old GC, CMS GC, G1GC, Shenandoah GC, ZGC ...
많은 GC들이 있기에 시간이 난다면 이들을 한번 훑어보는 것도 도움이 될 것입니다.
NO SILVER BULLET
언제나 그렇듯, 모든 것을 해결할 단 하나의 방법이 있는 경우는 특히 드뭅니다.
Major GC 진행시 5초, 10초씩 시간이 걸린다면 GC튜닝을 해야 할 필요성이 대폭 증가할 것입니다.
오래전 GC에 대해 동료들에게 처음 들었을 때는 GC튜닝이 필수인 것처럼 느껴졌습니다.
하지만 요즘은 GC튜닝을 하느니 리팩토링을 한 번 더 해본다 라는 이야기도 많이 듣습니다.
따라서 무언가 문제가 발생하면 하나의 해결책을 찾는 것보다 최대한 여러 경우의 수를 생각해보고, 자신의 경우에 알맞은 방법을 채택하는 능력이 필요하다 생각합니다.
그러기 위해서는 많은 학습과 경험이 필요할 것이고, 그것이 우리가 GC, GC튜닝에 대해 배우는 이유가 될 것입니다.
***수정, 문의, 기타사항은 댓글로 적어주세요.