JAVA와 C, C++의 가장 큰 차이 중 하나는 가비지 컬렉터(Garbage Collector) 이다.
JVM의 스레드는 Stack 영역에 독립된 공간을 가지고 Heap 영역은 공유한다. 그래서 가비지 컬렉터 스레드가 Heap 영역의 생성된 객체에 접근하여 제거할 수 있다. 이런 구조는 C/C++도 마찬가지이지만, C/C++은 가비지 컬렉터를 가지고 있지 않다.
왜냐하면 가비지 컬렉터는 치명적인 단점을 가지고 있기 때문이다.
가비지 컬렉터는 Mark-Sweep-Compaction 과정을 거쳐 객체를 제거한다.
Mark
전역 메모리 영역, 스택 영역의 참조변수에서 접근할 수 있는 객체는 reachable, 접근할 수 없는 객체는 unreachable로 표시(Mark)한다.
Sweep
unreachable 표시가 있는 객체는 모두 가비지 컬렉터의 제거 대상이 되어 제거된다.
Compaction
여기저기 흩어져 있는 reachable로 표시된 객체를 한 곳으로 모아 메모리 공간을 확보한다.
여기서 Compaction 과정이 문제가 된다.
reachable로 표시 된 객체는 다른 스레드가 참조하는 객체이다. 가비지 컬렉터가 객체를 compaction 과정에서 다른 메모리로 이동시킬 때, 다른 스레드가 해당 객체에 접근한다면 병행성에 문제가 발생한다. 그래서 가바지 컬렉터가 동작할 때는 다른 모든 스레드의 동작이 멈추는데, 이를 Stop-The-World (STW) 라 부른다.
여기서 문제는 언제 STW가 발생할지 아무도 모른다는 것이다.
JAVA 8의 JVM Heap 메모리 구조이다.
새로 생성된 객체는 Eden 영역에 생성된다. 그러다가 Eden 영역이 꽉차면 GC가 동작하여, unreachable한 객체는 제거하고 reachable한 객체를 Survivor1으로 이동시킨다. 그리고 생존횟수를 카운트 한다. 다시 Eden 영역에 객체가 꽉차면 Eden과 Survivor1 영역의 객체에서 unreachable한 객체는 제거하고 나머지를 Survivor2로 이동시킨다.
생존횟수가 많은 객체는 자주 사용되는 객체로 인식되어 Old Generation 영역으로 넘어간다. Old Generation 영역은 메모리 공간이 크기 때문에, GC가 자주 일어나지 않는다. 그러나 한번 일어나면 시간이 오래 걸린다. 이와 같이, 객체가 메모리 여기저기로 이동하기에 STW가 발생할 수 밖에 없는데, 문제는 Eden(Minor GC)이나 Old Generation(Major GC) 영역은 다양한 스레드에 의해 생성된 객체로 채워지므로, 언제 가득 찰지 아무도 예측할 수 없다는 것이다.
JAVA는 주로 웹 어플리케이션 개발에 사용되므로, STW에 의한 어느정도의 끊김현상은 GC가 주는 메모리 관리의 효용성과 비교하면 감수할 수 있는 정도이다. 그러나 C/C++은 이야기가 다르다. C언어는 주로 시스템 개발에 사용되고 C++은 실시간성이 중요한 게임분야에 사용된다. 그러므로 GC에 의한 끊김현상이 발생하면 타격이 크다. C/C++은 성능을 제어할 수 있고 예측가능성이 중요하기에, GC를 두지않고 개발자가 직접 메모리를 관리한다.
그렇다고 JAVA가 STW에 의한 끊김현상을 방관하고 있는 것은 아니다.
GC 성능개선
Parallel GC
GC 쓰레드를 여러 개 두어 병렬처리하는 방식이다. GC 쓰레드가 하나일 때보다 훨씬 빠른 속도로 GC가 일어난다.
CMS GC(Concurrent Mark Sweep )
STW의 가장 큰 원인인 Compaction을 제거한 방식이다. Mark와 Sweep 과정만 일어나는데, Mark 과정은 객체에 직접 접근해야 하므로, 여러 단계로 나누어 STW를 최대한 줄이고, 어플리케이션 스레드 동작과 번갈아 일어나도록 한다. 그리고 Sweep 과정은 어플리케이션 스레드가 접근하지 않는 unreachable 객체를 제거하는 동작이므로, 어플리케이션 스레드와 병행하여 동작한다.
그러나 CMS GC 방식은 여러 단계가 번갈아 동작하여 복잡하고 Compaction이 없으므로 메모리 파편화 현상이 발생한다. 그래서 JAVA9버전부터는 depreacted되었고 Java14버전에서는 사용이 중지되었다.
G1GC(Garbage First)
JAVA9 버전의 디폴트 GC로 지정되었다.
원리는 간단하다. Heap 메모리 구조를 바꾸는 것이다. JAVA8 힙메모리에서 GC가 오래 걸리는 이유는 Old Generation과 Young Generation이 하나의 큰 덩어리로 존재하기 때문이다. 영역이 크므로 GC가 일어나는 주기는 짧지만 한번 GC가 발생하면 Mark-Sweep-Compaction 과정이 오래 걸릴 수 밖에 없다.
G1GC는 Eden, Survivor, Old 영역을 Region이라는 작은 단위로 쪼개어 관리한다. GC는 몇몇 Region의 Eden, Survivor, Old 영역에서만 동작한다. 전체를 대상으로 GC가 일어나지 않으니 STW 시간을 현격히 줄일 수 있다.
참고자료
'JAVA > JAVA Basic' 카테고리의 다른 글
[JAVA] String, StringBuffer, StringBuilder의 차이 (0) | 2024.02.23 |
---|---|
[JAVA] JDK 동작원리 (0) | 2024.02.22 |
[JAVA] 어노테이션( Annotation )의 속성 (0) | 2023.05.29 |
[JAVA] 어노테이션( Annotation )이란? (0) | 2023.05.29 |
[ JAVA ] InputStreamReader : 인코딩 (0) | 2021.07.12 |