티스토리 뷰

Java

Garbage Collector(GC)

jvnlee 2022. 3. 13. 20:55

Garbage Collector가 뭔가요?

Garbage는 말그대로 쓰레기, 즉 더 이상 사용하지 않거나 필요하지 않아 버리는 것들을 지칭한다. Java에서도 마찬가지의 의미로 쓰인다. 우리는 필요에 따라 수많은 객체들을 생성해서 사용하는데, 이들 중에 더 이상 참조되지 않는 객체들은 굳이 메모리에 계속 보관할 필요가 없기 때문에 garbage라고 부른다. Java의 JVM은 Garbage Collector(이하 GC)라는 청소부가 있어 이런 garbage들을 인식하고 자동적으로 메모리에서 그런 불필요한 데이터들을 비워준다.

 

 

GC가 제거해줄 Garbage는 어디에 있나요?

JVM의 Runtime Data Area 안에는 Heap Area라는 메모리 영역이 존재한다. JVM 파헤치기에서 보았듯, Heap Area에는 new 연산자로 생성되는 객체들, 그리고 배열과 같이 동적으로 생성된 데이터들이 저장된다. GC는 이 Heap Area를 주시하고 있다가 더 이상 참조되지 않는 데이터를 제거해준다.

 

 

Garbage인지 아닌지는 어떻게 구별하나요? Reachability

앞서 보았던 내용에서 GC가 "더 이상 참조되지 않는" 데이터를 메모리에서 제거해준다고 언급했다. 여기서 Reachability 라는 개념이 등장한다.

 

Reachability란, GC가 어떤 객체가 garbage인지 아닌지 판별하기 위한 개념으로, 이 객체에 대해 유효한 참조가 존재하면 Reachable Object, 존재하지 않는다면 Unreachable Object라고 분류한다. 그리고 GC는 이 중에서 Unreachable Object를 대상으로 garbage collecting을 수행한다.

 

그런데 대부분 애플리케이션의 코드 안에서는 여러 객체들이 수없이 얽힌 복잡한 참조 관계(참조 사슬)를 가지고 있다. 이 때 GC 입장에서 Reachability를 정확히 판단하려면 참조 사슬 안에서 유효한 최초 참조(누가 제일 먼저 불렀니?)를 찾아야 한다. 이 유효한 최초 참조를 Root Set이라고 부른다.

 

GC는 Heap Area를 바라보고 있다고 했으므로, 이 안의 객체들이 어떻게 참조되는지를 먼저 짚어보자.

 

1. Heap Area 내의 다른 객체에 의한 참조

2. 개별 쓰레드에게 할당된 Stack Area에서 Java 메서드 실행을 위해 사용하는 지역 변수와 파라미터에 의한 참조

3. 개별 쓰레드에게 할당된 Native Method Stack에서 네이티브 메서드 실행을 위해 생성한 객체에 의한 참조

4. Method Area의 static 변수에 의한 참조

 

Heap Area의 객체에 대해 이렇게 4가지 종류의 참조가 가능한데, 이 중에서 1번을 제외한 나머지가 Root Set으로 간주되어 Reachability 판단의 기준이 된다. Root Set에서 참조하는 것이 Reachable Object이고, 그렇지 않은 것이 Unreachable Object로 GC 대상이다. 이 이상의 자세한 내용은 아래 링크를 참고하면 된다.

 

https://d2.naver.com/helloworld/329631

 

GC가 동작할 때는 무슨 일이 벌어지나요? ― Stop-The-World

JVM은 GC를 동작시킬 때 잠시 동안 전체 애플리케이션 실행을 멈추는데, 이를 stop-the-world라고 한다. 이 때에는 GC를 실행 중인 쓰레드를 제외한 나머지 쓰레드는 작업을 잠시 중단했다가 GC가 끝난 후에 재개한다. 어떤 GC 알고리즘(GC를 수행하는 방식)이든 stop-the-world는 발생한다. 그래서 GC (알고리즘)튜닝은 일반적으로 stop-the-world의 시간을 줄이는 것이라고 보면 된다.

 

 

GC는 어떤 방식으로 동작하나요? 

GC의 알고리즘은 Weak Generational Hypothesis라는 가설(이면서 전제 조건)을 가지고 출발한다.

 

1. 대부분의 객체는 얼마 지나지 않아 접근 불가능(Unreachable) 상태가 된다.

메서드의 실행 과정에서 생성되어 사용된 객체는 그 객체 자체를 반환하는게 아닌 이상, 메서드가 종료되면 더 이상 참조되지 않는다. 대다수의 객체의 수명이 이렇게 짧다고 보는 것이다. 그래서 이후에 살펴볼 Heap Area의 메모리 구조 상에서도 새로 생성된 객체를 저장하는 Young Generation 영역은 전체 대비 적은 비중을 차지한다.

 

2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

오래된 객체는 말 그대로 생성된 지 오래된 객체, 젊은 객체는 갓 생성되었거나 생성된지 얼마 안된 객체를 뜻한다. GC에 의해 제거되지 않고 버틴 오래된 객체가 금방 GC에 의해 소멸될 가능성이 높은 젊은 객체를 참조할 가능성이 낮다 정도로 받아들이면 될 것 같다. "오래된", "젊은"이라는 표현을 제대로 이해하려면 Heap Area에서 어떻게 데이터를 나누어 관리하는지를 알아야한다.

 

GC의 구체적인 알고리즘에 대해서는 별도의 글(2부)로 다루려고 한다. 여기서는 GC도 나름의 알고리즘을 갖추고 있어 그 알고리즘에 따라 garbage collecting을 한다는 것 정도만 알아두자.

 

 

Heap Area에 저장된 데이터의 비밀

GC의 타겟이 되는 Heap Area의 데이터들은 수명에 따라 다음과 같이 서로 다른 공간에 위치한다.

 

Young Generation

Eden과 Survivor들로 이루어진 "젊은 세대"의 데이터들이 존재하는 영역이다. 여기서 발생하는 GC를 Minor GC라고 부른다.

- Eden

성경의 에덴 동산을 떠올리면 쉽다. 에덴이 태초의 생명들이 있던 곳이듯이, new 연산자로 생성되는 객체들이 여기에 위치한다. 대부분의 객체는 생성 이후 여기에 존재하다가 Minor GC에 의해 메모리에서 제거된다.

- Survivor

Eden이 가득 차면 Minor GC가 이루어지고, 이 때 살아남은 객체들이 여기로 온다. Survivor는 공간이 2개로 분리되어 있는데, 살아남은 객체들은 우선 모두 Survivor1로 들어간다. 다음번 Minor GC 때 Eden이 비워지고, Eden에서 새로 넘어오는 생존자들이 또 있을텐데 이들과 원래 Survivor1에 있었던 생존자들은 모두 Survivor2로 들어간다. (따라서 Survivor1은 비워진 상태) Minor GC가 일어날 때마다 이런 방식으로 Survivor1 또는 2가 비워지고 안에 있던 데이터는 반대편 Survivor 영역으로 이동한다.

 

계속 살아남는 데이터는 Minor GC에 의해 번갈아 비워지는 Survivor의 두 공간 사이를 왔다갔다 하면서 age 값이 증가한다. 그러다가 age 값이 특정 기준치 이상이 되면 Old Generation으로 옮겨진다. 이렇게 Young에서 Old로 이동하는 것을 Promotion이라고 한다.

 

Old Generation

"늙은 세대"의 데이터들이 존재하는 영역이다. 여기서 발생하는 GC를 Major GC라고 부른다.

 

객체들은 Survivor 영역에서 Promotion을 거쳐 이 곳으로 오는데, Old Generation 영역이 가득 차면 Major GC가 발생한다. 이 사실로부터 Major GC는 Minor GC에 비해 상대적으로 훨씬 적게 발생한다는 것을 유추할 수 있다. (무수한 Minor GC로부터 살아남은 극한의 생존자들이 이동해서 Old Generation을 가득 채워야 그제서야 Major GC 1번이다.)

 

Weak Generational Hypothesis의 2가지 가설 중 두번째에서 "오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다."라고 했었다. 그런데 만약 오래된 객체에서 젊은 객체로 참조하는 상황이 있다면 GC는 어떻게 대응할까?

이런 경우를 대비해 Old Generation 영역에는 512 바이트짜리 card table이 존재한다. 이 table에는 Old Gen 객체가 참조하는 Young Gen 객체의 정보를 등록한다. 그리고 Minor GC가 일어날 때는 Old Gen 객체가 참조 중인 Young Gen 객체는 살려줘야하므로, GC가 이 테이블에 적힌 정보를 참고하여 그 객체는 남겨둔다.

 

Permanent Generation

Young부터 Old로 이어지는 Generation의 흐름 상 같이 언급되는 영역이기 때문에 논리적으로는 Heap의 일부인 것처럼 묘사를 하곤 한다. 그러나 엄밀히는 Heap 영역의 일부가 아니므로 주의해야 한다.

 

Java 8 부터는 Metaspace 라는 이름의 공간으로 대체되면서 Heap과의 논리적 관계 조차도 분리되었다고 보면 된다.

 

Java 8 이전, Heap에 포함된 Permanent Generation 시절에는 클래스와 클래스와 관련된 배열 객체, 메서드의 메타 데이터와 static object, 상수화된 String object 등을 보관하는 역할이었는데, Java 8에서는 static object와 상수화된 String object만 Heap에 넘기고 나머지는 모두 Metaspace에 넘기는 것으로 변경되었다.

 

 

Heap Area를 왜 이렇게 구분해서 관리하나요? 

이미 눈치챘을 수 있지만, Heap Area 내의 영역을 구분해서 데이터를 관리하면 다음과 같은 장점이 있다.

 

1. Young Generation 위주로 GC를 하면 되므로, garbage collecting 시간이 단축된다. (stop-the-world로 애플리케이션이 중지되는 시간도 최소화)

Minor GC가 발생하는 Young Generation 영역은 크기가 Old보다 작기 때문에 금방 가득 차게 된다. 따라서 자주 GC가 발생하여 시시때때로 비워진다고 보면 된다. Minor GC가 Major GC보다 훨씬 빠르기 때문에 이렇게 Minor GC 선에서 정리하는 것이 성능 측면에서 바람직하다.

 

2. Young Generation 영역에는 GC가 자주 일어나므로 이 영역에 해당하는 만큼 여유 공간이 계속 확보된다.

Memory Fragmentation(메모리 파편화)로 인해 Compaction(압축, 메모리 공간 정리 작업)이 발생하는 빈도를 최소화할 수 있다.
Compaction과 Memory Fragmentation을 이해하려면, 아래의 상황을 상상해보면 된다.

Heap Area에 데이터들이 가득 차면 어느 영역이냐 따라, Minor GC 또는 Major GC가 발생하게 된다. 그래서 불필요한 데이터를 모두 제거하고 나면, 제거된 데이터의 자리가 공백이 되어 메모리가 듬성듬성해질 것이다. 다음 번에 새 데이터를 저장하려고 할 때 그 데이터가 공백의 크기와 맞아떨어진다는 보장은 없기 때문에 여유 공간이 있음에도 효율적으로 사용하는 것이 불가능한 상황이 발생한다. 이렇게 메모리가 순차적으로 차있지 않고 듬성듬성 남아있는 상태를 Memory Fragmentation이라고 한다. 그리고 중간에 남아있던 공백들을 없애기 위해 데이터를 한 곳으로 모으는 것을 Compaction이라고 한다. ArrayList의 중간 어딘가의 데이터를 삭제하면 그 뒤에 위치하던 데이터들은 모두 index가 당겨지는 것과 비슷한 이치다.

이러한 Compaction이 필요할 때 Full GC가 나서게 된다. Full GC는 Heap Area 전체에 걸쳐 Compaction을 진행해주기 때문에 Minor GC나 Major GC보다도 더 느리고 큰 규모의 작업이다. 그리고 당연히 Full GC가 수행되는 동안 stop-the-world의 시간도 길어질 것이다. 이런 비효율적인 상황을 막으려면 애초에 GC가 자주 일어날 수 있는 Young Generation 영역에 Memory Fragmentation이 일어나지 않게 자주 메모리가 비워지는 것이 중요하다. 그리고 애플리케이션 스펙에 맞게 적절히 Young Generation과 Old Generation 영역의 크기 비율도 튜닝이 필요하다.

 

참고 자료

https://d2.naver.com/helloworld/1329

https://blog.gceasy.io/2020/05/31/what-is-java-heap-fragmentation/

 

'Java' 카테고리의 다른 글

volatile 키워드  (0) 2022.03.27
Java Virtual Machine(JVM)  (0) 2022.03.11
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday