Garbage collection 이 무엇인가요? 왜 쓰나요? 어떤 문제가 있을까요?
누군가 인터넷에 올려놓은 면접 질문 리스트라고 해서 쭉 살펴봤는데
https://okky.kr/article/1255457
웬걸? 2번째 질문부터 막혔다. 나는... 컴공을... 졸업하고... 결국 코드 몽키가 되어버린 건가...?
그래서 나중에 이 지식이 필요할 미래의 나를 위해 한번 정리해보는 시간을 가진다. :)
TL;DR
- Garbage collection 이 무엇인가요? → 접근 불가능한 객체들의 메모리 할당 해제 알고리즘
- 왜 쓰나요? → 필요 없는 객체의 불필요한 메모리 점거 제거를 위해
- 어떤 문제가 있을까요? → 메모리 해제를 위해 중간중간 프로그램이 멈춘다. (stop-the-world)
좀 더 장황하게 보자.
자바는 java virtual machine(이라 JVM )이라는 프로그램을 통해서 자바 바이트 코드를 실행시킨다. 자바 이전의 실행방식과 다른 점은 기존의 불편함을 jvm을 통해 해결한 것인데
프로그램 ←→ 운영체제 ←→ 하드웨어 간의 실행방식에서
프로그램 ←→ “JVM” ←→ 운영체제 ←→ 하드웨어 간의 실행방식으로 바꿔놨다.
기존 실행방식은 운영체제가 달라지면 프로그램 역시 수정이 필요했다. (=운영체제에 종속적이다!)
JVM(Java Virtual Machine)이 등장하면서 프로그래머는 운영체제에 대한 건 신경 쓰지 않고 프로그램을 구현할 수 있게 되었다.
(=JVM이 알아서 해주니까)
jvm을 설명하는 이유가 무엇인고 하니, 자바 코드를 바이트 코드로 바꿔주는 주체이면서 실행 시 메모리 관리를 해주는 주체 이기 때문에 살짝 설명해보았다.
jvm을 조금 더 자세하게 살펴보자
- 프로그래머는 코드를 작성
- 자바 컴파일러를 통해 class 파일 형식으로 변환
- class loader는 해당 바이트 코드를 메모리 위에 안착
- execution engine은 해당 바이트 코드를 명령어 단위로 분류 및 실행을 처리
- garbage collector는 실행하면서 생기는 메모리 누수를 알아서 해제시켜준다고 보면 된다.
제일 밑에 보이는 runtime data area는 jvm이 실행되면 운영 체제로부터 할당받은 메모리 공간이라고 생각하면 된다.
runtime data area 공간을 조금 더 자세하게 보자.
1. Method Area
- JVM이 실행되면서 생기는 공간이다.
- Class 정보, 전역 변수 정보, Static 변수 정보가 저장되는 공간이다.
- Runtime Constant Pool 에는 말 그대로 '상수' 정보가 저장되는 공간이다.
- 모든 스레드에서 정보가 공유된다.
2. Heap
- new 연산자로 생성된 객체, Array와 같은 동적으로 생성된 데이터가 저장되는 공간
- Heap에 저장된 데이터는 Garbage Collector 가 처리하지 않는 한 소멸되지 않는다.
- Reference Type의 데이터가 저장되는 공간
- 모든 스레드에서 정보가 공유된다.
3. Stack
- 지역변수, 메서드의 매개변수와 같이 잠시 사용되고 필요가 없어지는 데이터가 저장되는 공간
- Last In First Out, 나중에 들어온 데이터가 먼저 나간다
- 만약, 지역변수이지만 Reference Type일 경우에는 Heap에 저장된 데이터의 주소 값을 Stack에 저장해서 사용하게 된다.
- 스레드마다 하나씩 존재한다.
4. PC Register
- 스레드가 생성되면서 생기는 공간
- 스레드가 어느 명령어를 처리하고 있는지 그 주소를 등록한다.
- JVM이 실행하고 있는 현재 위치를 저장하는 역할.
5. Native Method Stack
- Java 가 아닌 다른 언어 (C, C++)로 구성된 메서드를 실행이 필요할 때 사용되는 공간
우리가 주목해야 하는 공간은 heap space로 garbage collection가 메모리 정리를 해주지 않는다면 공간을 계속 차지한다는 문제가 있다! (메모리 누수, memory leak)
그리고 그 메모리를 정리하는걸 garbage collection (GC)이라고 한다.
2가지를 먼저 얘기하고 넘어가야 한다. garbage collector의 1) 정책과 2) 메모리 영역 구분
1. 정책
JVM의 가비지 컬렉터는 'Weak Generational Hypothesis'를 전제로 설계되었다.
'Weak Generational Hypothesis'는 다음과 같다.
- 대부분의 객체들은 생성된 이후 짧은 시간 안에 Unreachable 상태가 된다.
- 생성된 지 오래된 객체에서 방금 생성된 객체로의 참조는 아주 적다.
* 여기서 unreachable이라는 단어는 ‘도달할 수 없는'이라는 의미이다. 예시로 살펴보자
Person person = new Person("hoonzi");
person.sayHello();
person = new Person("jihoon");
person.sayHello();
이 코드에서 처음에 선언된 “hoonzi” person 객체의 경우,
“jihoon” person 객체로 변경되면서
heap space에 남아있지만 아무도 참조할 수 없는 객체로 변했다.
이때 도달할 수 없다(Unreachable)이라고 말하며 가비지 컬렉션의 대상이 된다.
2. 메모리 영역 구분
위 Weak Generational Hypothesis 룰에 따르면, 대부분의 객체들은 생성된 다음,
금방 Unreachable 상태로 도달하므로 heap space에서 그런 상태에 놓인 메모리 영역을 회수하면 될 일이다.
가비지 콜렉터가 별도의 스레드에서 실행된다면 수행된다면 별 문제없을 것이다.
그런데 회수할 때 가비지 콜렉터는 메모리를 회수하기 위해 종종 자바 애플리케이션을 “멈추고” 메모리 회수를 진행한다.
- 회수를 위해 자바 애플리케이션을 멈추는 걸 “Stop The World”
- 회수 대상이 아닌 걸 표시해놓는 걸 “Mark” (즉, 현재 사용 중인걸 표시)
- 표시 안된 대상을 메모리 상에서 해제하는 걸 “Sweep”이라고 한다.
가비지 콜렉터의 Stop-The-World는 짧을 수도 길 수도, (살아있는 객체의 개수에 따라 달라진다.)
자바 애플리케이션을 멈출 수도 있기 때문에 가능한 한 빨리, heap space의 적은 범위만을 탐색할 필요가 있다.
그래서 메모리 영역을 구분해놨다!!
대부분의 객체는 생성된 다음 금방 사라지기 때문에, 객체는 처음 Young Generation 영역에 할당된다.
Young Generation에 생성&할당된 뒤, 가비지 컬렉션에 의해 메모리 회수되는 걸 Minor GC라고 부른다.
(minor GC를 수행할 때는 Young Generation 만 참조한다. 전체를 참고하는 게 아니라!)
young generation 영역도 Eden과 Survivor 영역으로 구분되어있는데, 이 역시 GC를 줄여
stop-the-world의 시간을 줄이기 위함이다.
- Eden에 객체가 할당된다.
- Eden에 객체가 꽉 차면, Minor GC가 일어난다.
- 살아남은 객체는 Survivor로 이동된다.
역시 해당 영역이 꽉 차면 GC가 일어난다. (살아남았을 경우, 나이처럼 Count 값이 하나씩 올라간다.) - Survivor 영역에서 살아남아있으면서, 일정 Age Count를 넘기면 Old Generation으로 옮겨진다.
**이때, 영역이 옮겨지는걸 Promotion이라고 부른다.
위 과정으로 Old Generation 영역도 객체가 점점 할당되다, 꽉 차게 되면 그때 비로소 Major GC가 발생한다.
Old Generation 영역은 Young Generation영역에 비해 크기가 크기 때문에 Stop-The-World 시 멈추는 시간이 배로 걸린다.
위 GC의 정책과 메모리 구조, 구조에 따른 GC 2가지 (minor, major)를 살펴봤다.
그렇지만 위 서술에서는 “회수" 한다고 한 단어로 정리됐지만 그 회수는 어떻게 하는 것일까?
참조되지 않은 개체(Unreachable)를 제거하는 기본 전략은 객체를 식별하고 나머지 객체를 모두 삭제하는 것이다.
크게 3단계로 나뉜다.
- 미래에 사용될 가능성이 있는 모든 개체를 찾기(Mark 단계)
- 사용하지 않는 모든 개체 제거(Sweep 또는 Copy 단계)
- 이후의 공간 줄이기 등의 처리 (Compact 단계)
Marking phase
마킹 단계에서는 살아있는 객체와 그렇지 않은 것(Unreachable)을 구별하는 것이다.
jvm에서 사용되는 모든 GC 알고리즘은 아직 살아있는 모든 객체를 찾는 것으로 시작한다.
GC 알고리즘은 일부 특정 객체를 가비지 컬렉션 루트(GC 루트)로 정의하는데,
- 현재 실행 중인 메서드의 로컬 변수 및 입력 매개변수
- 활성 스레드
- 로드된 클래스의 정적 필드
- JNI 참조
가비지 컬렉션은 가비지 컬렉션 루트에서 시작하여 객체에 대한 참조를 따라 메모리의 전체 객체 그래프를 순회한다.
방문되는 모든 객체는 활성 상태로 표시된다.
마킹 단계가 끝나면 살아있는 모든 객체는 마킹되고,
마킹되지 않은 객체의 경우 Garbage로 여겨지며 GC 알고리즘에 의해 향후 단계에서 제거된다.
Removing Unused Objects
사용되지 않는 객체(Garbage)를 제거하는 전략은 크게 3가지로 나눌 수 있다.
- Sweeping
- Compacting
- Copying
먼저
Sweep Phase
가장 간단한 알고리즘이다.
위에서 마킹되지 않은 객체들이 차지하고 있는 영역의 할당을 풀어버리는(free) 방법이다.
다음에 빈 공간에 재할당 될 수 있다. 하나, 빈 공간(=free-list)이 어딘지 알고 있어야 하기 때문에
해당 기록을 저장하는 공간과 할당/재할당 시마다 기록을 위해 약간의 overhead가 일어날 수 있다고 한다.
또 다른 단점으로는 빈 공간을 그대로 내버려두기에 해당 빈 공간 영역들보다 훨씬 큰 영역을 필요로 객체 할당이 필요로 할 때는 여전히 할당이 안될 수 있다. (자바에서 보는 극혐 OutOfMemoryError)
Compact Phase
sweap 단계와의 차이는 위에서 본 sweap 단계를 거치고 남는 중간중간 남는 영역들을 빈 곳 없이 채워준다는 거다.
위에서 발생한 free-list 관리와 OutOfMemoryError를 마주치지 않을 수 있게 된다는 장점이 존재한다.
(그래도 역시 해당 여유 공간보다 큰 객체 할당 시에는 당연히 에러가 나겠지?)
Copy Phase
마킹 & 카피 단계는 위 Compact 단계랑 유사하게 메모리의 재배치가 일어난다는 점이 공통점이다.
하지만 다른 점은 메모리 공간을 두 개로 나눠놓고, 마킹이 끝난 (살아있다고 체크된) 메모리는
나눠진 두 개의 공간 중 현재 공간이 아닌 다른 공간에 “재배치" 함으로써, 마킹과 재배치가 동시에 이뤄질 수 있다는 점이다.
마킹&콤팩트 작업 시에는 마킹 다~ 끝나고 빈 공간이 없게 채우는 작업이 들어간다면,
마킹&카피 작업 시에는 마킹이 된 객체는 다른 공간에 옮겨지는(copy) 작업이 들어갈 수 있다.
(마킹도 여전히 진행 중이면서 동시 작업! 효율성 증가!)
단점은 위 그림에서 보이다시피 메모리 공간이 반토막이 나버렸다는 것...?
메모리에서 unreachable(도달 불가능 객체)을 지우는 방법을 알아봤으니,
해당 방법을 조합한 여러 GC Collection을 알아보자.
GC의 여러 종류
- Serial Garbage Collection
- Parallel garbage collection
- Concurrent Mark and Sweep Garbage Collection
- G1 Garbage Collection
Serial Garbage Collection
해당 Collection은
• Young Generation 영역에서는 Mark-Copy
• Old Generation 영역에서는 Mark-Sweep-Compact
해당 Collection은 단일 스레드로 동작하는 GC로 해당 GC가 동작할 때는, GC를 제외한 모든 스레드의 동작이 멈춘다.(stop-the-world)
만약 jvm이 동작하는 머신의 코어수가 여러 개 여도 역시 하나만 이용해 동작하게 된다.
아래의 파라미터로 해당 Collection을 사용할 수 있다.
java -XX:+UseSerialGC
jvm이 동작하는 머신의 CPU가 단일 코어 일 경우, 유용할 테지만 요즘엔 다들 멀티코어 쓰니~ 패스
Parallel garbage collection
해당 Collection은
- Young Generation 영역에서는 Mark-copy
- Old Generation 영역에서는 Mark-sweep-compact
이 Collection 역시 stop-the-world를 발생시키지만, 위 Collection과 다른 점은
단일 스레드가 아닌 멀티스레드를 이용해 동작한다.
해당 Collection은 멀티 스레드의 동작으로 속도나 처리량이 늘어나지만 여전히 stop-the-world를 발생시키기 때문에
latency가 중요한 작업이나 프로그램의 경우, 다음에 나올 Collection을 고려해야 한다.
스레드 개수는 사용자가 조절 가능하고, 기본 스레드 개수는 머신의 코어 개수와 동일하다.
*java -*XX:ParallelGCThreads=NNN
young generation에서 동작을 위해서는 java -XX:+UseParallelGC
old genreation에서 동작을 위해서는 java -XX:+UseParallelOldGC
Concurrent Mark and Sweep Garbage Collection
해당 Collection은
- Young Generation 영역에서는 Parallel stop-the-world mark-copy algorithm
- Old Generation 영역에서는 Concurrent mark-sweep algorithm
해당 Collection은 old generation을 청소할 때 일어나는 (상대적으로) 긴 stop-the-world 시간을 줄이고자 고안된 Collection으로 아래의 방법을 이용해 시간을 좀 줄인다.
- Old generation에서는 compact 작업 x, 할당을 풀어버린 뒤, free-list로 관리
- mark&sweap 단계를 애플리케이션의 종료 없이 “Concurrent” 하게 동작!
해당 Collection은 위 두 개와 달리 명시적으로 작업들을 멈추지 않고 동시에 동작하게 된다.
그렇지만 동시에 동작하기 위해서는 응용 프로그램 스레드와 Collection 스레드 간의 CPU 사용시간을 놓고 경쟁하게 된다.
기본적으로 이 GC에서 사용하는 스레드 수는 컴퓨터의 물리적 코어 수의 1/4과 같다.
해당 Collection을 사용하기 위한 파라미터는 아래와 같다.
java -XX:+UseConcMarkSweepGC
위 두 개의 방법에 비해 stop-the-world를 막기 위한 방법이기 때문에 latency가 중요한 작업이나 프로그램의 경우 해당 collection이 유용할 수 있다. (멀티 코어 머신에서 아주 좋은 선택!)
하나 CPU의 코어들이 동시에 사용되기 때문에 때때로 프로그램 수행보다 GC 돌리는데 더 쓸 수도 있다는 점이 존재한다. (core=2 면, (GC=2, program=0) 이 될 수도 있다는 얘기…)
G1(Garbage first) Garbage Collection
해당 Collection은 CMS 방식의 GC 단점을 대체하고
stop-the-world의 시간과 해당 정지를 일으키는 공간의 대략적인 분포를 예상 가능하게 만드는 목표를 위해 고안되었다.
위 문장을 딱 보면? 만 뜬다.
어떻게 stop-the-world의 시간을 예상하고, 가비지 객체를 예측할 수 있을까?
여기서 기발한 아이디어가 나온다. 메모리 공간을 연속적인 공간이 아닌 “분리된” 각자의 영역으로 나눈다면?
그림으로 보자
위 영역 구분을 통해 해당 Collection 방법은
다른 Collection과 비교했을 때,
각 일정 사이즈의 영역별, 역할별 (eden, survivor, old 등으로) 세분화됐기 때문에
- 특정 영역만 지정해 보면 되고 (각 영역별로 garbage와 아닌 객체의 양을 예상)
- 해당 영역을 처리하는 시간은 영역별로 진행되기에 (stop-the-world 시간 예상)
위 두 가지가 가능해지게 된다.
1번의 특징 때문에 G1 GC는 다른 GC 방식에 비해 잦게 호출될 것이다.
하지만 작은 규모의 메모리 정리 작업이고,
2번의 특징 덕분에 Concurrent 하게 수행되기 때문에 지연이 크지 않으며,
가비지가 많은 지역에 대해 정리를 하므로 훨씬 효율적이다. 그렇기에 가장 큰 장점은 성능이다.
이러한 구조의 G1 GC는 당연히 앞의 어떠한 GC 방식보다 처리 속도가 빠르며
큰 메모리 공간에서 멀티 프로레스 기반으로 운영되는 애플리케이션을 위해 고안되었다.
G1의 GC 방식은 찾아보니 내가 주화입마에 걸릴 거 같아 이번 글은 여기까지!
출처
- https://mangkyu.tistory.com/118
- https://mangkyu.tistory.com/120?category=872426
- https://hbase.tistory.com/209
- https://velog.io/@shin_stealer/자바의-메모리-구조
- https://iq.opengenus.org/memory-management-in-java-mark-sweep-compact-copy/
- https://iq.opengenus.org/memory-management-in-java-garbage-collection-algorithms/