원문 : http://www.sysnet.pe.kr/2/0/10920
C# 메모리 관리와 관련된 자세한 내용을 알아보는 중 다음과 같은 글이 있었다.
GC(가비지컬렉터)의 동작에 관한 내용.
다음과 같은 질문이 있군요.
C# WeakReference이 CPU 플랫폼 설정 마다 결과가 틀리게 나옵니다. ; http://www.sysnet.pe.kr/3/0/4702
문제를 정리해 보면, 다음과 같이 코딩을 한 경우 (제 환경에서) Debug 빌드로 하면 Target이 살아 있는 걸로 나옵니다.
// .NET 4.5.2 + Debug 빌드 테스트 (x86/x64) public class Man { public string Name { get; private set; } public Man(string name) { Name = name; } } class Program { static void Main(string[] args) { TestWeakReference(); Console.ReadKey(); } private static void TestWeakReference() { Man m = new Man("A"); WeakReference refMan = new WeakReference(m); m = null; GC.Collect(); Console.WriteLine("약한 참조 원본: {0}", (m == null ? "null" : m.Name)); Console.WriteLine("약한 참조 참조: {0}", (refMan.Target == null ? "null" : (refMan.Target as Man).Name)); } } // 출력 결과 약한 참조 원본: null 약한 참조 참조: A
코드 상으로 보면 분명 "m = null"로 되었고 GC.Collect가 수행되었으니 당연히 WeakReference의 Target이 null이 나와야 합니다. 그런데, 왜 여전히 살아있을까요?
사실, 제가 답변에도 썼지만 마이크로소프트 입장에서는 WeakReference의 Target이 정확히 언제 null로 된다거나, 심지어 힙 객체가 '정해진 시점'에 해제된다고 명시하고 있진 않습니다. 즉, 그 시점을 정확히 예측해서 어떤 부가적인 코드를 작성하는 것은 바람직하지 않습니다.
그래도, 저런 결과가 나오면 좀 이상하긴 할텐데요. 어디... 그 원인을 한번 밝혀 볼까요? ^^
일단, 우리가 배운 바로는 GC가 되려면 또 다른 참조 객체, 스택 및 레지스터(CPU Register)에 그 참조가 없어야 한다는 것입니다. 그렇다면, 저 경우에 분명히 그 세가지 중의 하나는 어디에선가 참조값이 있어야 합니다.
그 중에서 우선 "또 다른 참조 객체"는 후보가 아닙니다. 코드 상에 WeakReference가 'm'의 참조값을 가지고는 있지만 GC는 Weak 참조는 GC 대상에서 고려하지 않기 때문에 무시해도 됩니다. 나머지 후보라면 스택 및 레지스터가 있는데요. 이를 살펴보기 위해 다음과 같이 소스 코드에 Console.ReadKey()를 추가하고,
private static void TestWeakReference()
{
Man m = new Man("A");
WeakReference refMan = new WeakReference(m);
Console.ReadKey();
m = null;
GC.Collect();
Console.WriteLine("약한 참조 원본: {0}", (m == null ? "null" : m.Name));
Console.WriteLine("약한 참조 참조: {0}", (refMan.Target == null ? "null" : (refMan.Target as Man).Name));
Console.ReadKey();
}
실행한 후 Visual Studio를 이용해 "Attach to Process..."로 연결한 다음 저 상태에서의 기계어 코드를 확인해 봅니다. 그럼, 대략 다음과 같은 화면을 볼 수 있습니다.
여기서, 생성된 m에 대한 인스턴스를 보관하고 있는 곳은 2군데입니다.
하나는 가장 상단의 "call 00007FFDDCB24C10"로 표현된 Man 객체의 생성자 호출에 이은 "mov qword ptr [rbp-40h], rax"로 "[rbp-40h]" 스택에 보관되어 있고,
00007FFD7D4D0510 E8 FB 46 65 5F call 00007FFDDCB24C10
00007FFD7D4D0515 48 89 45 C0 mov qword ptr [rbp-40h],rax
또 하나는 [rbp-40h]의 값을 다시 [rbp-30h]로 이동해서 보관해 놓은 것입니다.
00007FFD7D4D052F 48 8B 4D C0 mov rcx,qword ptr [rbp-40h]
00007FFD7D4D0533 48 89 4D D0 mov qword ptr [rbp-30h],rcx
반면 하단에서 "m = null" 할 때는 [rbp-30h] 값만 0으로 대입하고 있습니다.
m = null;
00007FFD7D4D0569 33 C0 xor eax,eax
00007FFD7D4D056B 48 89 45 D0 mov qword ptr [rbp-30h],rax
실제로 [rbp-40h]의 값과 [rbp-30h]의 값이 같다는 것을 메모리 창을 통해서 확인할 수 있습니다. 레지스터 창을 통해 RBP == 0000006B335FEE10로 나오니까, 각각 다음과 같이 계산되고,
[rbp-40h] == 0x0000006B335FEDE0 [rbp-30h] == 0x0000006B335FEDD0
이에 대한 메모리 값을 확인해 보면 "e8 45 a4 40 5a 02 00 00"로 동일하게 나옵니다.
따라서, GC는 [rbp-30h]의 참조가 없어졌다 해도 [rbp-40h]의 값으로 인해 해당 객체를 제거하지 못했던 것입니다.
어디... 그럼 실험으로 증명해 볼까요? ^^
해당 스택의 값을 비주얼 스튜디오의 메모리 창을 통해 임의로 '00 00 00 00 00 00 00 00'으로 초기화할 수 있습니다.
이렇게 해준 다음 실행을 계속해 보면 다음과 같이 성공적으로 참조 해제가 된 것을 확인할 수 있습니다.
약한 참조 원본: null 약한 참조 참조: null
그러니까, 결국 JIT 컴파일러가 생성해 낸 기계어 코드에서 Debug 빌드인 경우 스택에 두번 보관해 놓은 값 때문에 저런 문제(?)가 발생한 것입니다.
이 때문에 Release 빌드로 하면 이런 문제가 발생하지 않습니다. 릴리즈 빌드 시 JIT 컴파일러는 최적화된 코드를 생성하기 때문에 쓸데없는 스택 낭비를 유발하는 코드를 생성하지 않기 때문입니다.
이 외에도 코드를 다음과 같이 바꿔주면 Debug 빌드 시에도 약한 참조가 끊기게 됩니다.
// .NET 4.5.2 + Debug/Release 빌드 테스트 (x86/x64) class Program { static void Main(string[] args) { WeakReference refMan = TestWeakReference(); GC.Collect(); Console.WriteLine("약한 참조 참조: {0}", (refMan.Target == null ? "null" : (refMan.Target as Man).Name)); Console.ReadKey(); } private static WeakReference TestWeakReference() { Man m = new Man("A"); WeakReference refMan = new WeakReference(m); m = null; Console.WriteLine("약한 참조 원본: {0}", (m == null ? "null" : m.Name)); return refMan; } } // 출력 결과 약한 참조 원본: null 약한 참조 참조: null
이유를 아시겠죠? ^^ 이런 경우 디버그 빌드로 해도 스택에 2중으로 보관되어 있던 값이 메서드가 리턴하면서 스택이 해제되기 때문에 CLR의 GC 구성 요소가 검사해야 할 영역에서 제외되기 때문입니다.
(첨부한 코드는 이 글의 예제를 포함합니다.)
'C#' 카테고리의 다른 글
[C#] Float형을 byte형으로 변환하기 (0) | 2016.12.14 |
---|---|
[C#] Using 키워드의 용도 (0) | 2016.12.14 |
[C#] DataTable.Copy() 와 DataTable.Clone()의 차이 (0) | 2016.09.19 |
[C#][MSDN] C# Event 발행자(publisher)와 구독자(subscriber) 방식, 옵저버 패턴에 대해 (0) | 2016.08.21 |
[C#] WeakReference (약한 참조) (0) | 2016.08.21 |
[C#][Unity3D] List<T> Remove 사용 주의사항 (1) | 2013.03.07 |
[C#] Json 파서 구현 시 엑셀 파일 로딩에 대해 (0) | 2013.02.07 |
C# char, sbyte (0) | 2013.01.17 |
C# Delegate 그리고 Event (1) | 2013.01.08 |
[스크랩] 무제한으로 매개 변수 사용(C# 및 Java) - params(가변인자) (0) | 2012.12.20 |