레이블이 c#인 게시물을 표시합니다. 모든 게시물 표시
레이블이 c#인 게시물을 표시합니다. 모든 게시물 표시

2019년 11월 16일 토요일

c# - 가비지컬렉터(GarbageCollector)사용시 주의해야 할 점

C# __6.0 가비지컬렉터(GarbageCollector)사용시 주의해야 할 점


이전 내용에서 가비지 컬렉션에 대해 알아보았으니, 이번에는 비지 컬렉터의 성능을 저하시키지 않기 위해 객체를 생성함에 있어서 조심해야할 부분 살펴보겠습니다.

첫번째로 객체를 너무 많이 할당하지 말아야 합니다. 
가장 기본적인 지침인데요, CLR 객체 할당 속도가 빠르긴 하지만 너무 많은 수의 객체는 관리되는 힙의  세대에 메모리 포화를 초래할  있고이는 빈번한 가비지 컬렉션을 부르는 결과를 낳습니다객체 할당 코드를 작성할   필요한 객체인지와 필요 이상으로 많은 객체를 생성하는 코드가 아닌지의 여부를 고려해야합니다.


두번째는 너무  객체 할당을 피해야 합니다. 
CLR 보통 크기의 객체를 할당하는 힙과는 별도로 85KB 이상의  객체를 할당하기 위한 '대형 객체 (LOH:Large Object Heap)' 따로 유지합니다. 우리가 평소에 사용하는 힙은 대형 객체 힙에 대비되는 개념으로 소형 객체 이라고 부르기도 합니다.

커다란 객체를 소형 객체 힙에 할당한다면 0세대가 빠르게 차오르게 되므로 가비지 컬렉션을  자주 촉발하게 되고이는 어플리케이션의 성능 저하를 불러오게 됩니다그래서 대형 객체 힙을 별도로 유지함으로  성능 향상의 효과를 내고 있습니다만단점도 있겠지요.

우선 대형 객체 힙은 동작 방식이 소형 객체 힙과 다릅니다대형 객체 힙은 객체의 크기를 계산한  그만한 여유 공간이 있는지 힙을 탐색하여 할당합니다. GC 수행한  소형 객체 힙은 해제된 메모리 공간에 인접 객체들을 끌어당겨 차곡차곡 정리하지만대형 객체 힙은 해제된 공간을 그대로 둔답니다매우  메모리를 복사하는 비용이 너무 비싸기 때문이지요.
 공간은 나중에 다른 객체들에게 할당 되겠지만메모리를 0바이트의 낭비도 없이 사용하는 소형객체 힙과는 달리  공간을 군데군데 낭비하게 됩니다.

그리고 또하나 CLR 대형 객체 힙을 2세대 힙으로 간주하기 때문에 대형 객체 힙에 있는 쓰레기 객체가 수거되려면 2세대에 대한 가비지 컬렉션이 수행되어야 합니다2세대 가비지 컬렉션은  세대에 대한 가비지 컬렉션을 유발하기 때문에 수거되는 메모리의 양이  수록 어플리케이션이 정지되는 경우가 발생하게 됩니다그래서 너무  객체를 할당하는 코드는 조심스럽게 고려해보아야 한답니다.


세번째는 너무 복잡한 참조 관계는 자제해야 합니다
이러한 지침은 성능 뿐아니라 가독성을 위해서라도 주의하셔야합니다참조 관계가 많은 객체는 가비지 컬렉션 후에 살아남았을 때가 문제입니다가비지 컬렉션에서 살아남았을 경우 다음 세대로 옮기기 위해 메모리 복사를 수행하는데  참조관계가 복잡한 객체의 경우에는 단순히 메모리를 복사하는데서 끝나지 않습니다객체를 구성하고 있는  필드 객체 간의 참조 관계를 일일이 조사해서 참조하고 있는 메모리 주소를 전부 수정한답니다 클래스 구조가 간단했다면 메모리 복사만으로도 끝났을 일을 탐색과 수정까지 거치게 되는것이죠.


네번째는 루트를 너무 많이 만들지 말아야 합니다. 
가비지 컬렉터는 루트 목록을 순회하면서 쓰레기를 찾아냅니다루트 목록이 작아진다면 그만큼 가비지 컬렉터가 검사를 수행하는 횟수가 줄어들므로  빨리 가비지 컬렉션을 끝낼  있습니다따라서 루트를 가급적 많이 많들지 않는 것이 성능에 유리하겠지요 내용은 필요 이상으로 객체를 만들지 말라는 기본적인 지침입니다.

c# - 가비지 컬렉터 ( Garbage Collector ) _ 가비지 컬렉션 ( Garbage Collection )

C# __6.0 가비지컬렉터(GarbageCollector)_가비지컬렉션(GarbageCollection)

오늘은 가비지 컬렉션(Garbage Collection)에 대해 알아보겠습니다. 
가비지 컬렉션은 .NET 기반 언어에서 사용자의 직접 처리없이 메모리를 자동으로 관리해주는 메커니즘입니다.

[ 1. C/C++에서의 메모리 관리 (Native Code)  ]
C/C++언어로 프로그래밍을 할 때 객체를 할당하기 위해 일일이 메모리 공간을 확보해야 하며, 객체를 할당한 후에는 힙을 가리키는 포인터의 사용이 끝나면 메모리를 해제해주어야 합니다. 프로그래밍을 짜다보면 메모리해제에 대해 깜빡하게되버리면 곧 누수가 되고, 사용자들에게 문제를 야기할 수 도 있답니다.
또한 해제한 메모리를 사용하려고한 경우 큰 문제가 발생하는 경우도 있습니다. 이렇듯 메모리를 직접 다루고 관리하는 일은 까다롭고도 위험하답니다.

또한 C/C++언어는 힙에 객체를 할당하기 위해 비싼 비용을 치루어야 합니다. C/C++기반의 프로그램을 실행하는 C-런타임은 객체를 담기 위한 메모리를 여러 개의 블록으로 나눈 뒤, 이 블록을 링크드 리스트로 묶어 관리하게됩니다. 객체를 메모리에 할당하기 위한 코드가 실행이 되면, C-런타임은 메모리 링크드 리스트를 순차적으로 탐색하면서 해당 객체를 담을 수 있을 만한 여유가 있는 메모리 블록을 찾습니다. 적절한 크기의 메모리 블록을 만나면, 프로그램은 이 메모리 블록을 쪼개서  객체를 할당하고 메모리 블록의 링크드 리스트를 재조정하게 됩니다.
단순히 메모리 공간을 할당하는 것이 아니라 공간을 탐색하고 분할하고 재조정하는 오버헤드가 필요하다는 것입니다.

[ 2. C#에서의 메모리 관리 (Managed Code) ]
C#에서는 C/C++에서 골치아프게 했던 이런 문제들로부터 자유로워졌습니다. CLR이 자동 메모리 관리 기능을 제공하기 때문이지요. 자동 메모리 관리 기능의 중심에는 가비지 컬렉션이 있습니다. 가비지 컬렉션은 프로그래머로 하여금 컴퓨터가 무한한 메모리를 갖고 있는 것처럼 간주하고 코드를 작성할 수 있게 해줍니다.

가비지 컬렉션에는 우리가 작성한 프로그램이 객체를 메모리에 할당해서 사용하고 있을 때, 객체 중 쓰레기인것과 쓰레기가 아닌 것을 완벽하게 분리해서 쓰레기들을 수거하는가비지 컬렉터라는 것이 존재합니다. 이름이 비슷하지요.
C/C++에서 저희가 직접 했던 일을 C#에서는 가비지 컬렉터가 알아서 해주니 정말 편하겠군요.. 하지만 가비지 컬렉터도 소프트웨어이기 때문에 CPU와 메모리같은 자원소모를 합니다. 저희가 코드작성에 필요한 자원을 가비지 컬렉터와 같이 사용해야 한다는 것이지요.

그래서 저흰 가비지 컬렉터가 큰 편의성을 주는 반면, 가비지 컬렉터가 최소한으로 자원을 사용할 수 있게 메모리 관리도 이루어져야 합니다. 그러기 위해서는 가비지 컬렉터의 메커니즘을 이해하고 있어야 한답니다.


[ 3. C#에서 객체가 메모리에 할당되는 과정 ]
가비지 컬렉터가 어떻게 쓰레기를 수집하는지 알아보겠습니다.

1.1 아무것도 할당되지 않은 관리되는 

C#으로 작성한 소스 코드를 컴파일해서 실행 파일을 만들고  실행 파일을 실행하면, CLR 프로그램을 위한 일정 크기의 메모리를 확보합니다. C-런타임처럼 메모리를 쪼개는 일은 하지 않습니다그냥 메모리 공간을 통째로 확보해서 하나의 관리되는 힙을 마련합니다그리고 CLR 이렇게 확보한 관리되는  메모리의 첫번째 주소에 '다음 객체를 할당할 메모리의 포인터' 위치시킵니다.

1
Object A = new Object();
cs

아무것도 할당되지 않은 메모리 공간에  번째 객체를 할당해보겠습니다. CLR 다음 코드를 실행하면 '다음 객체를 할당할 메모리의 포인터' 가리키고 있는 주소에 A객체를 할당하고 포인터를 A객체가 차지하고 있는 공간 바로 뒤로 이동시킵니다.

1
Object B = new Object();
cs


객체를  하나 만들어 보겠습니다 번째로 만드는 객체는  번째 객체의 바로 , '다음 객체를 할당할 메모리의 포인터' 가리키고 있는 곳에 할당됩니다.

객체가 메모리에 할당되는 과정을 알아보았습니다그러면 할당된 객체들이 쓰레기인지 어떻게 판단하고 가비지 컬렉터가 수집해가는지 알아보겠습니다.

4. 가비지 컬렉터(Garbage Collector)의 원리
저희가 이미 알고 있는 것처럼  형식 객체는 스택에 할당되었다가 자신이 위치한 코드 블록이 끝나면 메모리로부터 사라집니다참조 형식 객체들만이 힙에 할당되어 코드 블록과 관계없이 계속 남아있겠지요잠깐 코드를 한번 보겠습니다.

1
2
3
4
if(true)
{
    object a = new object();
}
cs

위와 같은 코드가 있을  a 어디에 존재할까요, a 스택에 할당되고 a 참조하고 있는 메모리는  메모리의 주소를 참고하고 있답니다.



만약 if문이 끝나면 a 어떻게 될까요
a변수는 if문이 끝나자마자 스택에서 사라지고  이상 존재하지 않는 변수가 됩니다.




이제 힙을 가리키고 있던 a 객체는 사라지고 힙에 할당되었던 메모리는 어디에서도 접근할  없게됩니다 쓰레기가 되버린것입니다이제  쓰레기는 가비지 컬렉터가 수집을  대상이 되어버린거지요.

여기서 가비지 컬렉터가 어떻게 쓰레기인지 구분하는가 하면은먼저 a변수처럼 힙에 할당된 메모리의 위치를 참조하고 있는 객체들을 '루트(Root)'라고 합니다.
루트는 경우에 따라 스택에 할당될   있고힙에 할당될   있습니다. .NET 애플리케이션이 실행되면 JIT컴파일러가  루트들을 목록으로 만들고, CLR  루트 목록을 관리하며 상태를 갱신합니다 루트가 중요한 이유는 가비지 컬렉터가 CLR 관리하고 있던 루트목록을 참조해서 쓰레기 수집을 하기 때문입니다.


가비지 컬렉터가 루트 목록을 이용해서 쓰레기 객체를 정리하는 과정을 다시 정리하자면,
첫번째 작업을 시작하기 전에 가비지 컬렉터는 모든 객체가 쓰레기라고 가정합니다루트 목록내의 어떤 루트도 메모리를 가리키지 않는다고 가정하지요.
그리고 나서 루트 목록을 순회하면서  루트가 참조하고 있는  객체와의 관계 여부를 조사합니다루트가 참조하고 있는 힙의 객체가  다른  객체를 참조하고 있다면  또한 해당 루트와 관계가 있는 것으로 판단합니다  어떤 루트와도 관계가 없는 힙의 객체들은 쓰레기로 간주됩니다.


쓰레기 객체가 차지하고 있던 메모리는 이제 비어있는 공간으로 간주됩니다.
루트 목록에 대한 조사가 끝나면가비지 컬렉터는 이제 힙을 순회하면서 쓰레기가 차지하고 있던 '비어있는 공간' 인접한 객체들을 이동시켜 차곡차곡 채워 넣습니다모든 객체의 이동이 끝나면 다음과 같이 깨끗한 상태의 메모리를 얻게된답니다.


[ 5. 세대별 가비지 컬렉션 ]
가비지 컬렉션의 동작 방식에 대해서 알아보았습니다이번에는 가비지 컬렉션의 성능을 높이기 위한 '세대별 가비지 컬렉션 알고리즘' 대해 알아보겠습니다.
CLR 메모리를 구역별로 나누어 메모리에서 빨리 해제가  객체와 오래남아있을 객체들을 따로 담아 관리합니다. 구체적으로 이야기 하면 CLR 메모리를 0, 1, 2 3 세대로 나누고 0세대에는 빨리 사라질 것으로 예상되는 객체들을2세대에는 오랫동안 남아있을 객체들을 위치시킵니다. CLR 객체의 수명을 예측하는 방법은객체가 가비지 컬렉션을 겪은 횟수 따라 나뉘는데요 횟수가 높을수록 메모리에서 오랫동안 남아있는 객체로 간주되고횟수가 낮을 경우 빨리 사라질것이라고 간주됩니다.

따라서 0세대에는 가비지 컬렉션을  번도 겪지 않은  생성된 객체들이 위치하게 되고 2세대에는 최소 2회에서  차례동안 가비지 컬렉션을 겪고도 남은 객체들이 위치하게 됩니다.


.NET 애플리케이션이 실행이 되면 CLR 위와같이 비어있는 힙을 확보합니다어떠한 객체도 할당되지 않은 상태이지요.



애플리케이션이 진행이 되면 차츰차츰 객체들이 힙에 할당이 됩니다할당된 객체들의  크기가 0세대 가비지 컬렉션 임계치에 도달하면 가비지 컬렉터는 0세대에 대해 가비지 컬렉션을 수행을 합니다.


그리고 나서 살아남은 객체들은 1세대로 옮겨지게 되지요이로써 0세대는 깨끗하게 비워지며, 2세대도 아직까지는 깨끗한 상태로 남아있게되죠.


여전히 어플리케이션은 객체를 힙에 할당을 합니다새로 생성된 객체들은 당연히 0세대에 할당이 이루어집니다. 1세대에는 이전 가비지 컬렉션에서 살아남은 객체들이, 0세대에는 새로 생성된 객체들이 존재하게 됩니다이제 0세대 객체의 용량이 0세대 가비지 컬렉션 임계치를 넘어서게되면가비지 컬렉터는 다시 움직이기 시작합니다가비지 컬렉터는  다시 0세대에 대해 가비지 컬렉션을 수행합니다.


0세대는 깨끗이 비워졌지만  다시 어플리케이션에 의해 새로운 객체들이 할당이 됩니다이번에는 1세대의 임계치가 초과되었기 때문에 1세대에 대해 가비지 컬렉션을 수행합니다.


  가비지 컬렉터는 하위 세대에 대해서도 가비지 컬렉션을 수행하기 때문에 0세대와 1세대에 대한 가비지 컬렉션이 수행됩니다.   0세대에서 살아남은 객체는 1세대로,
1세대에서 살아남은 객체는 2세대로 옮겨집니다.


그리고 계속 어플리케이션이 종료가 되기 전까지 객체가 생성이 되고  메모리에 채워지게됩니다그러면  다시 0세대에 새로운 객체들로 채워지기 시작하지요 세대의 메모리 임계치에 따라 가비지 컬렉션이 수행이 되고가비지 컬렉션의 반복됨에 따라 0세대의 객체들은 1세대로, 1세대의 객체들은 2세대로 계속 이동을 합니다. 

하지만 2세대로 옮겨간 객체들은  이상 다른 곳으로 옮겨가지 않습니다. 2세대도 포화되어 2세대에 대한 가비지 컬렉션이 수행이 되면 0세대 1세대 모두 가비지 컬렉션을 수행합니다전체 가비지 컬렉션이라고 부르기도 하지요.


하지만 0세대 가비지 컬렉션이 수행될 , 1세대 2세대는 수행되지 않습니다. 1세대때에도 마찬가지로 2세대는 수행되지 않지요 처럼 힙의  세대는 2세대 < 1세대 < 0세대 순으로 가비지 컬렉션 빈도가 높습니다.  떄문에 2세대 객체들은 오랫동안 살아남을 확률이 높고따라서 가비지 컬렉터도 상대적으로  관심을 주는 편입니다.

만약, 2세대 임계치에 도달하게  경우 어떤일이 발생할까요?,

CLR 어플리케이션의 실행을 잠시 멈추고 전체 가비지 컬렉션을 수행함으로써 여유 메모리를 확보하려 합니다. CLR Full GC  때는 0세대부터 2세대까지의 전체 메모리를 거쳐 쓰레기를 수집하기 때문에 어플리케이션이 차지하고 있던 메모리가 크면 클수록 Full GC 시간이 길어지므로 어플리케이션이 정지되어있는 시간도 그만큼 늘어나게 됩니다.

 문제는 가비지 컬렉션을 이해해야 하는 가장 중요한 이유중 하나이기도 합니다.