본문 바로가기
JavaScript│Node js

Nodejs 가비지 컬렉터

by 자유코딩 2020. 6. 14.

managed 언어에는 흔히 가비지 컬렉터가 있다.

node js 의 가비지 컬렉터에 대해서 알아본다. 

 

가비지 컬렉터는 사용되지 않는 메모리를 정리한다.

 

GC 대상이 아닌것

객체가 포인터 체이닝을 통해서 접근 한 것

 

GC 대상인 것

그 외의 모든 것들은 garbage 이다.

 

이전의 알려진 가비지 컬렉터 - 사용되지 않는 객체를 찾고 지운다.

 

GC 동작 시간동안 프로그램이 멈추는 stop the world 현상이 나타날 수 있다.

 

node js 의 가비지 컬렉터는 기본적으로 mark - sweep 형태로 동작한다.

mark - 동작 방식

- 루트에서 포인터를 사용해서 참조한다.

루트 객체 - 글로벌 객체나 현재 활성화되어 있는 함수 등의 알려진 살아있는 객체의 셋

 

객체당 2개의 마크비트(00, 10, 11), 마킹 작업 목록을 사용해서 마킹을 구현한다.

00은 흰색으로 가비지 컬렉터가 아직 발견 못했다.

10은 회색으로 가비지 컬렉터가 marking worklist에 넣은 경우 = marking worklist

11은 검은색으로 가비지 컬렉터가 marking worklist에서 꺼내서 필드를 모두 방문한 경우

루트로부터 객체를 참조한다.

참조가 없는 객체는 GC 대상이다.

 

마킹은 더이상 회색 객체가 없을때 종료된다.

모든 남아있는 흰색 객체들은 도달 할 수 없으며 collect 된다.

마킹이 끝나면 힙에서 마킹되지 않은 모든 객체는 메모리에 반환된다.

 

흰색 - 회색 - 검정색 순서로 색이 바뀐다.

흰색(아직 안갔음) - 회색(marking worklist) 에 들어감 - 검은색(marking worklist 에서 꺼내서 필드 확인 완료)

이렇게 처리를 모두 마치고나서 아직 방문 안됐다는 것은

연결이 안되어있는 객체라는 뜻.(흰색) -> 제거

 

이 mark-sweep GC는 아래와 같은 순서로 발전해왔다. 언제, 어떻게 mark-sweep 할지 방법이 발전했다.

 

 

위에 있는 형태는 가장 초기의 형태이다.

저렇게 되면 gc 동작 중 프로그램이 동작하지 않는 stop the world 현상이 발생한다.

그 다음 아래 incremental 마킹으로 바뀌었다.

마킹 작업을 작은 덩어리로 쪼갠다.

그 사이사이에 애플리케이션이 실행 될 수 있도록 한다.

 

점진적 마킹 방식은 처음의 방식보다는 좋지만 단점이 있었다.

 

점진적 마킹 방식의 단점은 아래와 같다.

1. 애플리케이션의 처리량을 감소 시킬 수 있다.

2. 가비지 컬렉터에게 객채 그래프가 바뀌는 모든 연산에 대해서 알려야 한다.

 

점진적 마킹은 코드로는 아래와 같이 동작합니다.

// `object.field = value` 호출 후
write_barrier(object, field_offset, value) {
  if (color(object) == black && color(value) == white) {
    set_color(value, grey);
    marking_worklist.push(value);
  }
}

이런 코드를 통해서 가비지 컬렉터에게 객체 그래프가 바뀌는 모든 연산에 대해서 알린다.

코드를 보면 color(객체) == black 이면서 color(값) == white 일 결우 value 를 grey로 바꾼다.

그리고 marking worklist에 추가한다.

 

다시 한번 정리하면 white 는 아직 가지 않은 객체

grey는 marking worklist 에 담았을때.

black 은 marking worklist에 담긴 객체의 모든 필드를 방문했을때.

 

 

 

점진적 마킹 방식에서 워커 스레드를 추가하면 이 문제가 해결 될 수 있다고 해서 아래 방식이 나왔다.

워커 스레드를 만들면 처리량, 중단시간을 모두 개선 할 수 있다.

그 다음은 parallel 마킹와 concurrent 마킹이다.

 

parallel 마킹은 메인 스레드와 워커 스레드에서 일어난다.

이전의 stop the world 방식을 멀티스레드 형태로 바꿨다.

gc 하는 동안 애플리케이션은 동작 할 수 없다.

parallel 마킹 방식

concurrent 방식은 워커 스레드에서 동작한다.

gc 하는 동안 애플리케이션이 동작 할 수 있다.

parallel 마킹 방식은 마킹 동안 애플리케이션이 함께 실행되지 않을 수 있다고 가정할 수 있다.

객체 그래프는 정적이고 바뀌지 않기 때문에 구현이 간단해진다.

병렬로 객체 그래프를 마킹하기 위해서 가비지 컬렉터 자료구조를 thread-safe 하게 만들었다.

그리고 스레드들 사이에 마킹을 효율적으로 공유할 방법을 찾아야 했다.

 

여기서 각 스레드는 객체 그래프로부터 읽기만 하고 객체 그래프를 변경하지 않는다.

 

이런 구조로 thread safe 하게 동작한다.

다 같이 marking worklist 에서 가져가서 작업한다.

 

그럼 이제 concurrent marking (동시 마킹)을 살펴보자.

워커 스레드가 힙에서 객체를 찾는 동안 메인 스레드에서 자바스크립트 코드가 동작 할 수 있게 한다.

 

이런 동작은 레이스 컨디션이 일어날 수 있다.

*레이스 컨디션 = 다수의 스레드가 1개의 자원에 접근해서 각각의 연산을 할때 연산 결과가 잘못되는 것

 

레이스 컨디션은 흔히 아래 항목에서 일어날 수 있다.

1. 객체 할당

2. 객체 필드 쓰기

3. 객체 레이아웃 변경

4. 역직렬화

5. 함수

6.gc

7. 코드 patching

 

이 문제를 bailout worklist 라는 것을 도입해서 해결했다.

객체에 락을 거는 방식은 하지 않았다.

락을 걸게되면 메인스레드가 워커 스레드가 디스케줄 될때까지 기다려야 한다.

 

bailout worklist 는 메인스레드만 작업한다.

워커 스레드는 bailout worklist에 객체를 넣는다.

 

종합적인 concurrent marking 동작 방식

1. 메인스레드는 루트를 보고 marking worklist 를 채운다.

2. 메인스레드가 동시 마킹 작업을 워커 스레드에게 맡긴다.

3. 워커 스레드는 메인 스레드를 도와서 marking worklist 를 처리하게 한다.

4. 메인 스레드는 때때로 bailout worklist 와 marking list 를 처리하며 마킹에 참여한다.

5. marking worklist가 비게되는 때에 메인 스레드는 가비지 컬렉션을 끝낸다.

 

완료 시점에 메인스레드는 root 를 다시 탐색해서 화이트 객체를 더 발견할 수도 있다.

그림으로 보는 1~5

 

 

 

참고 글

https://v8.dev/blog/concurrent-marking?m=1

 

Concurrent marking in V8 · V8

 

v8.dev

 

댓글