728x90


가비지 컬렉터에 대해서 작성한 글이 있다. 참고의 포스팅이 바로 가바지컬렉터의 원리를 간략하게 설명한 것이다.

하지만 실제로 자바에서 가비지 컬렉터가 동작하는 원리는 별로 간단하지 않은데 이는 reference counting이 아니라

mark-sweap 알고리즘을 사용하기 때문이다.


Mark-Sweap 알고리즘

사용하는 reference를 표시(mark)한다 그 후 표시가 안된 녀석을 청소(sweap)한다.


자바에는 실제로 여러종류의 GC가 있지만 기본 배이스는 이녀석이다. 선택하고 선택 안된 녀석들 모조리 죽여버리는 것이다.

그럼 여러분은 궁금해 해야하는게 있다.


그럼 그 선택하는 원리가 뭔데?


사용하는 레퍼런스를 선택하는 원리가 무엇일까? 도대체 어떻게 사용하는 것일까?

거기에 대한 원리는 간단하다.


출처 - https://d2.naver.com/helloworld/329631


자바에서 reference를 가질 수 있는 경우는 딱 네가지 있다.


1-각각의 thread의 stack에 있는 변수가 가르키는 reference

2-메소드영역의 정적 변수가 가르키는 referene

3-native를 돌리는 thread에서 native stack이 가르키는 reference

4-1,2,3번사항의 녀석이 가르키는 reference 


4번의 경우 무조건 적으로 자신을 가르키는 더 상위개체가 존재하게된다.

그러나 1번과 2번과 3번의 경우 반드시 최상위 노드가 되는데(자기 자신보다 더 상위개체가 없음)

이러한 최상위 진입점을 Root Set이라고 부른다.



출처 - https://d2.naver.com/helloworld/329631


root set부터 그래프 구조를 순회하면서 모조리 마크하면서 다니면 된다.

이렇게 마킹된 녀석을 Reachable Object라고 부른다.

반대로 이렇게 마킹된 녀석을 제외한 녀석들을 Unreachable Object라고 부른다.


즉 mark sweap알고리즘은 간단하다.

최상위 진입점인 root set으로 부터시작해서 순회하면서 모조리 마킹하고 다닌다.

그 다음 마킹 안된녀석을 모조리 쓸어버리면 된다는 것이다.


하지만 이 알고리즘에는 사실 몇가지 문제점이 있다.


1. 커지면 시간이 너무 오래걸린다.

2. 메모리 단편화가 진행된다.


일단 1번부터 보도록 하자.


GC는 당연히 메모리가 부족할 때 발생할 것이다.

그렇다면 모든 메모리가 부족 할때 Full Scan을 하고나서 해제하는건 매우매우매우 비효율적이라는 것은 자명하다.

그래서 여러가지 변종 알고리즘들이 존재하게 되었다.


출처 - http://java-application-programming.blogspot.com/


jvm의 heap 영역은 개략적으로 위를 따른다.

크게 보면 3가지 영역으로 나눠지며 young은 다시 3가지 영역으로 분류된다.


young - 비교적 젊은 reference가 살아있는 곳

old - 특정 횟수 이상을 살아남은 reference가 살아있는 곳

permanent - Method Area의 메타정보가 기록된 곳


eden - young영역중에서도 특히 방금 막 생성된 녀석들이 있는 곳

survivor - 영역이 두개 존재하는데 eden에서 생존된 녀석들이 당분간 생존해 있는 곳


이렇게 영역을 나누는 이유는 Full GC를 막기 위해서다.

Full GC는 모든 heap영역을 몽땅 뒤져서 생존해 있는 녀석들을 모조리 죽여버리는 역활을 한다.

당연히 시간도 오래걸린다. 그래서 GC역시 세단계로 나눠져 있다.


Minor GC - young 영역만 뒤져서 다 죽인다

Major GC - old 영역까지 뒤져서 다 죽인다.

Full GC - permanent 영역까지 뒤져서 다 죽인다.


일반적으로 GC가 작동하는 알고리즘에 대해서 설명하도록 하겠다.

일단 기본적으로 Mark-Sweap을 하는건 동일하다.


Minor GC => young영역에서 발동되는 GC

이 minor GC에서 발동되는 알고리즘을 Stop And Copy알고리즘이라고 부른다.


young영역을 보면 eden과 두개의 survive로 나뉜다. 두개의 영역 중 하나만 사용한다. 즉 하나는 반드시 비어둔다.


참조(아랫사진 들 모두 동일) - https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

먼저 eden영역은 새로 들어오는대로 적재한다. 반드시 eden에 적재된다.


위 상황처럼 eden영역이 다차게 됬을경우 minor gc가 발동된다.

mark-sweap을 실행하지만 전영역을 실행하지는 않는다.

mark-sweap실행 시 해당 rootset에서 건너 뛴 변수가 young영역에 위치하지 않는다면,

더 이상 marking을 실행하지 않고 건너 뛰게 된다.

그렇기 때문에 사실 상 풀스캔을 하지 않고 young만 검사하게 된다.

여기서 여러분이 궁금해 하실게 그럼 old가 young을 참조하는 경우에는 어떻게 되느냐이다.

이는 아래서 설명하겠다.


마킹이 끝나면 죽을 녀석들은 죽이고 살릴녀석은 count를 증가시켜서 살린다.


여기서 또 다차서 gc가 발생할 경우 survivor영역의 모든 변수는 카운트를 1증가시키고 eden역시 카운트를 1증가시킨다.

그 후 반대편 survivor영역에 몰아넣는데 그 이유는 단편화를 피하기 위해서이다.

단편화가 진행되면 데이터를 접근하기 힘들어지는데 이를 피하기 위한 알고리즘이다.

즉 mark-sweap 알고리즘이 가진 약점을 해결하기 위해서 도입된 방식이다.


다시 gc가 발생할경우 과정을 반복한다.


알고리즘 작동 도중에 survivor에 있는 녀석이 임계값을 넘게 되면 old영역으로 승격이 된다.

예제에서는 8이지만 Oracle에서 사용하는 HotSpot JVM에서는 31이 기본값으로 되있으며 수정도 가능하다.

쉽게 말해서 오래 살아 남았으니 앞으로도 왠만해선 안죽겠지? 하는 마음에 이동시키고 더 이상 안보는 것이다.


위같은 stop and copy 알고리즘은 GC의 빈도를 높여서 자잘한 청소작업을 여러번하여

사용자로 하여금 프로그램이 정지되는 경험을 주지 않게하고 단편화역시 처리하는 아주 좋은 알고리즘이다.

이는 두가지의 대전제가 깔리기에 가능하다.


1. 대부분의 객체는 생성되고 나서 얼마 안되서 unreachable하게 된다.

2. 오래된 객체는 젊은 객체를 적게 참조한다.


이는 오랜 경험적인 측면에서 알게된 것이다. 1번 때문에 GC가 비교적 자주 발생시켜서 stop-the-world(모든 스레드 정지, 전 포스팅에서 설명함)를

적게 느끼는 효과를 가져오며 또한 Full Scan을 하지 않음으로써 시간을 절약시키는 효과까지 누린다.


하지만 아무래도 2번의 대전제가 마음에 걸린다. 적게지만 참조는 한다는 것인데 이러면 메모리 누수가 아니냐는 것이다.


참조 - https://d2.naver.com/helloworld/1329


old에서 young을 참조할 때는 카드 테이블이라는것을 만들어서 표시한다.

이는 old영역에서 적은 메모리(HotSpot JVM에서는 512MB)

minor gc작동시 root set에 이 card table을 포함시켜서 돌리게 되면 모두 지워낼 수 있게 되는 것이다.


Major GC와 Full GC 

둘은 영역만 다르지 알고리즘은 같다. Mark-Sweap-Compact를 사용한다.


Major GC는 eden이 다찼는데 survivor마저 다 찼다? 이 때 발동하게 된다.

Full GC는 young이 다찼는데 old마저 다찼을때 말동한다. 쉽게 말해서 잘 발동안한다는 뜻이다.


출처 - https://plumbr.io/handbook/garbage-collection-algorithms


일반적으로 더 이상 시간적, 공간적 여유도 없기 때문에 그냥 무식하게 모든 스레드에 lock(Stop The World)를 걸어버린다.

그리고 mark-sweap을 실행한다. 단편화를 줄이기위해서 모든 변수들을 앞당기게 되는데 이를 compact라고 부른다.

당연히 reference의 갱신과 메모리 이동시간 때문에 더 시간이 걸린다.

앞으로의 성능을 위해서 내리는 고육지책이다. 영원히 고통받을거 잠시 쎄게 고통받자는 거지.


major gc는 빨리 실행하기 위해서 여러 테크닉들이 있고 밖에 뒤저보면 좋은 칼럼들이 많다.

그러나 대세는 major gc를 되도록이면 안일으키는게 좋다는게 정론이다.

old영역은 그냥 데이터 적재용으로나 쓰고 최대한 minor gc만 일으키는걸 목표로 한다.

그게 가능하냐고? 완벽하게는 못하지만 프로그래머가 의도할 수는 있다.

Reference클래스의 사용 jvm옵션, 그외 코드 최적화를 통한 방식을 사용하는데 이를 gc튜닝이라고 부른다.(물론 여러가지 방법이 있다.)


그러나... 이러한 걸 다 설명하기에는 시간이 너무 오래걸리므로 여기서 설명을 마치려한다.

+ Recent posts