Garbage Collection(GC)이란?
가비지 컬렉션(GC)이란 자바의 메모리 관리 방법 중 하나로, JVM의 Heap 영역에 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체를 모아 주기적으로 제거하는 프로세스를 의미합니다.
이러한 가비지 컬렉션은 자바, 파이썬, 자바스크립트, Go 등 많은 프로그래밍 언어에 내장되어 있지만, C언어의 경우엔 좀 다릅니다.
C의 경우에는 자체적으로 가비지 컬렉션을 제공해주지 않아, 개발자가 직접 해당 메모리를 해제해 주어야 합니다.
반면 자바의 경우에는 가비지 컬렉터가 메모리 관리를 대행해주기에, Java 프로세스가 한정된 메모리를 효율적으로 사용할 수 있게 해주고, 개발자 입장에서는 메모리 관리 및 메모리 누수(Memory Leak) 문제에 대해 관리하지 않아도 되며 오롯이 개발에만 집중하게 해 줍니다.
하지만 이러한 GC에는 단점이 존재합니다.
- 개발자는 메모리가 언제 해제되는지 정확하게 알 수 없습니다.
- 가비지 컬렉션이 동작하는 동안에는 다른 동작을 멈추기 때문(Stop-The-World)에 오버헤드가 발생합니다
Stop-the-World란 GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 뜻합니다.
GC가 동작할 때 이때 GC 관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있습니다. 따라서 개발자들은 애플리케이션의 사용성을 유지하면서 효율적이게 GC를 실행하는 최적화 작업(GC 튜닝)이 하나의 숙제가 되었습니다.
그렇다면 가비지 컬렉션은 어떤 객체를 Garbage로 판단해서 지울까요???
가비지 컬렉션 대상
객체들은 실질적으로 Heap 영역에 생성되고 Method 영역이나 Stack 영역에는 Heap Area에 생성된 객체의 주소만 참조하는 형식으로 구성됩니다.
하지만 이렇게 생성된 Heap Area의 객체들이 메서드가 끝나는 등의 특정한 이벤트로 인하여 Heap Area 객체의 메모리 주소를 가지고 있던 참조 변수가 삭제되는 현상이 발생하면, 위 그림의 빨간색 객체처럼 어디서든 참조되고 있지 않은(UnReachable) 상태가 됩니다.
반면 파란색 객체들처럼 특정 참조변수에 의해 참조되고 있는 상태일 때 이를 Reachable 상태라고 부릅니다.
위 두 상태 중 가비지 컬렉터는 UnReachable 객체들을 주기적으로 제거해 줍니다
Mark And Sweep 알고리즘
Mark And Sweep 알고리즘은 가비지 컬렉션이 동작하는 원리로 Root에서부터 해당 객체에 접근가능한지에 대한 여부를 메모리 해제의 기준으로 삼습니다. 이러한 Mark and Sweep은 총 3단계( Mark -> Sweep -> Compact)를 거치게 됩니다.
- Mark : Root space로부터 그래프 순회를 통해 연결된 객체를 찾아내어 각각 어떤 객체를 참조하고 있는지 찾아서 마킹합니다. 이때 Root Space란 Heap영역을 참조하는 method area, static 변수, stack, native method stack(JNI를 통한 참조)등을 의미합니다.
- Sweep : Unreachable 객체, 즉 참조되고 있지 않은 객체를 Heap에서 제거합니다.
- Compact : Sweep 후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축하는 과정입니다.(GC 종류에 따라 하지 않기도 함)
이렇게 가비지 컬렉터는 Mark And Sweep이라는 알고리즘을 바탕으로 루트로부터 연결이 끊긴 객체(순환참조 객체 포함)들을 모두 지울 수 있습니다. 여기서 순환 참조란 두 개 이상의 객체가 서로를 참조하면서 아무런 루트 객체로부터 참조되지 않는 상황을 말합니다.
Heap 메모리의 구조
GC에 대한 이해를 조금 더 돕기 위해 Heap 영역에 대해 조금더 자세히 알아보겠습니다!
JVM에서 힙영역은 JVM이 관리하는 프로그램에서 데이터를 저장하기 위해 런타임에 동적으로 할당하는 영역입니다. 이러한 힙 영역에는 new 연산자로 생성된 객체의 인스턴스 변수 및 배열(참조형 타입들)등이 저장됩니다.
이러한 Heap 영역은 처음에 설계될 때 다음 2가지를 전제로 설계되었습니다.
- 대부분의 객체는 UnReachable 상태가 된다
- 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다
즉 객체의 대부분은 일회성을 보이며, 메모리에 오랫동안 남아있는 경우가 드물다는 것입니다.
이러한 특성을 반영하여, JVM 개발자들은 효율적인 메모리 관리를 위해, 객체의 생존 기간에 따라 물리적인 Heap 영역으로 나누게 되었고, Young Generation과 Old Generation 총 2가지 영역으로 나누었습니다.(Perm 영역은 자바 8 이후부터 제거됨)
Young 영역(Young Generation)
- 새롭게 생성되는 객체가 할당되는 영역
- 대부분의 객체가 금방 UnReachable 상태가 되기에, 많은 객체가 Young 영역에 생성되었다가 사라짐
- Young 영역에 대한 가비지 컬렉션을 Minor GC라고 부른다
Old 영역(Old Generation)
- Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- Young 영역보다 크게 할당되며, 더 오래 살아남은 객체들이 존재하므로, GC가 비교적 덜 발생한다.
- Old 영역에 대한 가비지 컬렉션을 Major GC 또는 Full GC라고 부른다
이후 더 효율적인 GC를 위해 Young 영역을 3가지 영역(Eden, Survival 0, Survival 1)으로 나누게 됩니다.
Eden 영역에는 new를 통해 새로 생성된 객체가 위치하게 됩니다.
이러한 Eden 영역에서 정기적인 GC 이후 살아남은 객체들은 Survivor 영역으로 보내집니다
Survivor 0/ Survior 1 에는 최소 1번 이상의 GC에서 살아남은 객체가 존재하는 영역입니다.
이 영역에는 특별한 규칙이 있는데, 두 영역 중 하나는 반드시 비어있어야 합니다
위와 같이 하나의 힙 영역을 세부적으로 쪼갬으로써 객체의 생존 기간을 면밀히 제어하여, GC가 보다 정확하게 불필요한 객체를 제거하는 프로세스를 실행하게 해주는 것입니다!
Minor GC & Major GC
Young Generation 영역은 짧게 살아남은 메모리들이 존재하는 공간으로, 처음엔 모든 객체가 이러한 Young 영역에 생성됩니다.
Young Generation 공간은 Old Generation에 비해 상대적으로 작기에, 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸리며, 이러한 Young 영역에서 발생하는 GC를 Minor GC라고 부릅니다.
Old Generation 영역은 길게 살아남은 메모리들이 존재하는 공간입니다.
즉 이 영역에는 오랫동안 살아남아 age-bit값이 임계점을 넘어 이동한 객체들입니다.
이러한 이동과정을 Promotion이라고 하는데, 이러한 Promotion 과정이 계속되어 Old 영역이 꽉 차게 된다면 발생하는 GC를 Major GC라고 부릅니다
그림과 함께 Minor GC와 Major GC 과정에 대해 살펴보겠습니다!
객체가 처음 생성되었을 때, Heap영역의 Eden에 age-bit 0으로 할당됩니다. 이러한 age-bit는 Minor GC에서 살아남을 때마다 1씩 증가하게 됩니다
이후 시간이 지나, Heap의 Eden 영역에 객체가 다 쌓이게 되면 Minor GC가 한번 일어나게 되고 참조 정도에 따라 Servivor0 영역으로 이동하거나 회수됩니다.
이후에도 계속 Eden 영역에는 신규 객체들이 생성되겠죠??
이렇게 또 Eden 영역에 객체가 다 쌓이게 되면 Eden+Survival 영역에 있는 객체들은 비어있는 survival1 영역에 이동하고 살아남은 모든 객체는 마찬가지로 age가 1씩 증가합니다
이후 또 Eden 영역이 가득 차면 다시 한번 minor GC가 일어나고, 이때도 위와 마찬가지로 Eden+Servivor2 영역에 있는 객체들이 비어있는 Survival0으로 이동한 후 age가 1 증가됩니다
위와 같은 과정을 계속 반복하다 보면 age-bit가 특정 숫자 이상으로 되겠죠??
이때 JVM에서 설정해 놓은 특정 age bit에 도달하게 되면 오래 쓰일 객체라고 판단된 후 Old Generation 영역으로 이동하게 되고 이러한 과정을 Promotion이라고 합니다
이후 Old 영역에 할당된 메모리가 허용치를 넘게 되면, JVM 힙 영역 전체를 바탕으로 참조되지 않는 객체들을 한꺼번에 삭제하는 GC가 실행됩니다. 위와 같이 Old Generation 영역의 메모리를 회수하는 GC를 Major GC라고 합니다.
이러한 Major GC는 시간이 오래 걸리는 작업이고 이때 GC를 실행하는 스레드 외 모든 스레드는 작업을 멈추게 됩니다(Stop-the-world)
Minor GC 또한 Stop-the-world 상태에 빠지지만, 이는 좁은 범위를 대상으로 하기에 그 시간이 짧지만 Major GC의 경우에는 전체를 대상으로 하기에 Stop-the-world 시간이 길어 CPU에 큰 부담을 주게 됩니다.
이러한 문제를 해결하기 위해 자바 개발자들은 Stop-the-World 시간을 최소화하고 애플리케이션 성능을 향상키는 방향으로 알고리즘을 발전해 왔습니다.
가비지 컬렉션 알고리즘 종류
JVM이 메모리를 자동으로 관리해 주는 것은 개발자의 입장에서 상당한 메리트입니다.
하지만 문제는 GC를 수행하기 위해 Stop The world가 발생되고, 이 때문에 애플리케이션 Thread가 모두 정지된다는 문제점이 있습니다. 또한 자바가 발전됨에 따라 Heap 사이즈가 커지게 되면서 애플리케이션의 지연 현상이 두드러지게 되었고, 이를 최적화 위해 다양한 GC 알고리즘들이 개발되었습니다.
지금부터 설명드릴 GC알고리즘은 모두 설정을 통해 Java에 적용할 수 있습니다. 즉 상황에 따라 필요한 GC 방식을 설정해서 사용할 수 있다는 점을 생각하면서 알고리즘들에 대해 살펴보겠습니다!
Serial GC
Serial GC는 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC입니다.
GC를 처리하는 스레드가 위 그림에서 확인하실 수 있듯이 싱글 스레드여서 가장 Stop-the-World 시간이 깁니다. Stop-the-world란 GC작업을 위한 스레드 외 모든 스레드가 종료된다는 것 기억하시죠??
이러한 Serial GC는 Minor GC에는 Mark-sweep을 사용하고, Major GC에는 Mark-Sweep-Compact를 사용합니다.
java -XX:+UseSerialGC -jar Application.java
위 코드는 Serial GC를 실행하기 위한 명령어입니다!
Parallel GC
Parallel GC는 Java 8의 Default GC입니다.
이는 앞서 설명드린 Serial GC와 기본적인 알고리즘은 같지만 Minor GC나 Major GC 둘 다 멀티스레드(병렬처리)로 동작합니다.
따라서 Serial GC에 비해 Stop-the-world 시간이 감소되겠죠??
java -XX:+UseParallelGC -jar Application.java
# -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수
Parallel Old GC(Parallel Compacting GC)
Parallel Old GC는 Parallel GC를 개선한 버전으로 Mark-Summary-Compact 방식을 사용합니다.
여기서 summary 단계는 Sweep 단계 대신 진행되는 방식입니다.
summary는 mark 이후에 이루어지며, 이 과정에서 가비지 컬렉터는 힙 메모리 내의 가비지가 아닌 객체(live 객체)에 대한 위치와 크기 정보를 수집합니다. 이렇게 수집된 정보는 Compact Phase에서 메모리 정리를 위해 사용합니다.
CMS GC(Concurrent Mark Sweep)
CMS GC는 애플리케이션 스레드와 GC 스레드가 동시에 실행되어 Stop-the-world 시간을 최대한 줄이기 위해 고안된 GC입니다.
이는 GC 대상을 파악하는 과정이 복잡한 여러 단계로 수행되기에 다른 GC 대비 CPU 사용량이 높지만 애플리케이션의 응답 속도가 매우 중요할 때 사용하기 좋습니다
이는 Java9 버전부터 Deprecated 되었고 Java14부터는 사용이 중지되었습니다.
// deprecated in java9 and finally dropped in java14
java -XX:+UseConcMarkSweepGC -jar Application.java
G1 GC(Garbage First)
G1 GC는 CMS GC를 대체하기 위해 jdk 7 버전에서 최초로 Release 된 GC이며 java 9+ 부터는 디폴트 GC로 지정되었습니다.
기존 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young/Old 영역으로 나누어 사용하였지만, G1 GC는 Heap 영역을 Region이라는 단위로 나누어 관리합니다.
상황에 따라 Eden, Survivor, Old 등 역할을 동적으로 부여하게 되며, Stop-the-World 시간이 앞서 설명드린 GC들 보다 짧습니다.
참고
지금까지 GC에 대해 알아보았습니다.
긴 글 읽어 주셔서 감사드리고 잘못된 내용이 있다면 댓글 부탁드립니다! 곧바로 수정하겠습니다
https://coding-factory.tistory.com/829
https://inpa.tistory.com/entry/JAVA-☕-가비지-컬렉션GC-동작-원리-알고리즘-💯-총정리
https://blog.mahmoud-salem.net/the-theory-behind-memory-management-java
https://d2.naver.com/helloworld/1329
https://coderstea.in/post/java/get-ready-to-deep-dive-java-memory-management-garbage-collector/
https://flightsim.tistory.com/240
'JAVA' 카테고리의 다른 글
JVM(Java Virtual Machine) 한눈에 알아보기 (0) | 2023.05.31 |
---|---|
JDK, JRE, JVM이란 무엇일까? (0) | 2023.05.30 |
Collection Framework 완전 정복 6탄(마지막편) - Map인터페이스(HashMap, LinkedHashMap,HashTable, TreeMap) (2) | 2023.05.29 |
Collection Framework 완전 정복 5탄 - Set인터페이스 (HashSet, LinkedHashSet) (0) | 2023.05.28 |
Hash, HashFunction, HashCollision 완벽 정복하기! (0) | 2023.05.26 |