관리 메뉴

The Nirsa Way

[JAVA] 가비지 컬렉션은 왜 필요한가: 활성 객체와 Mark and Sweep 본문

Development/JAVA

[JAVA] 가비지 컬렉션은 왜 필요한가: 활성 객체와 Mark and Sweep

KoreaNirsa 2026. 7. 25. 14:33
반응형
가비지 컬렉션은 왜 필요한가: 활성 객체와 Mark and Sweep

자바에서는 객체를 생성할 때 개발자가 직접 메모리를 할당하거나 반환하지 않습니다.

User user = new User();

위 코드처럼 new 키워드를 사용하면 객체가 생성되지만, C나 C++처럼 사용이 끝난 메모리를 직접 해제하는 코드는 작성하지 않습니다.

이러한 메모리 관리를 대신 수행하는 기능이 가비지 컬렉션(Garbage Collection, GC)입니다.

하지만 가비지 컬렉션을 단순히 “사용하지 않는 객체를 자동으로 삭제하는 기능”이라고만 이해하면 다음과 같은 의문이 생길 수 있습니다.

  • JVM은 어떤 객체가 사용 중인지 어떻게 판단할까요?
  • 변수가 null이면 객체가 바로 제거될까요?
  • 객체끼리 서로 참조하고 있다면 계속 살아남을까요?
  • 객체가 제거된 공간은 어떻게 다시 사용될까요?

이 질문을 이해하려면 먼저 활성 객체가비지 객체를 구분해야 합니다.

먼저 간단히 정리하면

  1. 활성 객체는 현재 프로그램에서 접근할 수 있는 객체입니다.
  2. 가비지 객체는 더 이상 접근할 수 없는 객체입니다.
  3. Mark and Sweep은 활성 객체를 표시한 뒤, 표시되지 않은 객체가 차지한 메모리를 회수합니다.

객체를 계속 만들기만 하면 어떤 문제가 생길까

프로그램은 실행되는 동안 수많은 객체를 생성합니다.

웹 애플리케이션을 예로 들면 요청 정보, 응답 데이터, 조회 결과, 문자열, 컬렉션과 같은 객체가 지속적으로 만들어집니다.

public String createMessage(String name) {
    String message = name + "님, 환영합니다.";
    return message;
}

메서드가 실행될 때마다 문자열 객체가 생성될 수 있습니다. 하지만 모든 객체가 프로그램 종료 시점까지 필요한 것은 아닙니다.

어떤 객체는 잠시 사용된 뒤 더 이상 필요하지 않게 됩니다.

이처럼 사용이 끝난 객체가 힙 메모리에 계속 남아 있다면 다음과 같은 문제가 발생합니다.

객체 생성
   ↓
힙 메모리 사용량 증가
   ↓
사용이 끝난 객체도 계속 공간 차지
   ↓
새로운 객체를 저장할 공간 부족
   ↓
메모리 부족 또는 프로그램 성능 저하

따라서 JVM은 더 이상 사용할 수 없는 객체를 찾아 해당 객체가 차지했던 메모리를 다시 사용할 수 있도록 만들어야 합니다.

이 작업을 수행하는 것이 가비지 컬렉션입니다.


객체가 힙에 존재하면 살아 있는 객체일까

객체가 힙 메모리에 존재한다는 사실만으로는 살아 있는 객체라고 판단할 수 없습니다.

가비지 컬렉션에서 중요한 기준은 "현재 실행중인 프로그램이 해당 객체에 다시 접근할 수 있는가?" 입니다.

프로그램에서 계속 접근할 수 있는 객체는 활성 객체, 더 이상 접근할 수 없는 객체는 가비지 객체로 구분할 수 있습니다.

구분 의미 가비지 컬렉터의 처리
활성 객체 프로그램에서 계속 접근할 수 있는 객체 메모리에 유지합니다.
가비지 객체 프로그램에서 더 이상 접근할 수 없는 객체 메모리 회수 대상이 됩니다.

가비지 객체를 회수한다는 것은 객체를 임의로 삭제한다는 의미가 아니라, 더이상 접근할 수 없는 객체가 차지하던 메모리 공간을 이후 객체 할당에 다시 사용할 수 있도록 만드는 작업입니다.


참조가 사라지면 객체는 어떻게 될까

다음 코드를 살펴보겠습니다.

public class GarbageExample {

    public static void main(String[] args) {
        User user = new User("Kim");

        user = null;
    }
}

처음에는 user 변수가 User 객체를 참조합니다.

user ──────▶ User 객체

이 상태에서는 프로그램이 user 변수를 통해 객체에 접근할 수 있으므로 활성 객체입니다.

하지만 다음 코드가 실행되면 참조 관계가 사라집니다.

user = null;
user ──────▶ null

              User 객체
              접근 경로 없음

이제 기존의 User 객체에 접근할 수 있는 다른 참조가 없다면 해당 객체는 가비지 객체가 될 수 있습니다.

다만 변수에 null을 대입했다고 해서 객체가 그 순간 즉시 메모리에서 제거되는 것은 아닙니다.

정확한 흐름은 다음과 같습니다.

객체에 대한 참조 제거
        ↓
객체에 접근할 수 없는 상태
        ↓
가비지 컬렉션 대상이 될 수 있음
        ↓
GC가 실행되는 시점에 메모리 회수

즉, null은 객체를 직접 삭제하는 명령이 아닙니다. 객체로 향하는 참조를 제거하는 표현에 가깝습니다.


활성 객체는 어떻게 찾을까

JVM은 힙 메모리의 객체를 무작정 하나씩 살펴보며 사용 여부를 판단하지 않습니다.

먼저 가비지 컬렉션 루트(GC Root)라고 부르는 출발점에서 객체 참조를 따라갑니다.

위 구조에서 GC Root로부터 도달할 수 있는 객체는 다음과 같습니다.

객체 A
객체 B
객체 C
객체 D

이 객체들은 프로그램이 계속 접근할 가능성이 있으므로 활성 객체로 판단합니다.

반면 객체 X와 객체 Y는 서로를 참조하고 있지만 GC Root에서 접근할 수 없습니다.

객체 X ⇄ 객체 Y

두 객체가 서로 참조하고 있다고 해도 프로그램의 유효한 출발점에서 접근할 수 없다면 가비지 객체가 될 수 있습니다.

따라서 자바의 가비지 컬렉션은 단순히 참조 개수만 세는 방식과는 다릅니다.

핵심 기준은 도달 가능성(Reachability)입니다.

상태 의미 분류
GC Root에서 참조 경로를 따라 도달 가능 프로그램이 다시 사용할 수 있음 활성 객체
GC Root에서 어떤 경로로도 도달 불가능 프로그램이 다시 사용할 수 없음 가비지 객체

GC Root의 종류와 구체적인 탐색 방식은 별도의 글에서 자세히 다룰 수 있습니다.


Mark and Sweep이란

Mark and Sweep은 가비지 컬렉션의 기본 원리를 설명할 때 사용하는 대표적인 알고리즘입니다.

이름 그대로 두 단계로 진행됩니다.

Mark: 살아 있는 객체를 표시합니다.
Sweep: 표시되지 않은 객체의 공간을 회수합니다.

여기서 중요한 점은 가비지 컬렉터가 처음부터 가비지 객체를 직접 찾아다니는 것이 아니라는 점입니다.

먼저 GC Root에서 도달 가능한 활성 객체를 찾습니다. 이후 활성 객체로 표시되지 않은 나머지 객체를 가비지 객체로 판단합니다.


6. Mark 단계: 살아 있는 객체를 표시합니다

Mark 단계에서는 GC Root에서 시작해 참조 관계를 따라가며 도달할 수 있는 객체를 찾습니다.

예를 들어 다음과 같은 객체 그래프가 있다고 가정해 보겠습니다.

GC Root에서 객체 A로 접근할 수 있고, 객체 A가 객체 B와 객체 C를 참조하고 있습니다.

따라서 객체 A, B, C는 모두 활성 객체로 표시됩니다.

[Mark 완료]

객체 A: 표시됨
객체 B: 표시됨
객체 C: 표시됨
객체 X: 표시되지 않음
객체 Y: 표시되지 않음

객체 X와 객체 Y는 서로 연결되어 있지만 GC Root에서 도달할 수 없으므로 활성 객체로 표시되지 않습니다.

Mark 단계의 목적은 객체를 즉시 제거하는 것이 아닙니다.

먼저 반드시 남겨야 할 객체를 확정하는 단계입니다.

이 구분이 완료되어야 다음 단계에서 안전하게 메모리를 회수할 수 있습니다.


 Sweep 단계: 표시되지 않은 공간을 회수합니다

Sweep 단계에서는 Mark 단계에서 표시되지 않은 객체를 확인합니다.

표시되지 않았다는 것은 GC Root에서 도달할 수 없다는 의미이므로, 해당 객체가 차지했던 메모리는 회수 대상이 됩니다.

Mark 단계 완료

[객체 A: 활성]
[객체 X: 가비지]
[객체 B: 활성]
[객체 Y: 가비지]
[객체 C: 활성]

Sweep 단계 이후에는 다음과 같이 바뀔 수 있습니다.

[객체 A: 활성]
[빈 공간]
[객체 B: 활성]
[빈 공간]
[객체 C: 활성]

회수된 공간은 이후 새로운 객체를 생성할 때 다시 사용할 수 있습니다.

Order order = new Order();

새로운 Order 객체가 생성되면 JVM은 회수된 메모리 공간을 새로운 객체 할당에 활용할 수 있습니다.


Mark and Sweep의 전체 처리 순서

Mark and Sweep의 흐름을 정리하면 다음과 같습니다.

단계별 역할은 다음과 같습니다.

단계 수행 내용
탐색 시작 GC Root를 기준으로 객체 참조를 따라갑니다.
Mark 도달 가능한 활성 객체를 표시합니다.
Sweep 표시되지 않은 객체가 차지한 공간을 회수합니다.
재사용 회수한 공간을 새로운 객체 할당에 사용합니다.

 


객체를 회수했는데도 문제가 생길 수 있을까

Sweep 단계에서 가비지 객체의 메모리를 회수하면 사용 가능한 공간이 늘어납니다.

하지만 회수된 공간이 힙의 여러 위치에 나뉘어 있을 수 있습니다.

[활성 객체][빈 공간][활성 객체][빈 공간][활성 객체]

사용 가능한 빈 공간의 총합이 충분하더라도, 각각의 공간이 작은 크기로 흩어져 있으면 큰 객체를 연속된 공간에 배치하기 어려울 수 있습니다.

이러한 상태를 메모리 단편화(Fragmentation)라고 합니다.

예를 들어 다음과 같이 2MB 크기의 빈 공간이 세 곳에 있다고 가정해 보겠습니다.

2MB 빈 공간 + 2MB 빈 공간 + 2MB 빈 공간

전체 빈 공간은 6MB입니다. 하지만 5MB 크기의 연속된 메모리가 필요한 객체는 배치하기 어려울 수 있습니다.

총 빈 공간: 6MB
연속된 최대 빈 공간: 2MB
새 객체 크기: 5MB

이 문제를 완화하기 위해 살아남은 객체를 한쪽으로 이동시키는 압축(Compaction)을 수행할 수 있습니다.


압축은 왜 필요할까

압축은 살아남은 객체를 한쪽으로 모아 빈 공간을 연속적으로 만드는 처리입니다.

압축 전의 메모리가 다음과 같다고 가정해 보겠습니다.

[객체 A][빈 공간][객체 B][빈 공간][객체 C][빈 공간]

압축 후에는 다음과 같은 구조가 될 수 있습니다.

[객체 A][객체 B][객체 C][연속된 빈 공간]

이를 그림으로 표현하면 다음과 같습니다.

압축을 수행하면 큰 객체를 할당할 수 있는 연속된 공간을 확보하기 쉬워집니다.

다만 객체를 이동시키고 참조 주소를 조정해야 하므로 추가 비용이 발생할 수 있습니다.

구분 Sweep Compacion
목적 가비지 객체의 공간을 회수합니다. 살아남은 객체를 한쪽으로 모읍니다.
처리 결과 빈 공간이 여러 위치에 생길 수 있습니다. 연속된 빈 공간을 확보할 수 있습니다.
주요 효과 사용 가능한 메모리를 늘립니다. 메모리 단편화를 완화합니다.
비용 객체 공간을 회수하는 비용이 발생합니다. 객체 이동과 참조 갱신 비용이 발생할 수 있습니다.

Mark and Sweep과 압축을 결합한 방식을 일반적으로 Mark-Compact 계열의 처리로 구분하기도 합니다.

따라서 Sweep과 Compaction은 완전히 같은 단계가 아닙니다.

Mark and Sweep
활성 객체 표시 → 가비지 객체 공간 회수

Mark and Compact
활성 객체 표시 → 살아남은 객체 이동 → 연속된 공간 확보

 


순환 참조 객체도 수집할 수 있을까

다음과 같이 두 객체가 서로를 참조한다고 가정해 보겠습니다.

class Node {
    Node next;
}

Node nodeA = new Node();
Node nodeB = new Node();

nodeA.next = nodeB;
nodeB.next = nodeA;

nodeA = null;
nodeB = null;

처음에는 다음과 같은 참조 구조입니다.

이후 nodeA와 nodeB에 null을 대입하면 지역 변수에서 객체로 향하는 참조가 사라집니다.

두 객체는 여전히 서로를 참조하고 있습니다.

하지만 GC Root에서 두 객체로 도달할 수 있는 경로가 없다면 두 객체 모두 가비지 객체로 판단할 수 있습니다.

  • 서로 참조하고 있는가 : 예
  • GC Root에서 도달 가능한가 : 아니요
  • 결론 : 가비지 객체

이것이 도달 가능성을 기준으로 객체를 판단하는 방식의 중요한 특징입니다.


Mark and Sweep을 이해할 때 주의할 점

Mark and Sweep은 가비지 컬렉션의 핵심 원리를 이해하는 데 적합한 모델입니다.

하지만 실제 JVM의 가비지 컬렉터가 항상 단순한 두 단계만 수행하는 것은 아닙니다.

실제 가비지 컬렉터는 다음과 같은 요소를 함께 고려할 수 있습니다.

  • 객체가 생성된 영역
  • 객체가 살아남은 시간
  • 애플리케이션 정지 시간
  • 객체 이동 여부
  • 메모리 단편화
  • 병렬 처리와 동시 처리
  • 처리량과 응답 지연의 균형

따라서 Mark and Sweep은 자바의 모든 가비지 컬렉터를 그대로 설명하는 완전한 구현 방식이라기보다, 다음 원리를 이해하기 위한 출발점에 가깝습니다.

도달 가능한 객체를 찾습니다.
        ↓
살아 있는 객체를 구분합니다.
        ↓
도달할 수 없는 객체의 공간을 회수합니다.

핵심 정리
  • 가비지 컬렉션은 더 이상 접근할 수 없는 객체가 차지한 메모리를 회수합니다.
  • 객체가 힙에 존재한다는 이유만으로 활성 객체가 되는 것은 아닙니다.
  • 객체의 생존 여부를 판단하는 핵심 기준은 GC Root에서의 도달 가능성입니다.
  • GC Root에서 도달할 수 있는 객체는 활성 객체로 유지됩니다.
  • GC Root에서 도달할 수 없는 객체는 가비지 객체가 될 수 있습니다.
  • Mark 단계에서는 살아 있는 객체를 표시합니다.
  • Sweep 단계에서는 표시되지 않은 객체가 차지한 메모리를 회수합니다.
  • 객체가 서로 순환 참조하더라도 GC Root에서 도달할 수 없다면 수집 대상이 될 수 있습니다.
  • Sweep 이후 빈 공간이 흩어지면 메모리 단편화가 발생할 수 있습니다.
  • Compaction은 살아남은 객체를 한쪽으로 이동시켜 연속된 빈 공간을 확보하는 처리입니다.

마무리

가비지 컬렉션의 핵심은 단순히 객체를 삭제하는 것이 아닙니다.

JVM은 먼저 현재 프로그램이 접근할 수 있는 객체를 찾아 활성 객체로 판단합니다. 그다음 활성 객체로 표시되지 않은 객체가 차지한 메모리를 회수합니다.

이 과정을 가장 기본적인 형태로 표현한 알고리즘이 Mark and Sweep입니다.

GC Root에서 객체 탐색
        ↓
활성 객체 Mark
        ↓
표시되지 않은 객체 확인
        ↓
메모리 공간 Sweep
        ↓
새로운 객체 할당에 재사용

 

반응형