JAVA의 장점 중 하나는 개발자가 메모리 관리를 하지 않아도 된다는 점이다. JAVA 언어만을 배운 개발자의 경우 이게 무슨소리인가 의아해 하거나 대충 개념은 알고 있는데 그게 왜 장점인지 이해하지 못할 수도 있다. 하지만 직접 메모리를 참조할 수 있는 언어로 개발을 해봤다면 GC가 얼마나 개발자의 스트레스를 줄여줬는지 알 수 있다.
C언어의 경우 동적 메모리를 사용할 때가 있다. 여기서 개발자가 수동으로 free() 함수를 호출해주지 않으면 해당 메모리는 시스템에 반환되지 않는다. 이 경우 Memory leak이 일어나 애플리케이션이 시스템 메모리를 야금야금 먹으며 커지는 현상이 발생한다.
GC(Garbage Collection)의 대상
자바의 GC은 JVM(Java Virtual Machine)에 의해서 일어난다. JVM의 여러 기능 중 하나가 GC를 통한 메모리 관리이다. JVM의 자세한 설명은 이전 포스트에 있으니 한 번 읽어보는 것도 도움이 될 것이다. (JVM 포스트)
객체가 생성 될 때 해당 객체 변수는 stack 영역에 생성된다. 그리고 해당 객체가 메모리를 사용하게 되는 실제 데이터는 heap 영역에 저장된다.
public class MyExample {
public void sample() {
String word = "HelloWorld. I`m in heap memory";
}
}
GC는 동적으로 할당된 메모리 영역(Heap memory)
중 사용하지 않는 메모리 영역을 탐지하여 해제한다. 위 코드의 경우 sample() 함수가 실행되면서 String 객체를 생성한다. 여기서 word라는 변수는 stack 영역에 생성되고, “HelloWorld. I’m in heap memory” 라는 값은 heap 영역에 저장된다. 그리고 함수가 종료되면 stack에서 word 변수는 반환된다. 이때, heap 영역에 남아있는 “HelloWorld. I’m in heap memory” 값은 Unreachable object
가 되고, GC에 의해 메모리를 회수한다. C언어였다면 종료 전에 free()를 해주지 않았기 때문에 메모리를 회수할 방법이 없다. (Memory leak 발생)
GC(Garbage Collection)의 과정
- Mark - Stack의 모든 변수를 스캔하면서 모든 객체를 마크한다. 이 과정을 통해 마크되는 객체는 reachable object이다.
- Sweap - Heap 영역에서 마크되지 않은 객체는 unreachable object이고, 이러한 객체들이 차지하는 영역의 메모리를 해제한다.
- Compact (Optional) - sweap 이후 분산된 객체들을 heap의 시작주소로 모아 heap 영역의 메모리를 할당된 부분과 미 할당된 부분으로 나눈다.
출처 : https://microeducate.tech/how-does-java-solve-retain-cycles-in-garbage-collection/
Heap 메모리 구조
출처: https://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java
- Young Generation
- Eden : 새롭게 생성한 객체가 존재
- Suvivor(S0, S1) : Eden 영역이 꽉차서 Minor GC가 발생했을 때 살아남은 객체를 저장해 놓은 공간.
S0과 S1 중 한 곳은 반드시 빈 상태
로 존재한다.
- Old Generation : Young Generation에서 여러번의 Minor GC가 발생하는 동안 살아남은 객체들이 존재
- Permanent 영역 : JAVA7까지 존재했다. JAVA8부터는 Native memory영역의 Metaspace로 변경(OS 관리 영역). Static object는 heap영역으로 옮겨졌다.
즉, GC가 일어나는 영역은 Young generation 영역과 Old generation 영역이다.
GC가 일어나는 과정
출처 : https://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf
- 객체가 새로 생성되면 Eden 영역에 할당됨
- Eden 영역이 가득참
Minor GC
발생- reachable object는 S0 또는 S1 영역으로 이동, unreachable object는 메모리 해제
- S0 → S1, S1 → S0 으로 객체 이동. 객체가 이동되면서 해당 객체의 age 증가
- 1~3의 과정이 반복되는 동안 S0 또는 S1 영역이 가득차거나 미리 정해둔
MaxTenuringThreshold
값을 초과할 경우 Old generation 영역으로 이동. Young → Old로 이동되는 것을Promotion
이라 함 - 1~4의 과정이 반복되다가 Old generation 영역이 가득찰 경우
Major GC
발생
GC의 종류
Stop-the-world
GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것을 말한다. stop-the-world가 발생할 경우 GC를 실행하는 스레드 외에 나머지 스레드는 모두 작업을 멈춘다. 그러므로 GC 튜닝을 한다는 의미는 stop-the-world 시간을 줄인다는 의미가 될 수 있다.
출처 : https://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf
- Serial GC
- GC를 처리하는 쓰레드가 1개이다. (싱글 스레드)
- Mark-compact 알고리즘을 사용한다.
- Stop-the-world가 길다
- Parallel GC
- GC를 처리하는 쓰레드가 여러개 이다.
- Serial GC에 비해 stop-the-world 시간이 짧다.
- 자바8의 default GC이다.
- Parallel Old GC
- 기존 Parallel GC의 업그레이드 버전
- Old generation 영역까지 멀티 쓰레드를 사용한다.
- Mark-sweap-compact 가 아닌 mark-summary-compact 알고리즘을 사용한다. (Old GC 처리량 개선)
- Concurrent Mark-Sweep (CMS) GC
- 앞선 방식에 비해 개선된 방식으로 stop-the-world를 최소화 한다.
- Initial Mark → Concurrent Mark → Remark → Concurrent Sweap 의 과정이 일어난다.
- GC 대상을 면밀히 파악해서 stop-the-world를 최소화 시키는 전략이다.
- GC 대상을 파악하는 과정에서 CPU 사용량이 높아진다.
- G1GC
- H/W 기술이 발전함에 따라 메모리 제약이 많이 사라졌으나 기존 GC 기술은 큰 메모리에서 좋은 성능을 내기 어려움
- Heap 메모리가 클 때 짧은 GC 시간을 보장하기 위해 사용
- 기존 Old/Young의 영역 구분이 아닌 전체를 region 영역으로 나누고 region 상태에 따라 역할(Eden, survivor, old)을 동적으로 부여한다.
- GC가 일어날 때 전체 영역이 아닌 특정 region만 탐색하여 stop-the-world 시간을 줄인다.
- Young GC와 Old GC가 발생된다.
- Young GC : Region 중 GC 대상 객체가 가장 많은 Region에서 수행되며 해당 GC를 통해 살아남은 객체를 다른 Region(survivor 역할)으로 이동시키고, 해당 region을 비운다.
- Old GC : Initial Mark → Root Region Sacn → Concurrent Mark → Remark → Cleanup → Copy 단계를 거치며, Old region 의 메모리를 회수한다.