-
Java Garbage Collection (GC)개발 2020. 3. 21. 23:57
평소에 관심을 가졌지만 어설프게 알고 있던 내용인 JVM GC에 대해 정리하고자 한다.
이 글은 Hotspot JVM을 기반으로 작성하였다.
Java8에 많은 부분이 바뀌었지만 이전 버전과 크게 다르지 않은 Heap 영역을 기준으로 설명할 것이다.
여러 자료에 많이 나오는 구조인데 Heap 영역은 아래와 같은 구조로 되어있다.
Young 영역은 Edan 과 Survivor0 (From), Survivor1 (To) 총 3개로 구성되어지는데, Edan 영역은 객체가 생성 시 Heap에 최초로 생성되는
영역으로 각 영역이 가득 차면 객체의 참조 여부에 따라 Edan 영역의 객체가 Survivor0으로 이동되고 Survivor0 영역의 객체가 Survivor1로
이동된다.
이 과정에서 참조가 끊어진 객체를 제거하게 되는데 이런 참조가 끊어진 Garbage 객체를 제거하는 동작을 Garbage Collection 이라 한다.
* 이후 Garbage Collection은 GC라 표현하겠다.
이런 GC는 Young, Old 영역 두곳에서 모두 발생하는데 위 설명은 Young 영역에서 발생하는 GC는 Minor GC, Old 영역에서 발생하는 GC는
Major GC라 부른다.
이 두가지 GC외에 Full GC 라는 GC를 언급하기도 하는데 이 부분은 찾아보면 Full GC는 MinorGC와 Major GC가 동시에 일어나는 GC라고
표현하는 글도 있고 Full GC는 Major GC 와 같다고 하는 글도 있어서 어떤게 맞는지 정확히 이해 되지 않는다.
이제부터는 위의 GC 과정을 상세하게 설명해보고자 한다.
먼저 참조가 끊어진 객체란 무엇인가를 알아야 이후 내용에 대해서 이해 할 수 있다.
객체의 사용 여부는 Root Set과의 관계로 판단하게 되는데 Root Set과의 어떤 식으로든 참조 관계가 있다면 Reachable 객체라고 하고 사용중인
객체로 간주한다.
Root Set 은 아래 3가지 종류가 있다.
> Java 스택, 메서드에서 사용하는 Local variable Section, Operand Stack 에서의 참조
> 메서드 영역의 정적 변수에 의한 참조
> Java Native Interface(JNI) 에 의해 생성된 객체의 의한 참조
위 3가지 종류를 제외하면 참조가 끊어진 객체라 할 수 있으며 이런 참조가 끊어진 객체는 곧 GC 대상이 된다.
참조가 끊어진 객체 즉 GC 대상에 대한 이해가 되었다면 다음으로는 Young 영역에서의 GC인 Minor GC 과정에 대해 설명하겠다.
위에서 설명한거와 같이 모든 객체는 생성 시 최초 Edan 영역에 생성된다.
객체가 계속 생성되다보면 Edan 영역이 가득 차게 되고 이 Edan 영역을 비워줘야 새로운 객체를 생성할 수 있게 된다.
이 Edan 영역은 어떤 원리로 비울 수 있을까?
위 이미지처럼 Eand 영역이 가득차게 되면 Minor GC가 수행되게 된다.
수행 과정은 Edan 영역의 객체를 순회하면서 Root Set에 의해 참조 중인 객체는 Mark (표시) 하고 이런 순회가 끝나면
Mark 된 객체는 Survivor0 영역으로 이동하게 된다. 그리고 Edan 영역은 전체를 비워버린다.
추가로 해당 부분은 Young 영역에 대한 내용이지만 Old 영역에서 참조 될 경우에도 Mark 되어져서 제거되지 않는다.
(이후 Mark 된 객체는 회색으로 표시 하겠다)
그 결과 아래와 같은 상태가 된다.
이 과정에서 객체가 최초 생성 시 갖고 있는 값인 Generational count 값이 0에서 1로 증가한다.
이 값은 추후 Old 영역으로 옮겨지는 기준이 된다.
그리고 다시 객체는 계속 Edan 영역에 생성되어질 것이고 다시 가득 차게 될 것이다. 그럼 다시 한번 GC 가 발생 하는데,
이번에는 Edan과 Survivor01 영역의 객체를 순회하며 Mark를 할 것이다.
이후 Survivor0 에서 Mark되지 않은 객체는 모두 제거될 것이고 Mark되서 살아남은 객체는 Generational count값이 증가하게 된다.
Survivor0에 존재했던 객체는 1이었으므로 2가 될 것이다.
또한 Edan에서 Mark된 객체는 다시 Survivor0으로 이동하고 Edan 영역 전체를 비워버린다. 이런 과정의 결과 아래와 같은 상태가 된다
다시 객체가 생성되면 아래와 같이 Edan 영역이 가득차게 되고 Edan 영역 확보를 위해 Minor GC가 동작할 것이다.
그에 따라 다시 Mark 작업을 하게되고 Survivor0 영역에서 Mark되지 않은 객체는 모두 제거될 것이다.
이제 Edan 영역의 객체를 Survivor0으로 옮겨야 하는데, 아래 이미지와 같이 Edan에서 Mark 되어 Survivor0으로 옮겨가기에
Survivor0 영역의 공간이 부족한 상황이 생긴다. 그 결과 Survivor0 영역도 정리가 필요하게 된다.
정리 과정은 Survivor0에서 살아남은 객체는 Survivor1로 먼저 옮겨지고 그 다음 Edan 영역의 객체가 Survivor1로 옮겨지게 된다.
위와 같이 Survivor0의 객체가 먼저 옮겨지는건 새로 생긴 객체 (Edan 영역 객체) 보다 오래된 객체 (Survivor0) 의 객체가 더 오래 살아남게
될 것이라는 가정에 의한 것이다.
오래 살아남은 객체가 상대적으로 Edan에서 이동된 객체보다 제거 될 가능성이 적고 Edan에서 이동된 객체들은 Survivor0에 있던 객체보다
금방 제거될 확률이 높기 때문에 상대적으로 메모리 단편화가 덜 발생할 것이기 때문이다.
이렇게 Survivor1로 객체가 모두 옮겨지고 Edan 영역과 Survivor0 영역의 객체는 모두 제거된다. 그리고 Survivor0 영역과 Survivor1 영역은 서로의
위치가 변하고 앞으로는 Survivor1에 Edan으로부터 넘어온 객체가 위치할 것이다.
아래는 위 과정의 결과이다.
(기존 Survivor0에 있던 객체 하나가 제거되고 Edan에서 2개의 객체가 이동되어졌다는 가정이다)
이런 과정을 거치면서 Survivor 영역의 객체는 Generational count가 일정 이상 증가하게 되고 그런 개체는 Old 영역으로
옮겨지게 된다. 이를 Promotion이라 표현한다.
이 내용들을 보면 왜 Survivor는 2개로 동작하게 되는건지 의문을 갖게 되는데, 이 부분은 조금 정리가 되지 않은 부분인데 Naver D2를
참조하면 Edan 영역에 객체를 생성 시 "bump-the-pointer" 라는 기술에 대해 언급하는데 이 기술과 관련된 것으로 보인다.
"bump-the-pointer" 는 Naver D2에서는 새로운 객체는 Top 영역에 배치한다고 표현하는데 Top 영역이 정확히 어떤 의미인지는 모르겠지만
내가 이해한건 메모리 영역에서 현재 존재하는 객체의 다음 영역으로 판단된다. "bump-the-pointer" 로 인해 매번 객체는 이전 GC 에 의해
제거되어 부분적으로 빈 메모리 영역이 아닌 항상 가장 마지막에 배치되는 것으로 보인다.
이런 기술을 통해 빠른 메모리 할당을 할 수 있는 것이다. 또한 이에 따라 메모리 단편화를 생각치 않고 객체를 할당 하기에 살아남은 객체만
현재 사용중이 아닌 다른 Survivor 영역으로 옮기고 기존에 사용했던 Survivor는 모두 비워버리는 식으로 동작하여 메모리 단편화를 막기위한
compact 를 할 필요가 없어 그에 따른 성능적인 이점도 가질 수 있는 것으로 보인다.
이런 과정을 생각해보면 Survivor 영역 중 하나는 무조건 비어 있어야 한다. 두 곳에 모두 객체가 존재한다면 비정상 동작으로 판단하면 될 것이다.
추가적으로 또 한가지 의문을 갖게 되는게 "Young 영역 객체 중 Old 영역에 대한 참조를 어떻게 알 수 있는 것인가" 이다.
매번 Old 영역에 있는 모든 객체를 순회하며 확인하는 것인가? 그렇다면 너무 성능적으로 불리한 것이 아닌가?
역시나 답은 "모든 객체를 순회하는 것이 아니다" 이다.
그럼 이에 대해 GC는 어떤식으로 Old 영역에 대한 참조를 판단할 수 있는 것일까?
여기서 Old 영역 내에 위치하는 Card Table 에 대해 알 필요가 있다.
Old 영역의 일부는 Card Table 로 구성되어있고 만약 Young 영역의 객체를 참조하는 Old 영역의 객체가 있다면 해당 Old 영역의 객체는
시작주소에 Flag를 Dirty 로 표시하고 해당 참조에 대한 내용을 Card Table에 표시하게 된다.
이 Card Table을 활용해 Young 영역의 객체에 대해 GC를 할 경우 Old 영역의 객체를 모두 순회하지 않고도 Old에서 참조가 되고 있는지
확인할 수 있다. 물론 이 Card Table은 참조하던 Young 영역의 객체가 제거되거나 Old 영역의 객체가 제거 될 경우 Card Table에서 제거된다.
참고로 Card Table은 총 512 Bytes로 Card 당 1 Byte의 공간을 차지한다.
그럼 이번에는 다시 Edan 영역에 객체를 할당하는 부분을 생각해보자. 새로 생성되는 객체는 Edan 영역에 할당된다 하였는데 생각해보면
객체가 Edan 영역보다 큰 메모리 공간을 필요로 할 수가 있다.
(보통 Edan 영역은 Survivor 영역보다 큰 공간을 갖고 있기 때문에 역시나 Survivor에도 할당 할 수 없을 것이다.)
이때 어쩔 수 없이 해당 객체는 바로 Old 영역에 할당되는데 이를 Premature Promotion이라 한다.
이번에는 Old 영역에 대해 얘기해보도록 하겠다.
Old 영역에서도 위에서 처음 설명한거와 같이 Major GC가 발생한다. Major GC의 경우 상대적으로 Minor GC보다는 발생 빈도수가 적다.
당연하겠지만 Old 영역까지 살아남아서 이동되어지는 객체가 적기 때문이다. 대신에 보통 Old 영역은 Young 영역보다는 큰 공간을 갖고 있고
이 공간이 가득 찬다는건 제거해야 할 범위가 넓다는 것이므로 더 많은 시간이 소요된다.
그럼 이런 Old 영역은 Young 영역처럼 여러 영역에 거쳐서 객체를 이동 시키고 그에 따라 공간을 비우고 하는 작업을 할 수 없는데 이에 따른 메모리
단편화 문제는 없을까?
당연하게도 이런 문제는 발생할 수 밖에 없고 이에 따라 Major GC는 Garbage Collection 종류에 따라 동작을 하게된다.
이 부분은 다시 Garbage Collection 종류에 대해 다룰 때 설명하도록 하겠다.
이 글을 적으며 참조한 곳은 여러 글을 참조해서 정확히 기억나지 않지만 아래와 같다.
- Naver D2 (사이트)
- JVM Performance Optimizing 및 성능분석 사례 (도서)
'개발' 카테고리의 다른 글
Clustering Index란? (0) 2020.04.05 AWS VPC란? (0) 2020.03.24 AWS IOT Rule Engine 을 통한 Elasticsearch 연동 (0) 2020.03.20 AWS Elasticsearch (0) 2020.03.20 SSL이란? (0) 2020.03.17