Garbage Collector
Java를 다뤄봤다면 한 번쯤 들어봤을 법한 언어 GC
이것은 대체 무엇일까?
- Heap 메모리에 위치한 unreachable한 객체를 삭제시켜주는 것
- 더 이상 사용하지 않는 객체를 의미한다.
- 객체가 null인 경우
- 객체가 블럭 안에서 생성되고 블럭이 종료 되었을 경우
- 부모 객체가 null이 되었을 경우, 자식 또는 포함된 객체들
- 더 이상 사용하지 않는 객체를 의미한다.
C, C++ 등의 코드 레벨에서는 메모리를 직접 할당받고, 헤제해야 지만
//C++
char *s = malloc(sizeof(char)*10);
s = "Hello GC!";
printf("%s", s);
free(s) // <- 메모리 할당 해제
//Java
String s = "Hello GC!";
System.out.println(s);
//s.free(); //잘못됐다.
다만 Java에서는 JVM의 GC가 자동으로 삭제 시켜주며 코드 레벨에서 메모리 관리를 벗어나 편리함을 누릴 수 있게 되었습니다👏
GC의 장단점
장점
- Memory Leak이 발생하지 않는다.
- 휴먼 에러 발생 가능성을 낮춤
- 메모리 할당 해제하는 코드를 작성하지 않는 경우
- 해제한 메모리를 다시 해제하는 경우 등단점
- 성능저하
- 어떤 메모리를 해재해야할 지 검사/삭제 하는 과정에서 추가적인 CPU, 메모리 등 PC 자원을 필요로 한다.
- Unreachable 객체가 많을 경우 비용은 더욱 증가😂
- 메모리 해제 시점을 알 수 없다
- JVM은 GC를 수행하기 위해 Application 실행을 멈추는데 이 시간을 개발자는 알 수 없다..
- Application의 중지 시점을 알 수 없으므로 실시간성이 매우 강조될 경우 대처할 수 없다.
JVM의 GC 알고리즘
Mark & Sweep
: GC Root에서 시작해 참조된 Object Mark
Mark
- GC Root라는 특정 객체를 기준으로 탐색을 실시합니다.
- Stack 영역에서 모든 객체(변수)를 스캔하면서 각각 힙 영역의 어떤 Obejct를 참조 하고 있는지 찾아서 마킹을 수행
- Heap 영역에서 reachable한 객체는 마킹, 그렇지 않는 경우 Sweep 실행
: Sweep 후 Heap공간 모습
Sweep
- Mark의 1번, 2번 과정을 통해 Marking 되지 않은 객체에 한해 메모리 할당 해제
Mark & Sweep의 특징
- 의도적으로 GC를 실행시켜야한다.
- JVM나름의 기준이 있어 GC를 실행시키는 타이밍이 존재.
- 해당 타이밍은 Heap영역을 참고하자..
- application 실행과 GC 실행이 병행된다.
의도적으로 GC를 실행시켜야한다??
우선 Heap
을 살펴보자
Heap은 크게 2가지 영역, New Generation과 Old Generation으로 구분 되어 있다.
- New Generation : 새로운 객체가 저장되는 영역. 1차 GC인 Minor GC가 발생하는 영역
- Old Generation : 오래 살아 남은 객체가 저장되는 영역, New 보다 영역이 크기 대문에 GC는 비교적 적게 발생한다
- 해당 영역의 GC는 Major GC이다.
Application 실행과 GC 실행이 병행된다??
앞에선 GC가 실행될 때 Applicataion 실행이 중단된다 했는데 이제와서 무슨일일까?
사실! 같이 실행되는 것은 아닙니다. GC를 실행하기 위해 Applictaion을 멈추는게 맞아요! 다만 실행을 멈추는 것을 최대하 짧게해 병행되는 것 처럼 보이게 되죠!!
- Application 실행을 멈춘다 == Stop the world
그럼 자주 사용되는 방식에 대해 몇 가지만 소개를 더 해보겠습니다.
Parallel GC
- Java 8 에서 사용되는 기본적인 GC 방식입니다
- multi Core 환경에서 사용
- 여러개의 Thread로 GC를 실행하기 때문에 stop the world 시간이 짧다는 장점이 있습니다.
G1 GC
- Java 9 부터 기본적으로 쓰이는 GC 방식
- heap을 일정 크기의 지역으로 나눠 New Generation, Old generation으로 구분합니다.
- 이때 지역을 그 때 상황에 맞춰 개수를 알아서 튜닝을 해줍니다!!!
- 이런 방식으러 stop the world를 최소화 할 수 있습니다.
=> 그래서 Java8과 Java11의 큰 차이점 중 GC 개선(Paralle -> G1)으로 인한 성능 향상이라고 합니다
왜 Heap을 2가지 영역으로 나눴을까?
- 위 이미지를 보면 새롭게 할당된 객체는 오랫동안 참조되지 않는게 다반사 입니다.
- 전역 변수 선언하는 양보다 지역변수(블록단위) 선언이 많거나 등
- 생각보다 빨리 garbage 상태가 됩니다.
- 또 오래된 객체에서 새로 생긴 객체로 참조하는 경우가 거의 없습니다.
=> 따라서 Heap을 스캔하는 효율을 향상 시키기 위해 2개의 영역으로 나누게 됬습니다.
- 오래된 객체는 따로 분리
- 할당된지 얼마 되지 않은 것을 스캔해 GC를 수행하는 것이 효율적
어떻게 Old Genertaion까지 도달하는 걸까?
New Generation은 위 그림과 같이 Eden
, survivor0
, survivor1
3가지 영역으로 나누어집니다. 모든 객체는 Eden
영역 에서 시작해 점차 survivor0
-> survivor1
으로 이동하게 됩니다.
- 다음 영역으로 이동할 때 age-bit가 1씩 증가하게 됩니다.
이런 상황에서 Eden
영역이 꽉 차게 되면 Minor GC가 실행됩니다.
Minor GC로 부터 살아남은 객체들은 위 그림 처럼 survivor0
영역으로 이동하게 됩니다.
이 과정이 반복되어 Minor GC에서 끝까지 살아 남는다면 Old Genertaion 영역으로 넘어가게 됩니다.
- age-bit가 특정 숫자를 넘기게 되는 경우도 있습니다.
최종적으로 Old Generation도 꽉 차게 되면 Major GC를 통해 앞서 언급한 `Mark and Sweep`` 알고리즘이 수행됩니다.