https://dev-dx2d2y-log.tistory.com/138
[CS] JVM 밑바닥까지 파헤치기 독서 #1 - 자바 런타임 메모리 영역
C와 C++은 객체 (C에는 객체가 없지만)의 생성, 관리, 삭제까지 모두 관리할 책임을 가지지만, 자바에서는 JVM이 대체적으로 이 역할을 수행한다. JVM에 대해서 배우면서 가장 먼저 자바 메모리 영역
dev-dx2d2y-log.tistory.com
저번에는 자바 메모리 구조와 자바에서 객체를 어떻게 저장하고 불러오는지에 대해서 알아보았다. 추가로 각 메모리 영역별 오버플로우에 대해서도 알아보았고.
앞으로 당분간은 가비지컬렉터와 메모리 할당에 대해서 알아보도록한다.
가비지컬렉터(GC)는 메모리영역에서 더 이상 참조되지 않는 객체를 찾아서 삭제하고 메모리 공간을 확보하는 역할을 프로그램 실행 도중 동적으로 수행한다.

자바 런타임 데이터 영역에서..
프로그램카운터 레지스터는 별로 큰 공간을 차지하지 않고,
가상머신스택과 네이티브 메서드스택은 스레드가 하나 열릴 때마다 생성되고 소멸하고를 반복한다. 스택 프레임에 할당되는 메모리 크기는 클래스가 만들어질 때 결정되니까 어느정도 예측이 가능하다.
반면 클래스 정보가 저장되는 메서드영역과, 특히 실제 자바 생성된 자바 객체들이 저장되는 힙메모리는 예측 불가능하다. 하나의 인터페이스더라도 구현체가 어떻게 구현을했냐에 따라서 힙메모리와 메서드영역에 저장되는 객체의 크기는 천차만별로 달라진다.
그래서 힙메모리와 메서드 영역의 메모리 할당과 회수는 동적으로 이루어진다. 그리고 가비지 컬렉터는 바로 이 기능에 집중한다.
가비지 컬렉터를 사용하는 이유는 바로 이것이다.
혹시 돌아가셨나요?
GC의 가장 기본이 되는 기준은 '객체가 죽었는가?'이다.
이 '죽었는가?'는 프로그램 코드에서 어떠한 방식으로도 객체가 다시 사용될 수 없는, 즉 아무 변수도 해당 객체를 참조하지 않아 더이상 해당 객체에 접근할 수 없다는 뜻이다.
참조 카운팅 알고리즘
https://dev-dx2d2y-log.tistory.com/159
[CS] 참조 카운팅 알고리즘과 파이썬의 순환 검출 알고리즘에 대해서
가비지 컬렉션에 대해서 배울 때, 객체가 '죽었는지' 여부를 판단하기 위해서는 그 판단 기준이 필요하다.이 '죽었는지'는 프로그램 코드에서 어떠한 방식으로도 객체가 다시 사용될 수 없는, 즉
dev-dx2d2y-log.tistory.com
대략적인 참조 카운팅 알고리즘은 각 객체를 가리키는 참조 카운터를 두고, 각 카운터에는 그 객체를 참조하는 다른 객체의 수를 저장한다. 이후 프로그램이 실행됨에따라 객체를 참조하는 객체의 수가 점점줄어 0이되면 해당 객체가 참조해제된 것으로 인식하고 GC의 대상이된다.
위 방식이 메모리를 다소 많이 사용하지만 직관적이고, 간단하다. 다만 문제는 순환참조문제
참조가 해제된 여러 객체가 서로를 참조 중일 때, 참조 카운터에 저장되는 값은 참조가 해제되더라도 0이 아니게되고 결국에는 메모리가 회수되지 않는다. GC의 제대로된 목적이 이루어지지 않는 순간.
public class Main {
public static void main(String[] args) {
ReferenceCountingGC.testGC();
}
}
class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
인위적으로 순환참조를 발생시키는 코드
objA와 objB는 참조가 해제되었지만 내부 인스턴스들이 서로 참조 중이라 기본적인 참조 카운팅 알고리즘에서는 메모리가 제대로 회수되지 않는다. 파이썬은 순환검출 알고리즘을 통해 이를 해결했지만, 자바는 그냥 참조카운팅 알고리즘을 사용하지 않았다. 그럼 럼 자바는 어떻게 객체가 GC의 대상이 되는지 판단할까?
도달 가능성 분석 알고리즘
자바, C#, JS 등 오늘날의 주류 언어들은 도달 가능성 분석 알고리즘을 사용한다.

도달 가능성 분석 알고리즘은 'GC 루트'라고하는 루트 객체들을 시작 노드 집합으로 사용한다.
그 시작 노드들에서 시작해서 연결된 다른 객체들을 파악한다. 이 경로를 참조체인(reference chain)이라고 칭한다.
만약 GC루트로부터 도달 불가능한 객체가 있다면(순환참조 같은 경우) 메모리 회수 대상이된다. 위에서 GC 루트와 연결된 객체 A, B, C, D는 GC 대상이 아니지만 GC 루트에서 접근할 수 없지만 참조 카운팅은 1 (또는 그 이상)인 객체 E, F, G는 GC의 대상이된다.
https://dev-dx2d2y-log.tistory.com/161
[Java] finalize()는 무엇이고, 왜 지양해야하는가?
자바에서 가비지컬렉터는 객체가 더 이상 참조가 불가능한지 판단하면 객체를 제거하게된다.죽은 객체의 사회하지만 가비지컬렉터가 더이상 참조가 불가능하다고 판단한 객체가 반드시 '제거
dev-dx2d2y-log.tistory.com
GC의 대상으로 결정된 경우에는 finalize()를 실행시켜야하는 경우에 따라서 나뉜다. 실행시켜야하는 경우에는 F-큐에 모아두었다가 나중에 일괄적으로 실행하고, 그렇지 않은 경우는 제거한다. 위의 게시글을 참고.
GC 루트는 어떻게?
자바에서 GC루트로 사용 가능한 객체는 정해져있다.
1. 가상 머신 스택에서 참조 중인 객체(스택 프레임의 지역 변수 테이블에 올라가있는 객체): 현재 실행 중인 스레드, 메서드에서 사용 중인 지역변수, 매개변수 등
2. 메서드 영역에서 클래스가 정적으로 참조 중인 객체: 참조형 정적변수
3. 메서드 영역에서 상수로 참조되는 객체: 문자열 테이블 안의 참조
4. 네이티브 메서드가 참조 중인 객체
5. 자바 가상 머신 내부에서 사용되는 참조: 기본적인 클래스 정보를 나타내는 Class 객체, 클래스로더, OOME, NPE 등 일부 예외객체 등
6. 동기화락 (synchronized 키워드)로 잠긴 모든 객체
7. 자바 가상 머신 내부 상황을 반영하는 JMXBean
등이 있으며, 이외에도 가비지컬렉터의 종류나 현재 회수 중인 메모리 영역에 따라 임시로 GC루트로 설정될 수 있다.
대체적으로 이미 객체가 '죽어있지 않다'는 것이 자명한 객체들, 또는 프로그램의 시작부터 끝까지 '죽어있지 않은' 객체들이 GC 루트 집합에 사용되는 객체로 정해진다.
가비지컬렉션 알고리즘
https://dev-dx2d2y-log.tistory.com/162
[Java] GC의 객체회수과정은 어떻게 일어나는가? - 마크-스윕, 마크-카피, 마크-컴팩트
가비지컬렉션은 '도달 가능성 분석 알고리즘'을 따른다. 이의 세부적인 구현방식은 GC에 따라서, 가상머신에 따라서 차이가 있다. 앞으로 몇 개의 게시글에서 이 가상 머신의 GC 알고리즘에 대해
dev-dx2d2y-log.tistory.com
가장 근본은 위의 도달 가능성 분석 알고리즘을 통해 일어난다. 남은 것은 객체를 '어떻게 회수할 것인가?'
위 글에서는 가비지컬렉션 알고리즘의 기초가 되는 세대 단위 컬렉션 이론과,
회수할 객체를 표시하고 한 번에 일괄적으로 회수하는 마크-스윕 방식 (가장 근본이 됨)
메모리 영역을 반으로 나눠 처음에는 한 쪽 반을 쓰다가 가비지컬렉션 이후 살아남은 객체들을 나머지 반에 연속적으로 배치하는 마크-카피 방식 (신세대 영역에서 주로 사용함)
마크-스윕 방식과 동일하나 가비지컬렉션 이후 살아남은 객체들을 한 쪽 끝에서 연속적으로 배치시키는 마크-컴팩트 방식 (구세대 영역에서 주로 사용함)
이 있다. 자세한 것은 위 글을 참고.

결국 다시 참조 카운팅 알고리즘의 순환참조 문제로 돌아와보면,
객체 C는 메인(GC루트)에서 연결되므로 도달 가능하며, 객체A와 객체B는 GC루트에서 도달할 수 없으므로 가비지컬렉션의 대상이된다. 즉, 순환참조 문제 해결.
이렇게 자바에서 객체를 어떻게 회수하는지 알고리즘의 이론적인 부분에 대해서 알아보았다.
다시 정리를해보면,
1. 힙메모리 영역에서는 객체의 크기가 천차만별이기 때문에 유연한 개발을 위해서 메모리 회수를 동적으로 수행한다. 가비지컬렉션은 이 때문에 존재한다.
2. 객체가 '회수 되어야하는가?'라는 것을 판단하기 위한 가장 기본적인 알고리즘은 특정 객체를 참조하고 있는 객체의 수를 세는 '참조 카운팅 알고리즘'
3. 그러나 참조 카운팅 알고리즘은 참조가 해제된 객체들끼리 참조하는 '순환참조' 문제를 해결할 수 없다. 파이썬은 이를 위해 '순환검출' 알고리즘을 사용하여 참조 카운팅 알고리즘을 보조하는 역할을 수행한다.
4. 자바는 참조 카운팅 알고리즘을 사용하는 것이 아닌, 가비지컬렉션이 시작될 당시 살아있음이 자명한 객체를 기준삼아서 연결된 객체들을 생존시키고, 연결되지 않은 객체들은 회수대상으로 삼는 '도달 가능성 분석 알고리즘'을 사용한다.
5. 만약 객체가 회수대상이된다면, 가상머신은 객체의 finalize() 함수를 호출할 필요가 있는지, 필요가 있다면 이미 호출되었는지를 조사한다. 만약 finalize()가 호출이 되지 않았고, 호출할 필요가 있다면 F-큐로 이동시킨 후 얼마 후에 종료자 스레드를 실행시켜 finalize() 메서드를 실행시킨다. 만약 여기서 GC루트와의 연결이된다면 객체는 다시 살아난다.
6. finalize()를 부르지도 않고, 부활에도 실패한 객체들은 GC를 통해 메모리 공간이 회수되게된다.
7. 객체들은 여러 번 GC에서 생존한 객체는 앞으로도 살아남을 가능성이 크므로 GC에서 생존한 횟수에 따라서 크게 신세대와 구세대로 구분한다. 신세대는 GC를 자주, 반복적으로 수행하게되고, 구세대는 신세대보다 적은 빈도수로 GC를 수행한다.
8. 대표적인 회수방식은 회수할 객체를 표시한 후 일괄적으로 회수하는 마크-스윕 알고리즘이 있다. 문제는 많은 객체를 회수할 때의 효율성 저하와 메모리 공간에 빈공간이 많이 생기는 파편화 문제.
9. 효율성 저하 및 파편화 문제를 해결하기 위해 메모리 공간을 반으로 나눠 한 쪽에서만 객체를 저장하다가 용량이 부족하면 가비지컬렉션을 수행해 객체를 지운 다음 나머지 반쪽에 연속적으로 객체를 배치하는 마크-카피 알고리즘이 있다. 복사 과정에서 메모리 소비가 다소 있기 때문에 많은 객체들이 회수되는 신세대 영역에서 주로 사용된다.
10. 회수할 객체를 표시한 후 수거한 다음에 살아남은 객체들을 메모리 한 쪽으로 연속적으로 옮겨담는 마크-컴팩트 알고리즘도 있다. 이는 회수할 객체가 적은 구세대 영역에서 주로 사용되지만, 메모리 주소를 옮긴다는 단점 때문에 마크-스윕 알고리즘과 혼용하는 경우도 있다.
'CS > JVM' 카테고리의 다른 글
| [JVM] 가비지컬렉터는 어디에서 실행되어야하는가? - 안전지점(Safe Region) (0) | 2025.12.27 |
|---|---|
| [JVM] OopMap으로 참조체인 내 속한 객체 찾기 (0) | 2025.12.27 |
| [JVM] GC의 객체회수과정은 어떻게 일어나는가? - 마크-스윕, 마크-카피, 마크-컴팩트 (0) | 2025.12.26 |
| [JVM] 참조 카운팅 알고리즘과 파이썬의 순환 검출 알고리즘에 대해서 (0) | 2025.12.25 |
| [JVM] JVM 밑바닥까지 파헤치기 독서 #4 - 메모리 영역 별 오버플로우 (1) | 2025.12.06 |
