GC (Garbage Collection) 란?
1. GC(Garbage Collection)란?
JAVA에서 객체가 생성되면 해당 객체는 JVM의 Heap영역의 메모리를 점유한다. 해당 객체는 현재 참조 되지 않더라도 메모리 공간을 점유한다. 물론 메모리 공간은 한정적이다. 한정적인 메모리에 현재 사용되지 않는 객체가 점유한 공간을 제때 정리해 주지 않으면 메모리 공간이 부족해 지고, 결국은 Out Of Memory Error 가 발생 할 수 밖에 없다.
JAVA는 JVM을 통하여 구동된다. JAVA의 특징 중 하나로 메모리 관리를 개발자가 직접 명시적으로 수행하지 않고, JVM에서 자동으로 수행해 주는데, 이 작업을 Garbage Collection이라 하며 GarbageCollector가 작업을 수행한다.
JAVA의 GC는 다음과 같은 가설을 전재로 도입되었다.
JAVA에서 GC의 도입의 전제 가설 (weak generational hypothesis)
- 대부분의 객체는 금방 접근 불가능 상태 (unreachable)가 된다.
- 오래된 객체에서 젋은 객체로의 참조는 아주 적게 존재한다.
2. JVM GC 동작 순서
JVM에서 Garbage Collection 은 크게 3가지 step 으로 동작한다.
- Heap 영역에 존재하는 객체들에 대해 접근 가능한지 확인한다.
- GC Root에서 부터 시작하여 참조값을 따라가며 접근 가능한 객체들에 Mark하는 과정을 진행한다.
- Mark 되지 않은 객체 즉, 접근할 수 없는 객체는 제거(Sweep) 대상이 된고, 해당 객체들을 제거한다.
접근 가능한 객체인지 어떻게 판단할까?
위 그림 처럼 Root에서 시작해서 참조하는 객체를 찾고, 또 그 객체가 참조하는 객체를 찾아가며 Mark 한다.
Mark 되지 않은 객체는 접근할 수 없는 객체 (Unreachable Object)로 판단하고 메모리를 돌며 제거(Sweep)한다.
3. GC 수행 영역 구분
GC는 수행되는 영역에 따라 두가지로 구분 할 수 있다.
GC가 수행되는 영역에 따라 Minor GC와 Major GC(Full GC)로 구분한다.
- Young Generation 영역 :
- 새롭게 생성한 객체의 대부분이 이곳에 위치한다.
- 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 대부분의 객체가 Young 영역에 생성되었다가 사라진다.
- 이 영역에서 객체가 사라질 때 Minor GC가 발생했다고 말한다.
- Old Generation 영역 :
- Minor GC이후에도 Young 영역에서 사라남은 객체가 여기로 복사된다.
- 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생하고 오래 걸린다.
- 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다.
Young Generation 영역에 Survivor 영역은 왜 2개일까?
바로 메모리 단편화를 줄이기 위해서이다.
메모리가 할당되고 헤제되기를 반복하다 보면 왼쪽 사진과 같이 총 메모리 공간은 남지만, 파편화되어있어 메모리를 할당할 수 없는 문제가 발생한다. (외부 단편화)
예를 들어 새로운 객체는 Eden에 생성된다. Eden이 가득차면 GC가 수행되고 살아있는 객체는 Survivor로 옮겨진다. 근데 그다음에 Eden이 가득차면, Eden과 Survivor 영역의 메모리를 정리하지만 이 영역은 연속적이지 않게 된다. 이런 현상을 방지하기 위해 두가지 Survivor 영역을 두어서 Eden과 Survivor 안에 있는 reachable한 객체들은 비어 있는 새로운 Survivor로 옮겨지거나 특정 객체(Old enough한)는 Old로 Promotion된다. 그리고 두 Survivor space는 역할을 바꾼다. 하나는 텅텅 비어있고 하나는 Eden에서 올라오는 것을 수용하는 공간. 이 과정을 통해 Heap에서의 연속적인 메모리 사용을 가능하게 한다.
만약, S1, S2 영역에 모두 데이터가 존재 할 경우. 우리의 JVM 상태는 정상이 아니다.
4. GC 시나리오
객체가 생성되면 Eden 영역에 위치 하게 된다.
Eden영역이 가득차게 되면 Minor GC가 발생하여 참조가 없는 객체는 삭제되고, 참조 중인 객체는 Survivor 영역으로 이동한다.
Survivor 영역이 가득차게 되면 Minor GC가 발생하고 참조가 없는 객체는 삭제되고, 참조 중인 객체는 다른 Survivor 영역으로 이동한다.
Survivor영역에서의 GC과정을 반복 하며, 계속 참조 중인 객체는 Old Generation으로 이동한다.
Eden 영역에서 Survivor 영역으로 이동 할 때 객체가 남아있는 영역보다 큰 경우 Old Generation으로 이동한다.
*Young Generation 에서 Old Generation으로 이동하는 과정을 promotion 이라고 한다.
Young Generation 영역에서 오래동안 살아남은 객체는 Old Generation 영역으로 옮겨지는데, 오래되었다는 기준은 무엇일까?
오래되었다고 하는 기준은 Young Generation 영역에서 Minor GC 가 발생하는 동안 얼마나 오래 살아남았는지로 판단한다. 각 객체는 Minor GC에서 살아남은 횟수를 기록하는 age bit 를 가지고 있으며, Minor GC가 발생할 때마다 age bit 값은 1씩 증가 하게된다. Age bit 값이 MaxTenuringThreshold 라는 설정값을 초과하게 되는 경우 Old Generation 영역을 객체가 이동하게 된다. 또는 Age bit가 MaxTenuringThreshold 초과하기 전이라도 Survivor 영역의 메모리가 부족할 경우에는 미리 Old Generation 으로 객체가 옮겨질 수도 있다.
5. Stop the World
Stop the World는 GC에서는 아주 중요한 개념이다.
Major GC(Full GC)가 발생하면 JVM은 어플리케이션을 멈추고, GC를 실행하는 쓰레드만 동작하게 되는데, 이를 Stop the World 라고 한다. 경우에 따라서는 시스템 장애로 이어 질 수 있기 때문에 GC와는 떼어 놓을 수 없다.
메모리 공간(heap)을 늘리면 Major GC(Full GC)를 피할 수 있을까?
메모리 공간을 늘리면 물리적 공간이 그만큼 커지니 Major GC의 첫 수행 시점은 늦출 수 있다. 하지만 Stop the World의 시간도 메모리 공간에 비례하기 때문에 Major GC 시간이 그만큼 중가하게 된다. 따라서 Stop the World는 절대 피할 수 없고 대신 최적화가 필요하다.
이를 GC 튜닝이라고 한다.
그렇다면 왜 GC를 수행하는 동안 Stop The World가 일어나도록 해놨을까? 그냥 GC도 하고 다른 쓰레드들도 동작하게 해두면 되지 않을까?
그 이유는 애플리케이션 스레드가 멈추어야 현재 메모리 상에서 살아있는 객체를 정확히 식별할 수 있기 때문이다. 만약 애플리케이션의 스레드를 멈추지 않는다면, 애플리케이션의 동작에 따라 변화하는 객체들의 상태를 빠르게 반영하지 못했을 수 있다. 따라서 stop the world를 통해 애플리케이션 스레드를 잠시 멈춤으로써 객체들의 상태를 완전히 반영하는 것이다.
6. Mark & Sweep & Compact
Mark: 접근 가능한 객체에 Mark하여 표시
Sweep: Mark되지 않은 객체들을 제거하는 과정
Compact: Sweep 과정에 의해 삭제되면 메모리 단편화가 발생하는데, Compact를 통해 빈자리들을 채워줌
7. Garbage Collector 종류
JVM이 메모리를 자동으로 관리해주는 것은 개발자의 입장에서 상당한 메리트이다.
하지만 문제는 GC를 수행하기 위해 Stop The World가 발생되고 이 때문에 애플리케이션이 중지되는 문제점이 발생하게 되었다.
또한, 자바가 발전됨에 따라 Heap의 사이즈가 커지면서 애플리케이션의 지연(Suspend) 현상이 두드러지게 되었고, 이를 최적화 위해 다양한 Garbage Collection(가비지 컬렉션) 알고리즘이 개발되었다.
- Serial GC
- Parallel GC
- Parallel Old GC
- CMS(Concurrent Mark Sweep) GC
- G1 GC
- Z GC
7.1 Serial GC (-XX:+UseSerialGC)
- 가장 단순한 방식의 GC로 싱글 스레드(스레드 1개)로 동작한다.
- 싱글 스레드로 동작하여 느리고, 그만큼 Stop The World 시간이 다른 GC에 비해 길다.
- Mark & Sweep & Compact 알고리즘을 사용한다.
- GC가 동작할때 어플리케이션의 모든 쓰레드를 멈추기때문에, 멀티쓰레드 어플리케이션 사용하기에 적합하지 않는다. 따라서 보통 실무에서 사용하지 않는다.(디바이스 성능이 안좋아서 CPU 코어가 1개인 경우에만 사용)
- 자바 프로그램을 실행할때 -XX:+UseSerialGC GC 옵션을 지정하여 해당 가비지 컬렉션 알고리즘으로 힙 메모리를 관리하도록 실행할 수 있다.
java -XX:+UseSerialGC -jar Application.java
7.2 Parallel GC (-XX:+UseParallelGC)
- Java 8의 default GC
- Young 영역의 GC를 멀티 스레드 방식을 사용하기 때문에, Serial GC에 비해 상대적으로 Stop The World 가 짧다.
java -XX:+UseParallelGC -jar Application.java
# -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수
7.3 Parallel Old GC (-XX:+UseParallelOldGC / -XX:+ParallelGCThreads=n)
- Parallel GC는 Young 영역에 대해서만 멀티 스레드 방식을 사용했다면, Parallel Old GC는 Old 영역까지 멀티스레드 방식을 사용한다.
- -XX:+ParallelGCThreads=n 옵션으로 멀티 스레드 개수를 지정할 수 있다.
java -XX:+UseParallelOldGC -jar Application.java
# -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수
7.4 CMS GC(Concurrent Mark Sweep GC)
- Stop The World로 Java Application이 멈추는 현상을 줄이고자 만든 GC이다.
- 접근가능한(Reachable) 객체를 한번에 찾지 않고 나눠서 찾는 방식을 사용한다. (4 STEP)
- Initial Mark: GC Root가 참조하는 객체만 마킹 (stop-the-world 발생)
- Concurrent Mark: 참조하는 객체를 따라가며, 지속적으로 마킹 (stop-the-world 없이 이루어짐)
- Remark: concurrent mark 과정에서 변경된 사항이 없는지 다시 한번 마킹하며 확정하는 과정(stop-the-world 발생)
- Concurrent Sweep: 접근할 수 없는 객체를 제거하는 과정 (stop-the-world 없이 이루어짐)
- 위와 같이 stop-the-world가 최대한 덜 발생하도록 하여, Java Application이 멈추는 현상을 줄인다.
# deprecated in java9 and finally dropped in java14
java -XX:+UseConcMarkSweepGC -jar Application.java
7.5 G1 GC (Garbage First GC) (-XX:+UseG1GC)
- Java 9+ 의 default GC
- 기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young / Old 영역으로 나누어 사용하였지만,
G1 gc는 아예 이러한 개념을 뒤엎는 Region이라는 개념을 새로 도입하여 사용. - 전체 Heap에 대해서 탐색하지 않고 부분적으로 Region 단위로 탐색하여, 각각의 Region에만 GC가 발생한다.
- Region의 상태에 따라 그 Region의 역할(Eden, Survivor, Old)가 동적으로 변동한다.
- G1GC 에는 Full GC 와 유사한 Concurrent Cycle 이라는 과정이 존재하는데, 해당 과정은 IHOP (InitiatingHeapOccupancyPercent) 에서 정한 수치를 초과하면 실행하게 된다.
- 자세한 동작은 다음과 같다.
- Initial Mark : Old Region에 존재하는 객체들이 참조하는 Survivor Region을 찾는다.(STW)
- Root Region Scan : 위에서 찾은 Survivor 객체들에 대한 스캔 작업을 실시한다.
- Concurrent Mark : 전체 Heap의 scan 작업을 실시하고, GC 대상 객체가 발견되지 않은 Region은 이후 단계를 제외한다.
- Remark : 애플리케이션을 멈추고(STW) 최종적으로 GC 대상에서 제외할 객체를 식별한다.
- Cleanup : 애플리케이션을 멈추고(STW) 살아있는 객체가 가장 적은 Region에 대한 미사용 객체를 제거한다.
- Copy : GC 대상의 Region이었지만, Cleanup 과정에서 완전히 비워지지 않은 Region의 살아남은 객체들을 새로운 Region(Available/Unused) Region에복사하여 Compaction을 수행한다. 살아있는 객체가 아주 적은 Old 영역에 대해 [GC pause(mixed)] 를 로그로 표시하고, Young GC가 이루어질 때 수집되도록 한다.
- [ G1 GC의 효율성 ]
Java9+ 부터 기본 GC로 자리잡은 G1 GC에서는 이전의 GC들처럼 일일히 메모리를 탐색해 객체들을 제거하지 않는다.
대신 메모리가 많이 차있는 영역(region)을 인식하는 기능을 통해 메모리가 많이 차있는 영역을 우선적으로 GC 한다.
즉, G1 GC는 Heap Memory 전체를 탐색하는 것이 아닌 영역(region)을 나눠 탐색하고 영역(region)별로 GC가 일어난다.
또한 이전의 GC 들은 Young Generation에 있는 객체들이 GC가 돌때마다 살아남으면 Eden → Survivor0 → Survivor1으로 순차적으로 이동했지만, G1 GC에서는 순차적으로 이동하지는 않는다.
대신 G1 GC는 더욱 효율적이라고 생각하는 위치로 객체를 Reallocate(재할당) 시킨다.
예를 들어 Survivor1 영역에 있는 객체가 Eden 영역으로 할당하는 것이 더 효율적이라고 판단될 경우 Eden 영역으로 이동시킨다.
7.6 ZGC (-XX:+UseZGC)
- Java 15에 release
- 대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC
- G1의 Region 처럼, ZGC는 ZPage라는 영역을 사용하며, G1의 Region은 크기가 고정인데 비해, ZPage는 2mb 배수로 동적으로 운영됨. (큰 객체가 들어오면 2^ 로 영역을 구성해서 처리)
- ZGC 개발자는 ZGC가 적은 메모리나 큰 메모리에서 STW 시간을 최대한 적게(10ms 이하로) 가져가기 위해 제작되었다. 실제로 STW 시간을 줄이기 위해서 Marking 시간에만 STW 을 가져가도록 하고 있다.
- ZGC 의 핵심은 바로 Colored pointers 와 Load barriers 라는 주요한 2가지 알고리즘이다.
Colored Pointers
객체를 가리키는 변수의 포인터에서 64bit 을 활용해가지고, Marking을 한 것을 볼 수 있다. (따라서 ZGC는 반드시 64bit 운영체제에서만 사용가능하다.)
- Finalizable: finalizer을 통해서만 참조되는 Object의 Garbage
- Remapped: 재배치 여부를 판단하는 Mark
- Marked 1 / 0 : Live Object
Load Barriers
ZGC 는 G1GC 와는 다르게 메모리를 재배치하는 과정에 STW 없이 재배치를 한다. (위에서 말한 64bit 를 바탕으로)
이때 RemapMark와 RellocationSet을 확인하면서 참조와 Mark를 업데이트하게 된다.
그래서 ZGC는 아래와 같은 Flow를 따르게 된다.
- Mark Start STW : ZGC의 Root에서 가리키는 객체 Mark 표시
- Concurrent Mark/Remap: 객체의 참조를 탐색하면서 모든 객체에 Mark 표시
- Mark End STW : 새롭게 들어온 객체들에 대해 Mark 표시
- Concurrent Pereare for Relocate: 재배치하려는 영역을 찾아 Relocation Set에 배치
- Relocate Start STW : 모든 Root 참조의 재배치를 진행하고 업데이트
- Concurrent Relocate: 이후 Load Barriers 를 사용하여 모든 객체를 재배치 및 참조 수정
따라서 G1GC 와의 가장 큰 차이점은, 바로 Pointer를 이용해서 객체를 Marking하고 관리하는 것이라고 볼 수 있다.
참고
- https://jhyonhyon.tistory.com/20
- https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC
'Java' 카테고리의 다른 글
해시맵(HashMap)이란? (0) | 2023.01.11 |
---|---|
클래스 변수(Class variable) vs 인스턴스 변수(Instance variable) vs 지역 변수(local variable) (0) | 2022.12.31 |
Java는 포인터(Pointer)가 없는데 왜 NullPointerException이 발생할까? (0) | 2022.12.27 |
0.1이 진짜 0.1이 아닌 이유(부동소수점(Floating Point)) (0) | 2022.12.13 |
JAVA 자료형 (0) | 2022.02.22 |