Garbage Collector
가비지 컬렉터와 가비지 컬렉션의 차이
- 가비지 컬렉터 : 메모리 관리를 담당하는 시스템 또는 프로그램의 구성 요소이며, 메모리에서 더 이상 사용되지 않는 객체를 찾아 제거하여 메모리를 회수하는 역할을 수행
- 가비지 컬렉션 : 메모리 관리 기술 중 하나로, 가비지 컬렉터에 의해 수행되는 프로세스를 의미
가비지 컬렉션은 프로세스 자체이고 컬렉터는 실제 역할을 수행하는 주체이다.
Garbage Collection(GC)?
자바의 메모리 관리 방법 중 하나로 JVM의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage)를 모아 주기적으로 제거하는 프로세스
장점
- Java 프로세스가 한정된 메모리를 효율적으로 사용할 수 있게 하고, 개발자 입장에서 메모리 관리, 메모리 누수(Memory Laak) 문제에 대해 관리하지 않아도 됨
단점
- 메모리가 언제 해제되는지 정확하게 알 수 없어 제어하기 힘들며, GC가 동작하는 동안에는 다른 동작을 멈추기 대문에 오버헤드가 발생 함(Stop-The-World)
STW(Stop The World)
GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미
GC가 작동하는 동안 GC 관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있음
따라서 이를 최소화 시키는 것이 쟁점
GC가 너무 자주 실행되면 소프트웨어 성능 하락의 문제가 됨, 애플리케이션의 사용성을 유지하면서 효율적이게 GC를 실행하는 최적화 작업이 필요하고 이러한 작업을 GC 튜닝이라고 한다.
가비지 컬렉션 대상
가비지 컬렉션은 특정 객체가 garbage인지 아닌지 판단하기 위해서 도달성, 도달능력(Reachability)이라는 개념을 적용함
객체에 레퍼런스가 있다면 Reachable로 구분되고, 객체에 유효한 레퍼런스가 없다면 Unreachable로 구분하여 수거한다.
- Reachable : 객체가 참조되고 있는 상태
- Unreachable : 객체가 참조되고 있지 않은 상태(GC의 대상이 됨)
JVM 메모리에서는 객체들은 실질적으로 Heap 영역에서 생성되고 Method Area나 Stack Area에서는 Heap Area에 생성된 객체의 주소만 참조하는 형식으로 구성 됨
생성된 Heap Area의 객체들이 메서드가 끝나는 등의 특정 이벤트들로 인하여 Heap Area 객체의 메모리 주소를 가지고 있는 참조 변수가 삭제되면 Heap 영역에서 어디서든 참조하고 있지 않은 객체(Unreachable)가 발생하게 됨
이러한 객체들을 주기적으로 가비지 컬렉터가 제거해주는 것임
가비지 컬렉션 청소 방식
GC가 Unreachable 객체를 청소하는 방식
Mark And Sweep
Mark-Sweep란 다양한 GC에서 사용되는 객체를 솎아내는 내부 알고리즘이며, 가비지 컬렉션이 동작하는 기초적인 과정이다.
원리 : 가비지 컬렉션이 될 대상 객체를 식별(Mark)하고 제거(Sweep)하며 객체가 제거되어 파편화된 메모리 영역을 앞에서부터 채워나가는 작업(Compaction)을 수행
- Mark 과정 : Root Space로부터 그래프 순회를 통해 연결된 객체들을 찾아내어 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
- Sweep 과정 : 참조하고 있지 않은 객체, Unreachable 객체들을 Heap에서 제거한다.
- Compact 과정 : Sweep 후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다(가비지 컬렉터 종류에 따라 하지 않는 경우도 있음)
Mark And Sweep 방식을 사용하면 루트로부터 연결이 끊긴 순환 참조되는 객체들을 모두 지울 수 있음
GC의 Root Space
Mark And Sweep 방식은 루트로부터 해당 객체에 접근이 가능한지가 해제의 기준이 된다
JVM GC에서의 Root Space는 Heap 메모리 영역을 참조하는 method area, static 변수, stack native method stack이 된다
가비지 컬렉션 동작 과정
Heap 메모리 구조
JVM의 힙(heap) 영역은 동적으로 레퍼런스 데이터가 저장되는 공간으로서, 가비지 컬렉션의 대상이 되는 공간임
Heap 영역은 처음 설게될 때 다음의 2가지를 전제(Weak Generational Hypothesis)로 설계 됨
- 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다
- 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다
즉, 객체는 대부분 일회성이며, 메모리에 오랫동안 남아있는 경우는 드물다는 것
이러한 특성을 이용해 JVM 개발자들은 보다 효율적인 메모리 관리를 위해, 객체의 생존 기간에 따라 물리적인 Heap 영역을 나누게 되었고 Yong과 Old 총 2가지 영역으로 설계함
Young 영역(Young Generation)
- 새롭게 생성된 객체가 할당(Allocation)되는 영역
- 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다
- Young 영역에 대한 가비지 컬렉션(Garbage Collection)을 Minor GC라고 부른다
Old 영역(Old Generation)
- Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- Young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 가비지는 적게 발생한다
- Old 영역에 대한 가비지 컬렉션(Garbage Collection)을 Minor GC라고 부른다
Old 영역이 Young 영역보다 크게 할당되는 이유는 Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰 객체들은 Young 영역이 아니라 바로 Old 영역에 할당되기 때문
힙 영역은 더욱 효율적인 GC를 위해 Young 영역을 3가지 영역(Eden, survivor, 0, survivor1)으로 나눈다
Eden
- new를 통해 새로 생성된 객체가 위치
- 정기적인 쓰레기 수집 후 살아남은 객체들은 Survivor 영역으로 보냄
Survivor 0 / Survivor 1
- 최소 1번의 GC 이상 살아남은 객체가 존재하는 영역
- Survivor 영역에는 특별한 규칙이 있는데, Survivor 0 또는 Survivor 1 둘 중 하나에는 꼭 비어 있어야 하는 것
하나의 힙 영역을 세부적으로 쪼갬으로서 객체의 생존 기간을 면밀하게 제어하여 가비지 컬렉터(GC)가 보다 정확하게 불필요한 객체를 제거하는 프로세스를 실행하도록 한다
Java 8에서의 Permanent
Permanent는 생성된 객체들의 정보의 주소값이 저장된 공간
클래스 로더에 의해 load되는 Class, Method 등에 대한 Meta 정보가 저장되는 영역이고 JVM에 의해 사용됨
Java 7까지는 힙 영역에 존재했지만 Java 8 버전 이후에 Native Method Stack에 편입되게 된다
Minor GC 과정
Young Generation 영역은 짧게 살아남는 메모리들이 존재하는 공간, 모든 객체는 처음에는 Young Generation에 생성되게 된다
Young Generation의 공간은 Old Generation에 비해 상대적으로 작기 때문에 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸림(작은 공간에서 데이터를 찾음), 이 때문에 Young Generation 영역에서 발생되는 GC를 Minor GC라 불림
1. 처음 생성된 객체는 Young Generation 영역의 일부인 Eden 영역에 위치
2. 객체가 계속 생성되어 Eden 영역이 꽉차게 되고 Minor GC가 실행
3. Mark 동작을 통해 reachable 객체를 탐색
4. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동
5. Eden 영역에서 사용되지 않는 객체(unreachable)의 메모리를 해제(sweep)
6. 살아남은 모든 객체들은 age 값이 1씩 증가
age?
Survivor 영역에서 객체의 객체가 살아남은 횟수를 의미하는 값, Object Header에 기록
age 값이 임계값에 다다르면 Promotion(Old 영역으로 이동) 여부를 결정
JVM 중 가장 일반적인 HotSpot JVM의 경우 이 age의 기본 임계값은 31
객체 헤더에 age를 기록하는 부분이 6bit로 되어 있기 때문
Survivor 영역의 제한 조건으로 Survivor 영역 중 반드시 1개는 사용되어야 하고, 나머지는 비어 있어야 함
만약 두 Survivor 영역에 모두 데이터가 존재하거나, 모두 사용량이 0이라면 현재 시스템이 정상적인 상황이 아니라는 반증이 됨
7. 또 다시 Eden 영역에 신규 객체들로 가득 차게 되면 다시 한 번 minor GC 발생하고 mark 한다
8. marking한 객체들을 비어있는 Survival 1으로 이동하고 sweep
9. 다시 살아남은 모든 객체들은 age가 1씩 증가
10. 이러한 과정을 반복
Major GC 과정
Old Generation은 길게 살아남는 메모리들이 존재하는 공간
Old Generation의 객체들은 거슬러 올라가면 처음에는 Young Generation에 의해 시작되었으나, GC 과정 중에 age 임계값이 차게 되어 이동 된 것들임
Major GC는 객체들이 계속 Promotion되어 Old 영역의 메모리가 부족해지면 발생하게 된다
Minor GC와 Major GC의 차이점
1. 객체의 age가 임계값(여기선 8로 설정)에 도달하게 되면, 이 객체들은 Old Generation으로 이동된다(promotion)
2. 위의 과정이 반복되어 Old Generation 영역의 공간(메모리)가 부족하게 되면 Major GC가 발생된다
Major GC는 Old 영역은 데이터가 가득 차면 GC를 실행하는 단순한 방식
Old 영역에 할당된 메모리가 허용치를 넘게 되면, Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 Major GC가 실행되게 된다
하지만 Old Generation은 Young Generation에 비해 상대적으로 큰 공간을 가지고 있어, 이 공간에서 메모리 상의 객체 제거에 많은 시간이 걸리게 된다
예를 들어 Young 영역은 일반적으로 Old 영역보다 크기가 작기 때문에 GC가 보통 0.5초에서 1초 사이에 끝난다
그렇기 때문에 Minor GC는 애플리케이션에 크게 영향을 주지 않음, 하지만 Old 영역의 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다.
여기서 Stop-The-World 문제가 발생하게 되는데, Major GC가 일어나면 Thread가 멈추고 Mark and Sweep 작업을 해야해서 CPU에 부하를 주기 대문에 멈추거나 버벅이는 현상이 일어나기 때문임.
이러한 문제를 해결하기 가비지 컬렉션 알고리즘은 발전됨
가비지 컬렉션 알고리즘 종류
GC를 수행하기 위해 Stop The World가 발생해 애플리케이션이 중지되는 문제점과 Heap 사이즈가 커지면서 애플리케이션의 지연(Suspend) 현상이 두드러지게 되었고, 이를 최적화하기 위해 다양한 Garbage Collection(가비지 컬렉션) 알고리즘이 개발 되었다.
Serial GC
- 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC
- GC를 처리하는 쓰레드가 1개(싱글 쓰레드)여서 가장 Stop-the-world 시간이 길다
- Minor GC에는 Mark-Sweep을 사용하고, Major GC에는 Mark-Sweep-Compact를 사용한다
- 보통 실무에서 사용하는 경우는 없다(CPU 코어가 1개인 경우에만 사용)
실행 명령어
java -XX:+UseSerialGC -jar Application.java
Parallel GC
- Java 8의 디폴트 GC
- Serial GC와 기본적인 알고리즘은 같지만, Young 영역의 Minor GC를 멀티 쓰레드로 수행(Old 영역은 여전히 싱글 쓰레드)
- Serial GC에 비해 stop-the-world 시간 감소
실행 명령어
java -XX:+UseParallelGC -jar Application.java
# -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수
Parallel Old GC(Parallel Compaction Collector)
- Parallel GC를 개선한 버전
- Young 영역 뿐만 아니라, Old 영역에서도 멀티 쓰레드로 GC 수행
- 새로운 가비지 컬렉션 청소 방식인 Mark-Summary-Compact 방식을 이용(Old 영역도 멀티 쓰레드로 처리)
실행 명령어
java -XX:+UseParallelOldGC -jar Application.java
# -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수
CMS GC(Concurrent Mark Sweep)
- 어플리케이션의 쓰레드와 GC 쓰레드가 동시에 실행되어 stop-the-world 시간을 최대한 줄이기 위해 고안된 GC
- 단, GC 과정이 매우 복잡해짐
- GC 대상을 파악하는 과정이 복잡한 여러 단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다
- 메모리 파편화 문제
- CMS GC는 Java9 버전부터 depreacted되었고 결국 Java14에서는 사용이 중지
실행 명령어
# deprecated in java9 and finally dropped in java14
java -XX:+UseConcMarkSweepGC -jar Application.java
G1 GC(Garbage First)
- CMS GC를 대체하기 위해 jdk 7 버전에서 최초로 release된 GC
- Java 9+ 버전의 디폴트 GC로 지정
- 4GB 이상의 힙 메모리, Stop the World 시간이 0.5초 정도 필요한 상황에 사용(Heap이 너무 작을 경우 미사용 권장)
- 기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young/Old 영역으로 나누어 사용하였지만 G1 GC는 아예 이러한 개념을 뒤엎는 Region 개념을 새로 도입
- 전체 Heap 영역을 Region이라는 영역으로 체스같이 분할하여 상황에 따라 Eden, Survivor, Old 등 역할을 고정이 아닌 동적으로 부여
- Garbage로 가득찬 영경을 빠르게 회수하여 빈 공간을 확보하므로, 결국 GC 빈도가 줄어드는 효과를 얻게 되는 원리
실행 명령어
java -XX:+UseG1GC -jar Application.java
Shenandoah GC
- Java 12에서 release
- 레드 햇에서 개발한 GC
- 기존 CMS가 가진 단편화, G1이 가진 pause의 이슈를 해결
- 강력한 Concurrency와 가벼운 GC 로직으로 heap 사이즈에 영향을 받지 않고 일정한 pause 시간 소요가 특징
실행 명령어
java -XX:+UseShenandoahGC -jar Application.java
ZGC(Z Garbage Collector)
- Java 15에서 release
- 대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC
- G1의 Region처럼, ZGC는 ZPage라는 영역을 사용하며, G1의 Region은 크기가 고정인데 비해, ZPage는 2mb 배수로 동적으로 운영됨
- ZGC가 내세우는 최대 장점 중 하나는 힙 크기가 증가하더라도 stop-the-world의 시간이 절대 10ms를 넘지 않는다는 것
실행 명령어
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar Application.java
기술면접 질문/답안
Garbage Collector의 역할, 원리, 알고리즘에 대해 아는 만큼 설명해주실 수 있을까요?
Garbage Collector의 주 역할은 더 이상 애플리케이션에서 참조되지 않는 객체, 즉 가비지(garbage)를 감지하고 메모리를 회수하는 것입니다. 이 과정은 주로 두 단계로 진행됩니다.
- 마킹(Marking): 이 단계에서는 가비지 컬렉터가 모든 객체를 스캔하며, 애플리케이션에서 직접적으로 또는 간접적으로 참조되는 객체를 식별(마크)합니다. 참조되지 않는 객체들은 가비지로 간주됩니다.
- 스위핑(Sweeping): 마킹된 객체들 중에서 더 이상 참조되지 않는 객체들을 실제로 메모리에서 제거합니다. 이 과정을 통해 할당되었던 메모리 공간이 회수되어, 새로운 객체 할당을 위해 사용될 수 있습니다.
Garbage Collector의 알고리즘은 여러 가지가 있으며, 주로 사용되는 것은 다음과 같습니다:
- Mark-and-Sweep: 객체가 더 이상 필요하지 않게 되면 마크하고, 마킹 과정이 끝나면 불필요한 객체를 메모리에서 제거하는 방식입니다.
- Generational GC: 객체를 젊은 세대(Young Generation)와 늙은 세대(Old Generation)로 구분하여 관리합니다. 대부분의 객체는 젊은 세대에서 생성되고 빠르게 소멸되므로, 이 영역에서 더 빈번한 가비지 컬렉션이 일어납니다. 오래 살아남은 객체는 늙은 세대로 이동되며, 여기서의 가비지 컬렉션은 덜 빈번하게 발생합니다.
- Stop-the-World: GC가 실행되는 동안 애플리케이션의 모든 작업이 중단됩니다. 이는 GC 작업의 효율성을 높이기 위해 필요한 경우입니다.
가비지 컬렉터는 메모리 관리를 자동화하여 개발자가 메모리 해제와 관련된 복잡성을 직접 처리하지 않도록 돕습니다. 이는 프로그램의 안정성을 증가시키고, 메모리 누수와 같은 문제를 방지하는 데 중요한 역할을 합니다.
Java Map
- 대응 관계를 쉽게 표현할 수 있게 해주는 자료형
- 맵은 사전(dictionary)과 비슷하며, 키(Key)와 값(Value)을 한 쌍으로 갖는 자료형이다
- 맵은 리스트나 배열처럼 순차적으로(sequential) 요소값을 구하지 않고 키(key)를 이용해 값(value)을 얻는다
- 맵 자료형에는 HashMap, LinkendHashMap, TreeMap 등이 있음
Map의 메서드
- put : key와 value를 추가할 수 있다
- get : key에 해당하는 value를 얻을 때 사용한다
- getOrDefault : 맵에 key에 해당하는 value가 없을 때 null 대신 기본값(default)을 얻고 싶을 때 사용
- containsKey : 맵에 해당 key가 있는지를 boolean 타입으로 리턴
- remove : 맵의 항목을 삭제하는 메서드로, 해당 key의 항목을 삭제한 후 value 값을 리턴
- size : 맵의 요소의 개수를 리턴
- keySet : 맵의 모든 key를 모아서 리턴(집합 자료형으로 리턴하는데, 집합 자료형은 리스트 자료형으로 바꾸어 사용할 수도 있음)
Map에 입력된 순서대로 데이터를 가져오거나 입력한 key에 의해 정렬하도록 저장하고 싶은 경우 LinkedHashMap과 TreeMap을 사용하면 된다
내부 구조
기본적으로 Map은 key-value 구조로 구성되어 데이터를 저장할 수 있으며, key를 가지고 value를 찾을 수 있다.
key를 이용한 데이터 검색에 최적화되어 있으나, 동일한 key에 다른 데이터 value가 저장되어 있을 경우 기존에 저장된 데이터가 덮어씌워져 사라진다(중복된 key는 존재할 수 없다).
HashMap<Key, Value>
HashMap은 Hash Table을 이용하여 만들어졌다
Hash Table은 key와 value를 저장하며, key를 이용하여 빠르게 데이터를 찾기 위한 자료구조를 가지고 있다.
- 특정 key는 해시 함수를 통하여 bucket에 접근할 수 있는 index로 변환한다
- index를 통하여 bucket에 접근한다
- bucket에 맞는 index에 key와 value를 저장한다
key가 해시 함수를 통해서 key에 대한 index가 만들어지고 index를 통하여 bucket에 접근하기 때문에 저장되는 데이터가 많을수록, bucket의 크기가 적을수록 index 충돌이 발생할 가능성이 높다
TreeMap<Key, Value>
TreeMap은 Node들의 연결로 이루어져 있으며, LinkedList와 유사하다고 볼 수 있음
- 루트라는 최상위 Node가 존재한다
- 부모 Node와 자식 Node로 구성되어 있다
- 자식 노드가 없는 경우는 Leaf Node라고 부른다
Node에는 key와 value가 같이 저장되며, key 값을 기준으로 좌측, 우측으로 구분하여 저장된다
LinkedHashMap<Key, Value>
HashMap을 상속하여 만들어진 클래스, HashMap과 차이점은 Node 객체를 Entry 객체로 감싸서 저장된 키의 순서를 보존하고 있다는 점임
기술면접 질문/답안
Java Map의 내부 구현은 어떻게 이루어져 있을지 추측해보실 수 있을까요?
Java의 Map 인터페이스는 키와 값의 쌍으로 데이터를 저장하는 구조입니다. Map의 내부 구현은 구현체에 따라 다를 수 있으나, 가장 널리 사용되는 HashMap의 구현을 예로 들어 설명하겠습니다.
HashMap은 해시 테이블을 사용하여 키-값 쌍을 저장합니다. 해시 테이블은 키에 대한 해시 함수를 적용하여 생성된 해시 코드를 사용하여 각 요소(엔트리)의 저장 위치를 결정합니다. 이 구조를 통해 HashMap은 평균적으로 상수 시간(O(1))의 빠른 접근 시간을 제공합니다.
- 해시 함수와 버킷: HashMap 내부에서는 해시 함수를 사용하여 각 키의 해시코드를 계산하고, 이 해시코드를 사용하여 값을 저장할 버킷의 인덱스를 결정합니다. 버킷은 Map 내부의 배열에서 특정 인덱스에 해당하는 위치입니다.
- 충돌 처리: 두 개 이상의 키가 같은 해시코드를 가질 경우 충돌이 발생합니다. HashMap은 이러한 충돌을 처리하기 위해 버킷 내에서 연결 리스트 또는 레드-블랙 트리(자바 8 이후)를 사용합니다. 해시코드가 같은 요소들은 같은 버킷 내에서 리스트나 트리 형태로 관리되며, 이는 검색, 삽입, 삭제 작업의 효율성을 높입니다.
- 동적 재해싱(Rehashing): HashMap의 크기가 특정 임계값을 초과할 때, 배열의 크기를 확장하고 모든 요소를 새 배열에 재해싱하여 배치합니다. 이 과정은 해시 테이블의 로드 팩터(load factor)와 임계값(threshold)에 의해 결정되며, 해시 테이블의 성능을 최적화합니다.
HashMap 외에도 Java에는 TreeMap, LinkedHashMap 등 다른 Map 구현체들이 있으며, 각각의 구현체는 사용하는 데이터 구조와 성능 특성이 다릅니다. 예를 들어, TreeMap은 레드-블랙 트리를 기반으로 하여 정렬된 순서를 유지하며, LinkedHashMap은 삽입 순서 또는 접근 순서를 유지하는 링크드 리스트를 사용합니다
'항해 99 > Java' 카테고리의 다른 글
정렬 알고리즘 (0) | 2024.05.09 |
---|---|
Array & LinkedList (0) | 2024.05.03 |
Java - 컴파일, JVM 스택 / 힙 메모리, 클래스 / 인스턴스 (0) | 2024.04.05 |
Java - SOLID (0) | 2024.03.12 |
WIL-4 (0) | 2024.03.03 |