JavaScript Object의 생명주기 관리

Node.js Core 에서 JavaScript 생명주기를 관리하는 방법을 알기 위해서는 V8 API를 통해 메모리를 관리하는 컨셉을 이해할 필요가 있다. 모든 JavaScript 값은 V8 API를 통해 소위 핸들이라는 것을 통해 액세스된다. 이 핸들은 메모리 관점에서 Local과 Global이란 타입으로 나뉜다.

메모리 관리 타입

v8::Local

v8::Local 이란 JavaScirpt 값이 다뤄지는 현재의 Local Block 영역 (Stack) 에서만 유효한 핸들을 의미한다. Local Block을 벗어나면 값은 유효하지 않게 된다. 값이 Referencing 되고 있는지에 따라 GC (Gabage Collector)를 수행하는 JavaScript 엔진 측면에서 메모리 관리는 중요한데, 기본적으로 C++ 에서 모든 JavaScript 값에 대해서 Local 핸들로서 관리함으로서 메모리의 leak을 방지한다. 즉 임시적으로 사용할 메모리는 모두 Local 핸들로서 직관적으로 다뤄진다.

v8::Local 은 JavaScript 핸들이 어떤 메모리 영역에서 관리되야하는지를 나타내는 Reference일 뿐 Local 메모리 생성을 의미하지는 않는다. Local 핸들을 생성하기 위해선 Stack 영역에 v8::HandleScope 또는 v8::EscapableHandleScope 객체를 생성해야한다. 생성된 이후 Local::New API를 사용하면 실제 Local 메모리에 적재된 JavaScript 값을 생성할 수 있고 마지막으로 생성한 Scope 객체에 메모리 영역이 잡히게 된다. 그러나 Node.js 에서 바인딩 함수 내부에 있는 경우는 Handle의 범위가 함수 외부에 이미 존재하므로 별도로 만들 필요가 없다.

v8::EscapableHandleScope는 Scope 내에서 생성된 v8::Local을 Scope 외부로 넘기고자할때 사용할 수 있다. v8::EscapableHandleScope 객체의 Escape API로 생성한 v8::Local 인스턴스를 넘기면 해당 영역 외부의 메모리 Scope로 전달된다.

v8::Global

특정 Scope내에서만 유요한 v8::Local과 다르게 다르 v8::Global 로 관리되는 핸들은 Scope와 관계없이 유지되며 GC 대상이 되지 않을 수 있다. 정확히 말하면 Global 핸들의 Strong 상태에서 GC 되지 않는다. Global 핸들은 Weak 상태로 변경할 수 있으며, Weak 상태가 되었을 때 GC 메카니즘에 의해 메모리가 릴리즈 될 수 있다. 그런데, 그냥 릴리즈 되는 것이 아니라 GC 메카니즘에 의해 메모리 릴리즈 되기 직전에 등록된 Callback을 통해 통보받는다. 그 시점에서 다시 Strong으로 변경할지 Weak를 유지하여 릴리즈를 할지 결정이 가능하다.

1
2
3
4
5
6
7
void function(v8::Isolate* isolate, v8::Local<v8::Object> obj) {
...
// Global의 Reset() 함수를 이용하여 Local 영역에 있는 obj 핸들을 Global로 이동시킨다.
// 이동이 되면 기본적으로 Strong 상태로 핸들을 관리한다.
v8::Global<v8::Object> reference = reference.Reset(isolate, obj);
...
}

생명주기 관리

JavaScript 엔진은 Gabage Collector를 주기적으로 실행하여 어떤 객체도 Referencing하지 않는 객체에 대해 제거한다. 사용자가 JavaScript 객체를 Referencing 하지 않더라도 Core는 필요에 따라 특정 JavaScript 객체가 엔진에 의해 제거되는 것을 방지해야한다. 그렇게 하기 위해서 JavaScript Object의 InternalField 에 C++ 클래스 인스턴스를 저장하고, JavaScript Object에 대한 메모리 영역을 Local에서 Global로 변경한다. 이 동작은 Node.js에서 객체와 바인딩되는 모든 Class의 부모 Class인 BaseObject 의 생성자에서 수행된다. v8::Global<v8::Object> 타입 변수 persistent_handle_에 Local 객체를 등록함으로서 상기 Code에서 Reset을 호출한것과 같이 Global 메모리 영역으로 JavaScript 값을 이동시킨다.

Weak 상태로 변경

v8::GlobalSetWeak API는 현재 Strong으로 관리되고 있는 메모리를 Weak 상태로 변경함으로서 GC 시 메모리를 릴리즈 할 수 있게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void BaseObject::MakeWeak() {

....

persistent_handle_.SetWeak(
this, // (a) Callback으로 전달할 포인터
[](const WeakCallbackInfo<BaseObject>& data) {

// (a) 에서 전달한 BaseObject 포인터를 parameter로서 획득한다.
BaseObject* obj = data.GetParameter();

/*
~BaseObject에서 internal field를 clean up 하는 부분이 있는데
GC에 의해 삭제된 경우는 굳이 해당 동작을 하지 않도록 한다.
*/
obj->persistent_handle_.Reset();

// BaseObject 인스턴스가 연결되어있던 JavaScript 객체는 이제 삭제되니, 이
// BaseObject도 삭제되어야한다. OnGCCollect 함수에서는 delete this를 수행한다.
obj->OnGCCollect();
},
// (b) Callback에 전달 가능한 타입은 kParameter 와 kInternalFields 이며
// kInternalFields 타입은, 지정된 internal field의 첫 두 field를 전달한다.
WeakCallbackType::kParameter);
}

상기 코드는 BaseObject의 MakeWeak함수의 일부이다. SetWeak를 통해 GC 발생시점에 콜백을 등록하고 Strong 에서 Weak 핸들로 변경한다. 자바스크립트 객체가 가비지 컬렉션되기 전에 등록된 콜백이 호출된다. BaseObject::OnGCCollect()를 호출하면 BaseObject 인스턴스가 삭제된다. SetWeak 등록된 후 BaseObject::ClearWeak()를 호출하여 Strong상태로 다시 변경할 수 있다.

일반적으로 해당 서브클래스가 이벤트 루프 등에서 참조되지 않는 한, HandleWrap 및 ReqWrap 클래스의 경우처럼 해당 서브클래스의 생성자에서 MakeWeak()를 호출하는 것이 합리적입니다.

Smart Pointer for BaseObject

BaseObjectPtr

BaseObject의 서브클래스 T의 객체를 Shared Pointer로서 보유한다. 즉, 이 포인터를 가지고 있는동안은 BaseObject는 Strong 참조를 유지한다.

BaseObject는 JavaScript 객체의 생명주기를 따르나 Detach()가 호출된다면 BaseObject를 JavaScript 객체의 생명주기로 부터 분리해낼수 있다. 이 경우 BaseObjectPtr 를 참조하는 마지막 포인터가 제거되면 BaseObject는 릴리즈된다.

BaseObjectWeakPtr

BaseObject의 서브클래스 T의 객체를 Weak Pointer로서 보유한다. Garbage Collection이 발생해 BaseObject가 제거되면 std::weak_ptr<T>와 유사하게 weak_ptr.get()의 값으로 nullptr가 반환된다.

v8: State transition diagram

1
FREE -> NORMAL <-> WEAK -> PENDING -> NEAR_DEATH -> { NORMAL, WEAK, FREE }
Share