[JVM] 가비지컬렉터는 어디에서 실행되어야하는가? - 안전지점(Safe Region)

2025. 12. 27. 16:15·CS/JVM

안전지점

https://dev-dx2d2y-log.tistory.com/165

 

[Java] OopMap으로 참조체인 내 속한 객체 찾기

루트 노드 열거GC를 시작하려할 때 가장 기본이되는 것이 GC루트를 기준으로 '도달 가능성 알고리즘'을 실행시키는 것이다.루트 노드 열거는 도달 가능성 알고리즘을 구현하기 위해서 GC 루트집

dev-dx2d2y-log.tistory.com

이전에는 OopMap을 통해서 어떤 과정으로 GC루트에서 연결된 객체를 찾는지에 대해서 알아보았다.

 

그런데.. 가비지컬렉터는 언제나 실행할 수 있을까?

참조관계가 변경되는 모든 메모리마다 가비지컬렉션을 실행한다면 가장 이상적인 방법이겠지만, 컴퓨터의 메모리에는 한계가 있고, 가비지컬렉션을 실행시키는데에도 큰 자원이 소요될 것이다. (스레드가 정지해야하니까)

 

그래서 핫스팟 vm은 모든 명령어에 가비지컬렉터를 실행시키지는 않는다. 대신, 특정한 일부 지점(안전 지점)에서만 스레드를 멈춰세우고 가비지컬렉션을 실행한다. 그래서 가비지컬렉터는 이 안전지점에 도달하기 전까지는 실행되지 않는다.

 

안전지점이 좋아보이긴하지만 너무 적으면 가비지컬렉터가 너무 오래 기다려야하고, 너무 많으면 그만큼 스레드가 정지되는 시간이 길어지게되니 적절한 비중으로 안전지점을 고르는 것이 제일 좋다.


안전지점을 고르기위해서는 '프로그램이 장시간 실행될 가능성이 있는가'이다. 메서드 호출, 반복문, 예외처리 등 프로그램이 장시간 실행될 가능성이 높은 명령어가 안전지점을 만들게된다.

메서드를 호출하면 스택프레임이 안정된 상태고, 반복문은 메모리가 너무 많아져서 GC를 해야하는데 반복문을 끝내지 못하는 경우가 있을 수 있으므로 반복문이 끝나고 다시 조건검사로 돌아갈 때 안전지점이 생성된다.

 

이처럼 장시간 실행될 가능성이 있는 명령어, 그리고 그러한 명령어들은 보통 명령어 흐름이 다중화(multiflexing)될 때이므로 명령어의 흐름을 다중화할 때 안전지점이 생성된다.


안전지점과 관련해서는 한 가지 문제가 더 있는데, 가비지컬렉터가 시작되면 JNI 호출을 실행 중인 스레드 이외의(JNI는 C, C++ 같은 네이티브 메서드를 실행시킨다. 여기서는 자바의 안전지점의 기준이 적용되지 않을 수 있으므로 예외) 모든 스레드들이 가까운 안전지점까지 실행된 후에 멈춰야한다. 

 

1. 선제적 멈춤(preemptive suspension)

가비지컬렉션이 시작되면 시스템이 모든 스레드를 뺏어와서 안전지점까지만 실행한다. 이미 안전지점일 경우 대기. 이런 방식을 이용하는 VM은 거의 없다.

 

2. 자발적 멈춤(voluntary suspension)

자발적 멈춤에서 가비지컬렉터는 간단한 플래그비트를 하나 설정한다. 가비지컬렉션이 필요한 경우 true로 설정되며, 각 스레드가 실행 중에 해당 플래그를 적극적으로 폴링(polling)한다. 

 

폴링이란, 하나의 프로그램(또는 장치)이 다른 프로그램(또는 장치)을 주기적으로 검사하는 것을 말한다. 즉, 가비지컬렉션이 필요하니 스레드가 가까운 곳에서 멈춰야하는지 여부(플래그비트)를 주기적으로, 적극적으로 확인한다. 만약 플래그가 true라면 가장 가까운 안전지점에서 스스로 스레드가 멈춘다. 가비지컬렉터가 직접 스레드들을 멈추는 것과 다르게 자발적멈춤에서는 가비지컬렉터가 스레드 수행에 직접 관여하지 않는다.

 

여기서 폴링 플래그 (플래그비트가 변경되었는지 확인하는 플래그)들은 모두 안전 지점에 위치하고, 메모리가 부족해 새로운 객체를 할당하지 못하는 것을 방지하기 위해서 힙메모리를 소비하는 시점 (객체 생성 등)에서도 폴링 플래그가 위치한다.

 

즉, 스레드들은 명령어를 수행하며 명령어 흐름이 다중화되기 전 '안전지점' 또는 힙메모리를 소비하기 직전에 위치한 폴링플래그를 확인하면 가비지컬렉션 플래그비트를 확인한다. 만약 false일 경우 가비지컬렉션이 아직 실행되기 전이므로 명령어를 계속 실행하고, true일 경우 멈춘다.


폴링은 자주 일어나기 때문에 효율적일 필요가 있어서 핫스팟은 '메모리 보호 트랩'이라는 방법을 사용한다.

 

사용자 스레드를 일시정지해야할 경우 JVM은 폴링플래그마다 모든 스레드들이 반드시 경유해야하지만 읽기권한이 없는 4KB 정도의 "폴링페이지"를 활성화시키는데, 이 폴링페이지를 읽으려다가 접근권한이 없어서 오류가 발생하게된다. 그러면 이 오류에 대한 예외핸들러가 스레드를 일시정지시킨다.

 

이 "폴링페이지"의 주소는 절대주소가 아니라 RIP-상대주소를 사용하기 때문에 유연한 페이지 연결이 가능하다.

RIP란 현재 실행 중인 명령어를 가리키는 포인터로 RIP값에 특정 값(오프셋)을 더한 주소에 있는 명령어를 수행시킨다. 그래서 명령어의 주소만 다르더라도 오프셋값만 맞춰주면 같은 폴링페이지를 읽게할 수 있다. 다음명령어는 CPU의 RIP레지스터가 보관한다.

 

이러한 방식을 사용하는 이유는 폴링은 거의 모든 안전지점에서 일어나야하지만, 거의 모든 경우에 발생하지 않기 때문이다. 모든 스레드는 안전지점에 도달할 때마다 폴링플래그를 확인해야하지만, 폴링플래그가 활성화되는 경우는 잘 없다. 그래서 폴링플래그를 매번 확인하자니 실행시간이 늘어나고, 확인하지 말자니 스레드를 멈출 수 없기 때문에, 폴링플래그를 확인하는게 아니라 폴링플래그가 활성화될 때 읽기권한을 회수하여 에러가 발생하게하고 해당 부분에서 스레드를 정지시키는 것 방법을 채택한 것이다.

 


실제로 위의 과정은 꽤 간단하기 때문에 어셈블리 명령어 하나로 처리할 수 있다.

 

https://shipilev.net/jvm/anatomy-quarks/22-safepoint-polls/?utm_source=chatgpt.com

 

JVM Anatomy Quark #22: Safepoint Polls

Suppose you have the managed runtime like JVM, and you need to stop the Java threads occasionally to run some runtime code. For example, you want to do the stop-the-world GC. You can wait for all threads to eventually call into JVM, for example, ask for al

shipilev.net

자세한 내용은 위를 참고하면 좋을듯하다. 영어다.

 

그래서 위의 과정을 정리해보자면,

 

0. 안전지점이란 명령어가 다중화되기 전, 스레드의 상태들을 명확히 파악할 수 있는 지점으로 이 지점에서 스레드들이 멈추면 안전지점에 멈춘 스레드들을 토대로 가비지컬렉션이 시작된다. OopMap은 "JIT컴파일러가 컴파일할 때 스택이나 레지스터마다 참조위치를 알려주는, GC루트들에서 참조의 위치를 알려주는 역할"을 수행하므로 JIT컴파일러가 컴파일할 때 미리 작성된 OopMap을 가비지컬렉터가 참조한다.

 

1. 가비지컬렉션이 실행되기 직전, 가비지컬렉터는 가비지컬렉션 플래그비트(정식 용어는 아니다. 그냥 내가 쓰는 단어)를 true로 바꾼다.

 

2. 각 스레드는 명령어를 수행하다가 안전지점에서 '폴링플래그'에 도달하게되는데, 만약 가비지컬렉션 플래그비트가 true라면 JVM은 폴링플래그마다 '폴링 페이지'를 만들어 모든 스레드들이 경유하게한다. 이 폴링 페이지는 모든 스레드가 읽기권한이 없어 오류가 발생하고, 오류가 발생하면 스레드는 해당위치에서 멈추도록 예외처리된다.

 

3. 이러한 과정이 모든 스레드에 순차적으로 일어나게되면서 스레드들은 안전지점에 멈추게되고, 모든 스레드들이 안전지점에 멈추면 가비지컬렉터가 실행을 시작한다.


안전지역

좋다. 이제 각 스레드를 멈춰세운 후 가비지컬렉션을 실행하면될 것으로 보이나, 문제가 하나 있다.

만약 실행 중이지 않은 프로그램, 그러니까 잠자기 상태, 또는 블록된 상태의 사용자 스레드는 어떻게해야할까? 이 스레드들을 가상머신이 뺏어와 안전지점까지 실행시킬 수 없을 것이고, 폴링플래그를 사용하는 것도 안될 것이다. 이래서 등장한 것이 '안전 지역'이다.

 

안전 지역은 참조 관계가 변하지 않음 보장하는 코드 내의 일정 영역을 의미한다. 대표적으로는 Thread.sleep()이나 Object.await() 처럼 스레드가 당분간 실행되지 않는지역이나 오래 지속되지만 객체참조가 바뀌지 않는 반복문에 한해서 제한적으로 JIT가 안전지역이 지정하는데, 이 지역에 스레드가 진입한 경우 JVM이 스레드가 안전지역에 들어왔음을 표시한다.

 

가비지컬렉터는 안전지역에 들어온 스레드들은 가비지컬렉션할 때 이 스레드가 안전지점에 있는지 굳이 신경쓸 필요가 없다. 다만 안전지역에서 벗어나려는 스레드는 VM이 루트 노드 열거를 완료했는지, 가비지컬렉션의 단계 중 스레드를 일시정지시켜야하는 단계를 이미 실행했는지 확인한다. 아직 완료되지 않은 경우에는 안전지역을 벗어나도 좋다는 신호가 떨어질 때까지 대기해야한다.


이처럼 모든 스레드들은 가비지컬렉션 과정에서 스레드를 멈춰세우기 전, 모든 스레드들이 안전한 지점에 도달하고나서야 스레드를 멈출 수 있다.

'CS > JVM' 카테고리의 다른 글

[JVM] 객체를 언제, 어떻게 표시할 것인가?  (0) 2026.01.05
[JVM] JVM 내에서 카드테이블과 사전장벽, 사후장벽이란?  (1) 2026.01.01
[JVM] OopMap으로 참조체인 내 속한 객체 찾기  (0) 2025.12.27
[JVM] JVM 끝까지 파헤치기 독서 #5 - 가비지 컬렉터의 알고리즘 이론  (0) 2025.12.26
[JVM] GC의 객체회수과정은 어떻게 일어나는가? - 마크-스윕, 마크-카피, 마크-컴팩트  (0) 2025.12.26
'CS/JVM' 카테고리의 다른 글
  • [JVM] 객체를 언제, 어떻게 표시할 것인가?
  • [JVM] JVM 내에서 카드테이블과 사전장벽, 사후장벽이란?
  • [JVM] OopMap으로 참조체인 내 속한 객체 찾기
  • [JVM] JVM 끝까지 파헤치기 독서 #5 - 가비지 컬렉터의 알고리즘 이론
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211) N
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59) N
        • JVM (12)
        • 백엔드 (20) N
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[JVM] 가비지컬렉터는 어디에서 실행되어야하는가? - 안전지점(Safe Region)
상단으로

티스토리툴바