Chapter 3. Garbage Collector & Memory Allocation Strategy (1/2)
가비지 컬렉터와 메모리 할당 전략
1. 들어가며
동적 메모리 할당과 가비지 컬렉션 기술을 사용하기 위해 고려해야 할 점은 크게 3가지입니다.
어떤 메모리를 회수해야 하나?
언제 회수해야 할까?
어떻게 회수해야할까?
2. 어떤 메모리를 회수해야 하나?
참조 카운팅 알고리즘
힙을 청소하려면 가장 먼저 어떤 객체가 살아있고, 어떤 객체가 죽었는지 판단해야합니다.
보통은, 객체를 가리키는 참조 카운터를 통해, 참조하는 곳이 늘어나면 +1, 사라지면 -1을 하며, 0이 되면 객체를 사용할 수 없다고 합니다. 하지만, 자바에서는 참조 카운팅을 쓰지 않습니다.
그 이유는 순환 참조 문제입니다.
A와 B객체에는 모두 instance 라는 필드가 있고, A.instance = B, B.instance = A 라고 하고, 두 객체의 참조를 해제한 경우. 이 경우 외부에서 해당 객체에 접근할 길이 사라집니다. 외부에서 접근을 할 수 없기 떄문에 실제로는 삭제 대상이지만, 참조 카운트가 0이 아니기 때문에, 삭제가 되지 않습니다.
도달 가능성 분석 알고리즘
GC 루트라고 하는 루트 객체들을 시작으로 출발하여, 참조하는 다른 객체들로 탐색해 들어가는 알고리즘입니다. 탐색 과정에서 만들어지는 경로를 참조 체인(reference chain)
이라고 합니다. 루트로부터 시작하여 도달할 수 없는 객체는 더 이상 사용할 수 없는게 확실해집니다.
참조 체인을 찾지 못한 객체는 한번의 살아남을 기회가 있습니다. finalize()를 호출합니다.
finalize()가 필요 없는 개체
이미 finalize()를 한번 호출한 경우, 실행할 필요 없음 으로 처리
두 경우에 포함되면 회수됩니다.
여러 참조 상태
참조 됐다.
참조되지 않았다
버리기는 아까운 객체다.
버리기는 아까운 객체란, 메모리가 여유롭다면 그냥 두고, 메모리가 부족하면 회수하는 객체(예. 캐시 기능). 이를 위해 참조 개념을 확장하여 네 가지로 구분하기 시작했습니다.
강한 참조: 절대 회수되지 않는 참조
부드러운 참조: 유용하지만 필수는 아닌 객체. 메모리 오버플로가 나기전에 두번째 회수를 위해 회수 목록에 추가. 두 번째 회수 후에도 메모리가 부족하면 메모리 오버플로.
약한 참조: 다음번 가비지 컬렉션까지만 살아 있는 객체.
유령 참조: 가장 약한 참조로, 목적은 대상 객체가 회수될 때 알림을 받기 위함. 객체 수명에 어떤 영향도 없으며, 유령 참조를 통해 객체 인스턴스를 가져오는 것마저 불가능.
메서드 영역 회수하기
자바 가상 머신 명세
에 따르면, 메서드 영역은 가비지 컬렉션 대상입니다.
다만, 어떤 클래스를 GC로 회수하려면:
그 클래스에 더 이상 어떤 객체도 없어야 하고,
ClassLoader도 GC 대상이어야 하고,
해당 클래스에 해당하는 객체를 아무 곳에서 참조하지 않고, 리플랙션 기능으로 이 클래스의 메서드를 이용하는 곳도 전혀 없어야 합니다.
메서드 영역 회수 조건은 까다롭고, 비용 효율이 좋지 않기 때문에, JVM은 이 영역에 대해서는 GC를 자주 하지 않거나, 아예 안 하기도 합니다.
3. 가비지 컬렉션 알고리즘
1. 세대 단위 컬렉션 이론
힙을 세대별로 나눠서 관리를 합니다. 세대로 나눠서 힙을 관리하는 이론은 두 가지 가정을 합니다.
약한 세대 가설: 대다수 객체는 일찍 죽는다.
강한 세대 가설: GC 과정에서 살아남은 횟수가 늘어날수록 더 오래 살 가능성이 커진다.
(추가) 세대 간 참조 가설: 세대 간 참조의 개수는 같은 세대 안에서의 참조보다 훨씬 적다.
단순히 메모리를 나누는게 아닌, 목적이 있습니다. 한 세대에서 다른 세대의 객체를 참조하는 상황이 있습니다.
Minor GC만 돌리고 싶어도, 신세대에 속하지만 구세대에 참조 중인 객체가 존재할 수 있음
Major GC만 돌리고 싶어도, 구세대에 속하지만 신세대에 참조 중인 객체가 존재할 수 있음
따라서 살아남을 객체를 찾으려면 도달 가능성을 분석할 때, 고정된 GC 루트들뿐 아니라, 다른 세대 객체까지 모두 탐색해야 합니다. 이는 비효율적이고, 추가적인 세 번째 가설이 필요합니다.
세번째 가설에 따르면, 세대간 참조의 수는 매우 적기 때문에, 구세대 전체를 훑(어야하지만)는건 낭비입니다.
이를 효율적으로 해결하기 위해 기억집합을 활용합니다. 기억 집합은, 구세대를 조각으로 분리하고, 그 조각에 세대 간 참조가 있는지 기록하는 방식입니다. (조각으로 나누는 이유는 효율성. 3번째 가설때문.)
메모리 효율성: 전체 탐색
시간 효율성: 각각의 구세대 기업집합을 조각으로 나누지 않고 관리.
그 중간인, 기억 집합을 조각화해서 관리하는 방식이 채택.
2. 마크-스윕 알고리즘 (최초의 GC 알고리즘)
3. 마크-카피 알고리즘(신세대)
회수할 객체가 많아 질수록 효율이 떨어지는 마크-스윕 알고리즘을 해결하기 위해 사용
가용 메모리를 똑같은 크기로 2개로 나눕니다. 한 공간에서 살아남은 객체를 다른 공간으로 복사하고, 기존 공간은 모두 삭제
+파편화 해결
-가용 메모리를 절반으로 줄여, 메모리 낭비(STW 자주 발생)
지금은 신세대(에덴, 생존자1, 생존자2로 3개로 나눠서. 비율 8:1:1)로 관리되도록 발전했습니다. 다만, 생존자에 들어갈 수 있는 만큼의 공간이 부족하면 구세대 공간에 그냥 넣습니다.
4. 마크-컴팩트 알고리즘(구세대)
마크 카피는 복사하기 때문에, 객체 생존률이 높으면 복사할게 많아져서 효율이 나쁘기도하고, 보증용 공간을 따로 둬야하기 떄문에, 구세대에는 적합하지 않습니다.
구세대를 고려한 알고리즘이 마크-컴팩트이며, 컴팩트 단계에서 생존한 모든 객체를 메모리 영역의 한쪽 끝으로 모은 다음, 나머지 공간을 한꺼번에 비웁니다.
신세대는 마크-카피: 살아남는 객체가 적을거라는 1번 가설 적용.
구세대는 마크-컴팩트: 살아남는 객체가 많을거라는 2번 가설 적용
4. 핫스팟 알고리즘 상세 구현
루트 노드 열거
도달 가능성 분석에서 GC 루트로부터 참초 체인을 찾는 작업에서 GC 루트를 결정하는 과정. 이떄는 무조건 STW가 발생.(여기서 모든 스레드가 정지함. 참조 but 체인 찾는 과정에서는 별도 스레드가 수행 가능)
루트 노드 열거 과정에서, 모든 스레드가 정지 한 후, 실행 콘텍스트와 전역 참조의 위치를 빠짐없이 확인해야하지만, "정확한 가비지 컬렉션"으로 인해 그럴 필요가 없습니다.
자바는 OopMap 이라는 데이터 구조를 사용합니다. (OopMap 덕분에 참조 위치만 추려서 빠르게 탐색 가능)
클래스 로딩이 완료되면 객체에 포함된 각 데이터의 타입을 확인
JIT 컴파일 과정에서 스택의 어느 위치와 어느 레지스터의 데이터가 참조인지 기록.
안전지점
참조 관계나 OopMap의 내용을 변경할 수 있는 명령어가 너무 많고, 명령어마다 OopMap을 만들어 넣으면 메모리 효율성이 낮다. 결국 모든 명령어에 OopMap을 생성하지는 않는다.
다만, 안전 지점으로 특정 위치에만 기록을 하고, 각 스레드가 이 "안전 지점"에 도달할때까지는 STW를 하지 않습니다. 안전 지점 위치에 따라 컬렉터가 너무 오래 기다리거나, 메모리가 너무 많이 낭비될 수 있습니다.
선제적 멈춤: GC가 실행되면 시스템이 모든 사용자 스레드를 인터럽트 시도. 스레드가 안전 지대에 도착할때 까지 반복.
자발적 멈춤: 플래그 비트를 설정하고, 각 스레드가 실행중에 플래그를 폴링하여 멈춰야 하는지 판단.
안전 지점 후보는, 장시간 실행 시간이 낮은 구간이 후보가 됩니다.
정확히는, 프로그램 흐름을 바꾸는 요소들(메서드 호출, 순환문, 예외 처리 등 명령어 흐름이 다중화 되는 명령어)를 만나는 지점들이 후보가 됩니다.
이때 인터럽트 또는 폴링 방식이 효율적이어야 하고, 메모리 보호 트랩 이라는 방법을 써서 효율적으로 수행할 수 있도록 단순화했습니다.
하지만 문제가 발생합니다. 실행 중이 아닌 프로그램/쓰레드의 경우는 인터럽트 요청에 응답할 수 없고, 폴링도 할 수 없습니다. 이를 위해 "안전 지역" 이라는 개념이 생겼습니다.
안전 지역은 일정 코드 영역에서는 참조 관계가 변하지 않음을 보장하고, 이 지역 안에서는 GC가 시작해도 괜괜찮다는 지점입니다.
기억 집합과 카드 테이블
세대 간 참조 객체를 기록하려면 차지하는 공간과 관리 비용이 모두 높습니다. GC시 컬렉터는 기억 집합을 이용해 특정 비회수 영역에서 회수 영역을 가리키는 포인터가 존재하는지만 확인하면 됩니다.
기억 집합을 구현하는 방식:
워드 정밀도: 레코드 하나가 메모리의 워드 하나에 매핑.
객체 정밀도: 레코드 하나가 객체 하나에
카드 정밀도: 레코드 하나가 메모리 블록 하나에 매핑. 특정 레코드가 마킹되어 있다면, 해당 블록에 세대 간 참조를 지닌 객체가 존재한다는 뜻.
카드 정밀도로 기억 집합을 구현한 것을 카드 테이블 이라고 합니다.
카드 테이블은 기록 정밀도와 힙 메모리의 매핑 관계 등을 정의하여 기억 집합을 구체적으로 구현한 방법 중 하나입니다.
카드 페이지 하나의 메모리는 보통 하나 이상의 객체가 들어있고, 이 객체들 중 하나에라도 세대 간 포인터를 갖는 필드가 이쓰면, 해당 원소 카드를 1로 표시하고, 이를 더럽혀졌다고 합니다. 객체를 회수 할때는 카드 테이블에서 더럽혀진 원소만 확인하면 됩니다.
이제 GC Root의 스캔 범위를 줄이는 문제를 해결했습니다. 이젠, 아래의 문제만 남았습니다.
언제 더럽혀지는가?
더럽히는 주체는 무엇인가?
쓰기 장벽
가상 머신 수준에서 '참조 타입 필드 대입' 시 끼어드는 AOP 애스펙트에 비유할 수 있습니다.
사전 쓰기 장벽: 대입 전 쓰기 장벽
사후 쓰기 장벽: 대입 후 쓰기 장벽.
카드 테이블은 멀티스레드 시나리오에서 거짓 공유 문제가 발생할 수 있다. 거짓 공유는 동시성과 관련되어 있는데, 이는
위 조건문으로 쉽게 회피할 수 있다.
동시 접근 가능성 분석
이론적으로 도달 가능성 분석은 STW 상태에서 진행되어야합니다. 왜냐하면 중간에 참조가 변경될 수 있기 떄문입니다(일관성).
이떄, tri-color marking 기법을 통해 STW를 줄일 수 있습니다.
흰색: GC가 방문한 적 없는 객체. 도달 가능성 분석을 시작하면 처음에는 모든 객체가 흰색입니다. 분석이 끝나고도 흰색은 도달 불가
검은색: GC가 방문한 객체. 검은 객체는 생존함을 뜻합니다.
회색: GC가 방문한 적은 있으나, 이 객체를 가리키는 참조 중 스캔을 완료하지 않은 참조가 존재
tri-color marking과 프로그램 스레드가 동시에 수행되면 어떻게 될까?
죽은 객체를 살았다고 잘못 표시
살아 있는 객체를 죽었다고 표시(CRITICAL!!)
객체가 사라지는 두번째 상황에서, 검은색이어야할 객체를 하얀색으로 칠하는 상황은 아래 두 조건이 만족할때만 나타납니다
사용자 스레드가 흰색 객체로의 새로운 참조를 검은색 객체에 추가
사용자 스레드가 회색 객체에서 흰색 객체로의 직간접적인 참조를 삭제
위 두 조건중 하나만 깨뜨리면 문제를 해결할 수 있습니다.
해결 방법은 두가지 입니다. 증분 업데이트와 시작 단계 스냅숏
증분 업데이트: 검은색 객체에 흰색 객체로의 참조가 추가되면 새로 추가된 참조를 따로 기록합니다. 동시 스캔이 끝난 후 기록해 둔 검은색 객체들을 루트로 하여 다시 스캔합니다. "검은색 객체에 흰색 객체로의 참조가 추가되면 검은색이 다시 회색으로 바뀐다"
시작 단계 스냅숏: 회색 객체가 흰색 객체로의 참조 관계를 끊으려 하면 기록합니다. 동시 스캔이 끝난 후 기록해둔 회색 객체들을 루트로 하여 다시 스캔합니다. "참초 관계 삭제 여부와 상관없이 스캔을 막 시작한 순간의 객체 그래프 스냅숏을 기준으로 스캔한다"
쓰기 장벽(AOP)를 기준으로 사용자 스레드가 기록을 합니다.
5. 클래식 가비지 컬렉터
1. 시리얼 컬렉터
단일 스레드로 동작
회수가 완료 될때 까지 모든 작업 스레드가 멈춰 있어야합니다.
신세대, 구세대 구분없이 사용됩니다.
메모리 사용량이 가장 적습니다.
단일 코어 프로세서 또는 코어 수가 적은 환경이라면 스레드 상호작용에 의한 오버헤드가 없습니다.
-XX:+UserSerialGC 매개변수 사용
2. 파뉴 컬렉터
여러 스레드를 활용하여 시리얼 컬렉터를 병렬화한 버전.
스레드 회수에 멀티스레드를 이용
신세대용 컬렉터로 인기가 많습니다.
3. 패러렐 스캐빈지 컬렉터 (aka 처리량 컬렉터)
신세대용 컬렉터 (PS 컬렉터)
마크-카피 알고리즘에 기초
여러 스레드를 이용해 병렬로 회수
STW를 줄이는 곳에 집중하지 않고, 처리량을 제어하는게 목표.
4. 시리얼 올드 컬렉터
시리얼 컬렉터의 구세대용 버전
단일 스레드 컬렉터
마크-컴팩트 알고리즘 사용
PS 컬렉터와 함께 사용되거나, CMS 컬렉터가 실패할 떄를 위한 대비책으로 사용(동시 회수 중 동시 모드 실패)
5. 페러렐 올드 컬렉터
PS 컬렉터의 구세대용 버전
멀티스레드를 이용한 병렬 회수
마크-컴팩트 알고리즘을 기초
6. CMS 컬렉터
표시와 쓸기 단계 모두를 사용자 스레드와 동시에 수행.
STW 시간을 최소로 줄이는데 목표
마크-스윕 알고리즘 기초
최초 표시-동시 표시-재표시-동시 쓸기 단계로 과정이 진행됩니다.
7. G1 컬렉터
부분 회수라는 컬렉터 설계 아이디어.
리전을 회수 단위로 하는 메모리 레이아웃
큰 객체를 저장하기 위한 거대 리전(humorous region) 유형을 사용
신세대, 구세대로 나뉘어지지 않습니다.
최초 표시-동시 표시-재표시- 복사 및 청소 과정을 거칩니다.
Last updated