23.03.02. (목) - ★가비지 컬렉션★ 면접때 필수 질문!!

김도익·2023년 3월 2일
0

C#

목록 보기
25/29

<교수님 말씀>

  • 메모리를 관리하는 것은 정말 어렵다.

  • 사용하지 않는 메모리를 가비지(garbage)라고 표현합니다.

  • 가비지 컬렉션이 있는 곳에도 누수가 날 수 있습니다.

  • 참조에 강한 영향을 주는 것을 강한 참조
    참조에 약한 영향을 주는 것을 약한 참조

  • 추적 가비지 컬렉션, 참조 카운팅은 무조건 머릿속에 있어야합니다.

  • C#은 CLR이 가비지 컬렉션을 지원합니다.

  • 파편화 == 단편화

  • 메모리 할당은 0세대에 할당됩니다. 1세대, 2세대에서는 할당되지 않습니다.

  • 메모리 해제는 가비지 컬렉터가 가장 적합한 때 자동으로 일어나기 때문에 정확하게 언제 해제되는지는 확인이 불가능하다.

  • LOH는 덮어쓰지 않습니다. LOH는 크기가 크기 때문입니다.
    LOH는 압축이 일어나지 않습니다.
    그래서 메모리 단편화가 발생할 수 있습니다.

  • 가비지 컬렉터는 자원을 크게 소모하는 연산입니다.
    멀티스레드 환경일 경우 가비지 컬렉션이 수행되는 동안 다른 스레드가 중단 됩니다.

리소스란?

    1. 리소스
      운영체제로부터 빌려와 사용한 후 반환해야하는 것
    1. 메모리도 리소스
      → 동적 할당된 메모리를 사용한 후에는 시스템에 돌려줘야 함

개요

본디 메모리는 리소스입니다. 즉, 동적 할당된 메모리를 전부 사용했다면 시스템에 돌려줘야합니다. 그래서 C++에서는 메모리를 프로그래머가 관리했습니다.*

*C++11 부터는 스마트 푸인터 라이브러리가 추가되어 모던 C++에서는 명시적 해제보다는 스마트 포인터 사용을 권장하고 있습니다.

이 과정에서 여러가지 실수가 많이 발생하는데 아래와 같습니다.

  • 메모리 누수(Memory Leak)

    • 메모리 사용이 끝났음에도 불구하고 해제하지 않은 것입니다.
  • 이중 해제(Double Free)

    • 이미 해제가 된 메모리임에도 불구하고 또 해제하는 것입니다.

      • 이미 해제가 된 메모리를 가리키는 포인터 / 레퍼런스를 댕글링 포인터 / 댕글링 레퍼런스(Dangling Pointer / Reference)라고 합니다.
  • 섣부른 해제(Premature Freee)

    • 아직 사용이 끝나지 않았음에도 불구하고 해제하는 것입니다.

이런 불편함을 해결하기 위해 나온 기술이 자동 메모리 관리(Automatic Memory Management) 기술인 가비지 컬렉션(Garbage Collection)입니다.

이번 시간에는 가비지 컬렉션에 대해서 알아보고 .NET 아키텍처에서는 어떻게 동작하고 있는지 알아보도록 하겠습니다.

동작원리

가비지 컬렉션은 가비지 컬렉터(Gabage Collector)가 더이상 사용하지 않는 메모리*를 재사용 하는 것입니다.

*이를 가비지(Gabage)라고 합니다.

하지만 애석하게도 어떤 객체가 아직 사용되고 있고, 사용되지 않는지(which object is still lived) 정확하게 판변할 수 있는 알고리즘이 없습니다.**

**정지 문제(Halting Problem)와 관련이 있습니다.

그래서 다음과 같은 2가지 방법으로 객체의 사용 유무(Liveness)를 가정합니다.

1. 추적 가비지 컬렉션(Tracing Garbage Collection)

  • 추적 방식에서는 도달 가능성(Reachability)으로 생존을 가정하는데 루트(Root)에서 출발해 해당 메모리까지 도달할 수 있는지 보고, 도달되지 못한 메모리는 가비지로 가정합니다.

2. 참조 카운팅(Referece Couting)

  • 참조 카운팅*에서는 해당 메모리에 참조하는 것이 없을 때 가비지로 가정합니다.

*앞서 말한 스마트 포인터 라이브러리가 이 방식으로 동작합니다.

  • 참조 카운팅 방법은 순환 참조(Circular Reference)를 주의해야 합니다.
    순환 참조란 서로 다른 두 메모리가 서로를 참조하는 것을 의미합니다.
    이를 방지하기 위해 참조 횟수에 영향을 주지 않고 참조를 하는 약한 참조(Week Reference)라는 개념을 사용합니다.

이 두방법은 하이브리드 형식으로 같이 사용될 수 있습니다.

가비지 컬렉션에도 여러 가지 종류가 있는데,
보수적 가비지 컬렉션(Conservative Garbage Collection)
복제 가비지 컬렉션(Copying Garbage Collection)
분산 가비지 컬렉션(Disributed Garbage Collection)
증분 가비지 컬렉션(Incremental Garbage Collection) 등등이 있습니다.

.NET 아키텍처의 가비지 컬렉션

C#은 가비지 컬렉션을 지원합니다.*
가비지 컬렉션을 지원하는 언어를 매니지드 언어(Managed Language)라고 합니다.**

*엄밀히는 CLR이 지원합니다.

** 반대를 언매니지드 언어(Unmanaged Language)라고 합니다. 언매니지드 언어는 프로그래머가 거의 모든 작업을 수행하고, 실제 프로그램이 운영체제에 의해 로드 되므로 네이티브 언어(Native Language)라고도 합니다.

C#에서 사용하는 방식은 세대별 가비지 수집(Generational Garbage Collection)입니다. 어떻게 동작하는지 확인해보겠습니다.

세대

먼저 세대(Generation)에 대한 이해가 필요합니다.
가비지 컬렉터가 관리하는 메모리를 매니지드 힙(Managed Heap)이라고 합니다.

이 힙을 0세대, 1세대, 2세대 총 3개의 세대로 나눠 관리합니다.
세대를 나눈 이유는 메모리를 재사용하기 용이하기 때문입니다.

  • 가비지 컬렉션이 일어날 때 파편화를 방지하기 위해 메모리를 압축하는 데, 힙 전체를 대상으로 하기보다 일부분에서만 수행 하는게 더 빠릅니다.

  • 최근에 만들어진 객체일 수록 수명이 짧고 오래 사용된 객체일 수록 수명이 길어 재사용할 메모리를 빠르게 분류할 수 있습니다.

  • 메모리 할당은 0세대에서만 일어나는데 최근에 만들어진 객체끼리 서로 연관되는 경향이 있어 캐싱 측면에서 좋습니다.

매니지드 힙에는 여러 개의 포인터가 있으며 이를 이용해 세대를 구별합니다.

메모리 할당

C#에서 모든 참조 타입의 객체는 매니지드 힙의 0세대에 할당되며, 연속적으로 배치됩니다.

매니지드 힙은 메모리를 미리 시스템으로부터 할당 받아 놓기 때문에* 스택에서 메모리를 할당하는 속도만큼 빠르게 할당할 수 있고, 접근도 빠르게 할 수 있습니다.

*메모리 풀링(Memory Pooling)을 떠올리면 됩니다.

단, 85KB 이상의 크기를 가지는 객체는 LOH(Large Object Heap)라는 2세대 메모리에 할당 됩니다.

메모리 해제

메모리를 해제할 때는 추적 방식을 사용합니다.
루트에는 스택 루트, CPU 레지스터, 정적 필드 등이 있으며* 메모리 해제는 가비지 컬렉터가 가장 적합한 때** 에 자동으로 일어나게 됩니다.

*이 외에도 GC 핸들이나 Finalize 큐가 있으나, 일반적으로 고려할 부분은 아닙니다.

**상세한 조건을 알고 싶다면 여기서 확인하면 됩니다.

메모리를 해제한다고 했지만 정말로 시스템에 돌려주는 것은 아닙니다.
실제로는 다른 메모리에 의해 덮여쓰여집니다.***
이 과정에서 참조 변수의 주소값을 모두 수정하며, 각 세대의 시작을 가리키는 포인터 또한 수정합니다.

*** 단, LOH에서는 덮어쓰는 과정을 생략됩니다.

가비지 컬렉션이 일어나는 순서도 정해져 있습니다.
가장 먼저 가비지 컬렉션이 일어나는 세대는 0세대입니다.
이 과정에서 가비지가 아닌 메모리는 윗 세대로 승격(Promotion) 시킵니다.

만약 0세대에서 가비지 컬렉션을 수행했음에도 불구하고 새로운 객체를 만들기 위한 메모리 공간이 충분하지 않다면 먼저 1, 2세대 순으로 수집을 수행합니다.
그럼에도 또 부족하다면 세대 2, 1, 0의 순서로 수집을 수행합니다. 이 경우에도 2세대를 제외하곤 세대 승격은 일어납니다.

주의 사항

우리가 가비지 컬렉션에 대해서 명확히 이해해야 하는 이유는 성능과 직결되기 때문입니다.

가비지 컬렉션은 결코 자원을 적게 소모하는 연산이 아니며, 멀테스레드 환경인 경우 가비지 컬렉션이 수행되는 동안 다른 스레드가 중단(Suspend)됩니다.

따라서 아래의 사항을 주의하면 되겠습니다.

참조 카운팅 방식으로 가비지 수집이 일어나지 않습니다.

세대별 가비지 컬렉션은 추적 방식입니다.
이를 눈으로 확인해보겠습니다.



// 가바지 컬렉션이 잘 수행되었습니다.

참조 카운팅 방식이라면 할당 후 총 메모리와 수집 후 총 메모리가 같아야합니다. 하지만 그렇지 않음을 볼 수 있습니다.

왜냐면 Foo()에서 생성된 객체에 루트를 통해서 접근할 수 없기 때문입니다. 다시 말해 도달할 수 없습니다.

필요하다면 약한 참조를 사용할 수 있습니다.

어떤 객체에 도달 가능할 때, 이를 객체에 대한 강한 참조(Strong Reference)를 갖는다고 표현합니다. 하지만 객체의 도달 가능성에 영향을 주지 않으면서(객체의 생존에 영향을 주지 않으면서) 해당 객체를 참조하고 싶은 때도 있을 것입니다.

이때 약한 참조(Weak Reference)를 사용할 수 있습니다.
아래 예시 코드입니다.




약한 참조에는 짧은 참조와 긴 참조가 있는데 이에 대해서는 여기를 참고하면 되겠습니다.

빈번한 할당을 조심해야합니다.

가비지 컬렉션이 일어나는 조건 중 하나는 객체를 할당할 충분한 공간이 없을 때 입니다.

즉, 0세대가 가득 찼을 때를 의미합니다. 객체를 빈번하게 생성하면 0세대에 여유 공간이 부족해 가비지 컬렉션이 일어날 수 있습니다.

너무 큰 객체 할당은 피하도록 해야합니다.

앞서 설명했듯 85KB 이상의 크기를 가지는 객체는 LOH에 할당되며, LOH에서는 메모리 압축이 일어나지 않아 내부 단편화가 발생할 수 있습니다.

복잡한 참조 관계를 피해야합니다.

가독성도 문제지만 가비지 컬렉션 후에 메모리 주소 관리를 어렵게 합니다.

특히나 오래된 세대에서 새로운 세대에 대한 메모리를 참조하게 될 때 수집을 방지하기 위해 쓰기 장벽(Write Barrier)을 만드는 데 이는 많은 성능을 필요로 합니다.

관리되지 않는 리소스도 있습니다.

메모리 외에 파일 핸들, 윈도우 핸들, 네트워크 연결 등의 운영체제 리소스를 래핑하는 경우 제대로 정리가 되지 않습니다.

이런 경우에는 IDisposableusing문을 이용할 수 있습니다.
자세한 것은 여기서 참고하면 되겠습니다.

가비지 컬렉션은 반드시 알아야되는 내용입니다.
면접 단골 질문이기도하지만, 개발자라면 필수적으로 인지해야되는 내용입니다.
내용이 어려우니 자주 반복하면서 친해질 수 있도록 노력합시다.

profile
고급 개발자가 되고 싶어요!

0개의 댓글