[Unity] Managed, Unmanaged
댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!
앞서 2편에서 CLR이 Managed 힙의 객체를 어떻게 관리하는지 보았다. 특히 CLR이 OS에 세그먼트 단위의 요청을 하고, GC로 객체를 자동으로 해제하며 압축하는 과정에 대해 이야기했다.
지금까지는 최대한 ‘힙’과 ‘Managed 힙’을 구분지어 서술하려고 노력했는데, 그것은 이 둘에 차이가 있기 때문이다. 이 글은 CLR의 통제를 받지 않는 힙, Unmanaged 힙에 대해서 다룬다.
CLR이 관리해주는 것은 크게 둘이다.
이 자동 메모리 관리가 이뤄지는 곳이 바로 Managed Memory, 그리고 이 곳의 객체들이 Managed Object다.
Managed Memory에 들어가는 것은 C#의 모든 참조타입 인스턴스로, 다음과 같은 것들이 있다.
GC는 이들을 관리하기 위해 이들의 위치와 크기, 참조 관계(reachability)를 알고 있다.
CLR의 관리 밖에있는 힙 영역이다. 이곳의 메모리는 명시적으로 해제되어야한다. GC가 관리해주지 않기 때문이다.
OS가 관리하는 자원은 이곳에 할당된다. C#에서 FileStream을 사용해본 적이 있다면 using절이나 Dispose를 사용한 기억이 있을텐데, 이것이 OS에게 해당 객체 해제를 명시적으로 요청한다.
C/Cpp로 작성된 라이브러리는 동적할당으로 malloc이나 new를 사용하는데, 이 또한 Native Heap에 들어간다. 이들도 delete나 free 등을 통해 명시적 해제가 필요하다. 유니티는 내부적으로 Cpp를 사용하므로 알아둘 필요가 있겠다.
또 C#에서 명시적으로 할당하는 Unmanaged 객체가 있다. 우리가 잘 아는 것으로는 이전의 포스트에서도 다루었던 NativeArray가 있다.
// Unmanaged 메모리에 직접 배열을 할당
NativeArray<float> positions = new NativeArray<float>(1000, Allocator.Persistent);
// 사용 이후,
// 반드시 직접 해제해야 한다
positions.Dispose();
System.IDisposable을 구현한다면 using키워드를 통해 해제를 간편히 할 수 있다.
아래는 CancellationTokenSource를 사용하는 예제이다. 커스텀 클래스가 동적 할당을 해제해야할 때 이를 구현시킬 수 있을 것이다.
// 이 코드를 작성하면
using (var cts = new CancellationTokenSource())
{
// ... 사용 ...
}
// 컴파일러가 이렇게 변환한다
var cts = new CancellationTokenSource();
try
{
// ... 사용 ...
}
finally
{
if (cts != null)
cts.Dispose();
}
GameObject 하나를 생성하자. 그리고 클래스의 필드 obj에 그것을 할당하자. obj = new GameObject(). 나는 단순히 객체 하나를 생성하여 obj변수에 담았다고 생각해왔다. 이제는 이 객체가 Native 메모리와 Managed 메모리에 모두 담긴다고 생각할 수 있다. 구체적으로는, 실제 객체 자체는 Native에 저장되고 그것을 가리키는 C# 래퍼 obj 변수가 Managed에 저장될 것이다.
Native에 있는 객체에 접근하기 위해 우리는 Managed 래퍼 객체인 obj에 접근한다. GC는 이 래퍼를 어디에서도 참조하지 않을 때 메모리를 해제할 것이다. 예를 들어, obj가 포함된 인스턴스가 파괴되고, 다른 어디에서도 이 것을 참조하지 않으면, obj가 있던 그 곳은 GC에 의해 수거되어 빈 공간이 된다.
이렇게 우리는 obj가 참조하던 GameObject의 Native 객체를 가리키던 핸들 - obj를 잃는다. 다만, obj가 가리키던 객체는 여전히 파괴되지 않고 Native에 남아있다. 헉! 어떡하지?
다행히 우리는 Find계열 메소드로 이 객체에 대한 핸들을 새롭게 발급받을 수 있다! 하지만 그것은 새로운 GC 할당을 의미하기도 한다. 이는 Find계열 메소드가 비용이 크다고 말할 수 있는 Managed 메모리 측면에서의 근거다.
Managed에 Used와 Reserved가 있다는 것을 지난 장에서 이야기했다. Unmanaged 메모리는 프로파일러에 Native로 표시된다.
따라서 최적화를 위해 메모리 프로파일링을 한다면, 이들을 구분하고 적절한 조치를 하는 것이 요구될 것이다.
GC가 관리한다는 의미에서 Managed. 이 힙 메모리 공간에서의 메모리 해제는 GC가 하므로 우리가 명시적으로 하지 않는다.
여기에 C# 클래스 인스턴스가 들어가며, Native객체의 C# 래퍼의 역할을 한다.
CLR/GC가 관리하지 않는 힙 메모리 공간. GC가 관리하지 않아 Unmanaged라 부르고, 주체적 의미로서 Native라고도 부른다.
Unity의 내부나 외부 라이브러리에서 C/Cpp로 동적 할당한 객체가 들어가고, 사용자가 NativeArray 등으로 만든 객체도 들어간다. 명시적인 해제가 필요하다. 해제가 없으면 메모리 누수(leak)라 부른다.
이건 질문이 곧 답이다.