이전에 말했던 내용을 간단히 정리하면, GC란 "메모리를 알아서 정리해주는 기능"이라고 한다.
그렇다면 왜 Garbage Collection이라고 하는 걸까?
먼저 "Garbage"라는 용어에 대해 알아볼 필요가 있다.
Garbage의 사전적 의미는 "쓰레기"라는 의미를 가진다. 그렇다면, 컴퓨터 과학 측면에서 Garbage라는 의미는 무엇을 의미할까?
Garbage를 한 마디로 정의하자면, "유효하지 않은 메모리 주소"를 말한다.
Garbage를 Unreachable Object(사용하지 않을 객체)라고 정의하는 사람들도 있지만, 사용을 하고 싶더라도 메모리 주소를 잘못 설정하여 잘못된 쓰레기 값을 사용하는 경우가 존재하기 때문에 "유효하지 않다"라는 정의가 더 Garbage의 정의에 맞지 않을까 싶다.
유효하지 않다? 이게 무슨 말일까. 예시를 통해 알아보자.
"arr"라는 객체가 존재하고, 이 객체는 주솟값 100에 1이라는 값을 저장하고 있는 상태라고 가정하자.
열심히 이 값을 사용하다가, (이런 일은 벌어지면 안 되겠지만) "arr"라는 객체 이름은 동일하지만, 주솟값 200에 "JAVA"라는 값이 저장되도록 새롭게 메모리가 할당되었다고 가정하자.
자, 그럼 이후 코딩 때 "arr"라는 객체 이름을 통해 접근할 때 어떤 값을 얻을 수 있을까?
"JAVA"라고 대답할 수 있다면 잘 따라오고 있는 것이다.
그런데, 개발자가 변덕스러워서 이전 주솟값 100에 저장했던 1이라는 값을 다시 불러오고 싶다고 가정하자. 어떻게 해야 할까?
정답은 "답이 없다"이다.
이미 주솟값 100을 가리키는 이전 arr라는 객체는 더 이상 해당 주솟값을 가리키고 있지 않기 때문에 해당 주소는 영원히 잃어버린 상태가 되는 것이다.
위 예시에서 100이라는 주솟값이 "유효하지 않은 메모리"가 되며, 이 값이 Garbage가 되는 것이다.
(프로그래밍 언어로는 "Dangling Object"라고도 한다)
그렇다면 Garbage는 알았는데, 왜 GC가 필요한 것일까?
자 생각해보자.
arr라는 객체를 100, 101,..., 200을 차례대로 가리키도록 설정했다고 가정하면 실제로 활용하는 메모리 주솟값은 200밖에 없는데 100 ~ 199 또한 Garbage로써 메모리를 차지하게 된다.
만약 이런 객체가 100개라면? 아니면 1000개라면?
실제로 활용하는 메모리는 적은 반면 Garbage 때문에 사용되는 메모리는 매우 많게 될 것이며, 이를 "데이터 누수(Data Leakage)" 현상이라고 한다.
이런 데이터 누수 현상을 해결하기 위해서 C언어는 free() 명령어를 통해 할당한 메모리를 해제해주는데, JAVA는 GC를 통해 이런 문제를 해결하는 것이다.
JAVA에서는 알아서 JVM 내에 존재하는 GC가 원래 코드의 실행을 잠시 멈추고 불필요한 메모리(Garbage) 값을 찾아 이를 정리해준다.
이렇게 Garbage 값을 자동으로 GC가 정리해주기 때문에 개발자는 메모리 해제에 신경 쓰지 않고 개발에 더욱 집중할 수 있어 큰 장점을 가지게 되는 것이다.
GC를 이해하기 위해선 먼저 JVM의 Heap 영역에 대해 이해할 필요가 있다.
Heap 영역의 구조는 위와 같이 New/Young 영역, Old 영역, Permanent Generation 영역이 존재한다.
Permanent Generation은 생성된 객체들의 정보의 주소값이 저장된 공간이다.
이곳에 Class, Method, Static 변수와 상수에 대한 Meta Data가 저장되는 공간으로 흔히 메타데이터의 저장 영역이라고도 한다.
처음 공부할 때 Method Area와 너무 유사한 작업을 하지 않나 싶었다. 그래서 찾아봤는데, Method Area는 JVM 제품마다 구현이 다르고, Hotspot JVM(Orale)의 Method Area를 Permanent Area(PermGen)이라는 것을 알게 되었다.
이 PermGen에 대해서는 많은 이슈가 존재하였고, JAVA 8부터는 Metaspace 영역으로 대체되었다고 한다.
Metaspace 영역은 Heap이 아닌 Native 메모리 영역으로 취급되므로, JVM이 아닌 OS 레벨에서 관리하는 영역이 된다.
Metaspace가 Native 메모리를 이용함으로써 JVM에서는 그만큼의 공간을 더 사용할 수 있게 되었고 개발자는 영역 확보를 위한 메모리 고민을 덜 해도 되는 장점을 가지고 왔다.
New/Yount 영역의 Eden은 "객체들이 최초로 생성되는 공간", Survivor 0/1은 Eden에서 참조되는 객체들이 저장되는 공간을 의미한다.
Old 영역에는 New 영역에 저장된 객체 중 오래된 객체가 이동되어 이 곳에 저장된다.
새로 생성된 객체는 Eden 영역에 위치하게 된다.
이후 GC가 한 번 발생하였을 때 살아남았다면 Survivor 0이나 Survivor 1 중 한 곳으로 이동하게 된다.
계속해서 이 과정을 반복했을 때 살아남았다면 객체는 일정 시간 계속해서 참조되고 있다는 의미이므로 해당 객체를 Old 영역으로 이동시킨다.
조금 더 상세히 말하자면, Eden 영역과 Survivor 0 영역을 조사하여 사용하지 않는 객체를 삭제하고, 사용하는 객체를 Survivor 1으로 보낸다.
다음 GC 때는 Eden 영역과 Survivor 1 영역을 조사하여 살아남은 객체를 Survivor 0으로 보내는 것이다.
이 과정을 반복하다 오래 살아남은 객체는 Old 영역으로 보내는 과정이 Minor GC라고 할 수 있다.
(참고로 Old 영역으로 보내는 것을 Promotion이라고 한다)
Eden 영역이 꽉 찼을 경우 실행된다.
Old 영역에 있는 모든 객체들을 검사하여 참조되지 않은 객체들을 한꺼번에 삭제하는 과정을 말한다.
Minor GC는 0.5초 정도로 끝나는 반면에 Major GC는 시간이 오래 걸리며, 실행 중 프로세스가 정지된다.(GC를 수행하는 스레드 이외의 스레드는 작업을 멈추고, GC 작업이 완료된 이후 작업을 다시 시작함)
Old 영역이 꽉 찼을 경우 실행된다.
이전에 Major GC를 수행할 때 프로세스가 정지된다는 말을 했다.
Stop The World는 GC 실행을 위해 JVM이 애플리케이션의 실행을 멈추는 작업을 말한다. GC의 단점인 시간이 오래 소요되는 이유는 Stop The World 때문이기 때문에 GC의 성능 개선을 한다는 것은 곧 Stop The World 시간을 줄이는 작업과 이어진다.
JVM도 이런 시간문제를 해결하기 위해 다양한 실행 옵션을 제공한다.
Eden 영역에 객체를 빠르게 할당하기 위해 활용되는 기술이 Bump the Pointer, TLABs(Thread-Local Allocation Buffers)이다.
Bump the Pointer란 Eden 영역에 마지막으로 할당된 객체의 주소를 캐싱해두는 것이다.
마지막으로 할당된 객체 주소가 캐싱되어 있으므로 새로운 객체는 저장된 객체 주소 다음 위치에 바로 저장시키면 되기 때문에 객체 크기와 Eden 영역 여분 메모리만 비교하면 되므로 빠른 메모리 할당이 가능해진다.
문제는 객체는 Heap Area에 존재하므로 모든 Thread에서 공유한다는 점이다.
만약 여러 Multi Thread 환경에서 Bump the Pointer 방식을 활용할 때 동시에 2개의 Thread가 객체를 저장한다면 똑같은 주솟값에 2개 이상의 객체가 저장될 수도 있기 때문에 원래라면 1개 Thread가 Eden 영역에 객체를 저장할 때 Lock을 걸어야 할 것이다.
하지만 Lock을 건다는 것은 다른 Thread는 그만큼 Lock이 풀릴 때까지 기다려야 한다는 의미이므로 많은 시간이 소요될 것이다.
이런 시간문제를 해결하기 위한 기법이 TLABs이다.
TLABs는 각각의 Thread마다 Eden 영역에 객체를 할당하기 위한 주소를 부여하는 것이다.
즉 각 Thread마다 가지고 있는 주소가 모두 다르기 때문에 Lock 등의 동기화 작업 없이 빠르게 Eden 영역에 메모리를 할당하게 되는 것이다.
Serial GC의 Young 영역에서 발생한 알고리즘은 Mark & Sweep 그대로이다.
하지만 Old 영역에서는 Mark Sweep Compat Algorithm이 활용된다.
Sweep 과정에 의해 삭제된다면 삭제된 메모리 공간은 비어있을 텐데, 이때 Compact 과정을 통해 빈자리를 채워주는 알고리즘이다.
하지만 싱글 스레드로 동작하며, 그만큼 Stop The World의 시간도 다른 GC에 비해 길기 때문에 실무에서 활용하는 경우는 없다.
JAVA 8에서 채택한 Deafult GC이다.
Young 영역의 GC를 멀티 스레드 방식을 활용하므로 Stop The World 시간이 짧다.
하지만 Old 영역에서는 멀티 스레드 방식을 활용하지 않는다.
GC 오버헤드를 상당히 줄여주었지만 Stop The World를 수행할 때 Application이 멈춘다는 단점은 피할 수 없는 알고리즘이다
◎ Parallel Old GC
Parallel Old GC는 Old 영역까지 멀티스레드 방식을 활용한 Parallel GC 방식이다
Stop The World의 Application이 멈추는 현상을 줄이고자 만들어진 GC이다.
Mark Sweep 알고리즘을 Concurrent 하게 수행하는 것이 특징이다
재사용할 객체를 한꺼번에 찾는 게 아니라 4개의 Step에 걸쳐 찾는 방식을 활용하여 Application 중지를 최소화한 알고리즘이다.
4개의 Step은 아래와 같다.
CMS GC는 자원이 GC를 위해서도 활용되기 때문에 그만큼 JVM이 활용할 메모리가 적어져 응답이 느려질 수는 있지만, 응답이 멈추는 현상을 최소화할 수 있다는 장점이 있다.
CMS GC는 메모리와 CPU를 더 많이 필요로 하며 Compact 과정이 일어나지 않는다는 단점이 존재한다.
CMS GC는 좋아 보이지만 결국 GC에 메모리를 할당하여야 한다는 단점으로 인해 JAVA 9 버전부터는 Deprecated 되었으며 JAVA 14부터는 사용이 중지되었기 때문에 이런 게 있다 정도로만 알고 있으면 된다.
JAVA 9 version 이상부터 Default로 설정된 GC이다.
현재 GC 중 Stop The World 시간이 가장 짧다는 장점을 가진다.
Heap을 Region이라는 일정한 부분으로 나눠서 메모리를 관리한다. 이후 전체 Heap을 탐색하지 않고 Region 단위로 탐색하며, Region 단위로만 GC를 발생시키는 것이다.
Region을 여러 역할로 구분하여 해당 영역에 적절한 객체를 할당한 것이다. 이후 Garbage가 많은 Region에 대해 우선적으로 GC를 실행하는 방식을 채택했다.
G1 GC에서는 Region 영역에 Available/Unused가 추가되었는데, 이는 사용되지 않은 Region이라는 것을 의미한다.
따라서, Eden 영역에서 Minor GC가 일어나 Available 지역으로 객체가 이동한다면 Survivor 영역이 되며, Eden 영역에는 이제 남아 있는 객체가 없을 것이므로 Avialable/Unused 지역이 되는 것이다.
Major GC에도 Old 영역을 모두 찾는 것이 아닌, 결국 Garbage가 가장 많은 Region에 대해서만 GC가 수행되기 때문에 애플리케이션의 지연도 최소화될 것이다.
물론 Old 영역을 한꺼번에 정리하는 기존 GC보다는 더욱 GC 작업이 잦게 발생하겠지만 Region만 정리하므로 훨씬 작은 규모의 메모리 정리 작업이며 Concurrent 하게 수행되기 때문에 효율적이며 지연도 크지 않다.
Garbage란 "유효하지 않은 메모리"를 의미하며, 더 이상 참조할 수 없는 메모리 공간이라고 생각해도 된다. Garbage가 많아지면 메모리 누수 현상이 발생할 수 있으며, C언어는 free() 명령어를 통해 일일이 해제해줘야 하지만 JAVA에서는 GC(Garbage Collection)을 통해 이런 메모리 관리를 자동으로 수행해준다.
GC는 Minor GC와 Major GC로 나뉘는데, Minor GC는 Eden 영역과 Survivor 영역을 조사하여 사용하지 않는 객체를 남은 Survivor 영역으로 이동시켜주거나, 너무 오래 존재하던 객체는 Promotion 과정을 통해 Old 영역으로 이동시켜주는 것이다. Major GC는 Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 과정으로, 시간이 매우 오래 걸린다.
GC는 Stop The World 방식으로 애플리케이션을 중지시키고 Mark and Sweep을 통해 사용되지 않는 메모리를 삭제시키는 과정을 거쳐 동작한다.
GC Algorihtm은 Old 영역에 Compact 방식을 결합한 Serial GC, Young 영역에 멀티 스레드 방식을 활용한 Parallel GC, Old 영역에도 멀티 스레드 방식을 적용시킨 Parallel Old GC, 현재는 사용되지 않는 CMS GC, 마지막으로 최근 Version에서 활용되는 Region을 이용한 G1 GC가 존재한다.
G1 GC는 Minor GC에서는 Eden영역을 조사하여 Available/Unavailable으로 객체를 보내 활성화시키는 과정을 거치며 Major GC에서는 Garbage가 가장 많은 Region인 Garbage First Region을 찾아 GC를 수행하는 과정을 거친다. Region에 대해 GC를 수행하다 보니 GC가 조금 더 많이 수행되지만 Region이 작은 규모이고 Concurrent 하게 수행되므로 많은 장점을 지닌다