Tag Archives: C#

C# 6.0 완벽 가이드/ C#의 사용자 정의 형식

클래스

메서드

메서드에 적용할 수 있는 수정자들

정적 수정자 static
접근 수정자 public, internal, private, protected
상속 수정자 new, virtual, abstract, override, sealed
부분 메서드 수정자 partial
비관리 코드 수정자 unsafe, extern
비동기 코드 수정자 async

식 본문 메서드 (C# 6)

int Foo (int x) { return x * 2; }

int Foo (int x) => x * 2; // 위 코드와 동일

void Foo (int x) => Console.WriteLine(x); // 반환형이 void인 함수도 이런 식으로 사용할 수 있다.
  • 위의 코드와 같이 표현식이 본문인 메서드(expression-bodied method)를 줄여서 식본문 메서드라고 부른다. 중괄호와 return 키워드 대신 ‘이중 화살표’가 쓰였다.

Continue reading

C# 6.0 완벽 가이드/ C# 언어의 기초

첫 번째 C# 프로그램

컴파일

  • C# 컴파일러는 확장자가 .cs인 일단의 소스코드 파일들을 컴파일해서 하나의 어셈블리를 생성한다. 어셈블리는 .NET 프로그램 패키징 및 배포 단위이다. 어셈블리는 응용 프로그램(application)일 수도 있고 라이브러리(library)일 수도 있다.
  • 보통의 콘솔 또는 Windows 응용 프로그램은 진입점 Main 메서드를 가지고 있으며 파일 확장자는 .exe이다. 라이브러리의 확장자는 .dll이며, .exe와는 달리 진입점이 없다. 라이브러리의 주된 용도는 라이브러리 안의 코드를 다른 응용 프로그램이나 다른 라이브러리가 호출 또는 참조하는 것이다. .NET Framework는 라이브러리들의 집합이다.
  • C# 컴파일러 프로그램의 이름은 csc.exe이다. C# 프로그램은 Visual Studio 같은 IDE로 컴파일 할 수도 있고, 명령줄에서 csc를 직접 실행해서 컴파일 할 수도 있다.
csc MyFirstProgram.cs // 응용 프로그램 컴파일
csc /target:library MyFirstProgram.cs // 라이브러리 컴파일

구문

키워드와의 충돌 피하기

  • 예약된 키워드에 해당하는 식별자를 꼭 사용해야 한다면 식별자 앞에 접두사 @를 붙이면 된다.
    • @ 기호는 식별자 자체의 일부로 간주되지 않는다. 즉, @myVariable은 myVariable과 같다.
    • 접두사 @는 C#과는 다른 키워드들을 가진 다른 .NET 언어로 작성된 라이브러리를 C# 프로그램에서 사용할 때 유용할 수 있다.

Continue reading

C# 6.0 완벽 가이드/ C#과 .NET Framework 소개

(필요한 부분만 정리)

C#과 CLR의 관계

  • C#은 자동 메모리 관리와 예외 처리 같은 다양한 기능을 갖춘 런타임에 의존한다. C#의 설계는 해당 실행시점 기능들을 제공하는 Microsoft CLR의 설계와 밀접하게 대응된다.(비록 기술적으로는 C#이 CLR과 독립적이지만) 게다가 C#의 형식 체계는 CLR의 형식 체계와 밀접하게 대응된다.

CLR과 .NET Framework

  • .NET Framework는 CLR과 아주 다양한 라이브러리들의 집합으로 구성되어 있다. 그 라이브러리 집합에는 핵심 라이브러리들과 그 핵심 라이브러리들에 의존하는 응용 라이브러리들이 있다.
  • CLR은 관리되는 코드(managed code)를 실행하기 위한 런타임이다. C#은 여러 관리되는 언어(managed language) 중 하나인데, 관리되는 언어로 작성한 소스 코드를 컴파일하면 관리되는 코드가 생성된다. 관리되는 코드를 실행 파일(.exe) 또는 라이브러리(.dll) 형태로 만들고 그것을 형식 정보, 즉 메타자료(metadata)와 함께 하나의 패키지로 묶은 것을 어셈블리(assembly)라고 부른다.
  • 관리되는 코드는 중간 언어(intermediate language, IL)로 표현된다. 어셈블리를 적제(load)할 때 CLR은 어셈블리에 담긴 IL 코드를 해당 컴퓨터(x86 등) 고유의 기계어 코드로 변환한다. 이러한 변환을 담당하는 것이 CLR의 JIT(just-in-time) 컴파일러이다. 어셈블리는 원래의 원본 언어의 구성을 거의 그대로 유지하기 때문에 코드를 조사하기 쉽고 심지어 동적으로 생성하기도 쉽다.
    • ILSpy나 dotPeek, Reflector 같은 도구를 이용하면 IL 어셈블리의 내용을 조사하고 역컴파일 할 수 있다.
  • CLR은 여러 실행시점 서비스들의 호스트 역할을 한다. 그런 서시브들로는 이를테면 메모리 관리, 라이브러리 적재, 보안 서비스 등이 있다. CLR은 언어 중립적이므로 여러 언어(C#, F#, Visual Basic, .NET, Managed C++ 등)로 응용 프로그램을 개발하는 것이 가능하다.

Continue reading

C#/ Exponential Notation

지수 표기법

자리수가 큰 수를 지수를 이용해서 줄여서 표기하는 법을 말한다. 특정 숫자를 Numberx10n 꼴로 만든 후에, 지수인 n을 e 다음에 표기하는 것. 예컨대 12000000000 => 1.2e+9, 0.00000012 => 1.2e-7 같은 식으로 표현된다. 자리수가 큰 숫자를 짧게 줄여 쓰는 표기법.

e 다음에 오는 숫자가 양수면 10n 형태이기 때문에 뒤의 숫자만큼 오른쪽으로 소수점을 옮기고, 음수면 (1/10)n 형태이기 때문에 뒤의 숫자만큼 왼쪽으로 소수점을 옮긴다.

지수 표기법 변환

지수 표기법을 일반 숫자로 바꾸기

지수 표기법으로 넘어온 데이터를 일반 숫자로 바꾸려면 각 숫자 형식의 Parse를 시도하면 된다. 물론 Parse 자체보다는 TryParse가 안전하니 그 방법을 사용하는 것을 권장한다. TryParse는 out 키워드가 포함되어야 한다.

// parse를 이용한 방법
decimal x = decimal.Parse("1.2345E-02", NumberStyles.Any);

// try parse를 이용한 방법
// NumberStyle 뿐만 아니라 CultureInfo도 포함되어야 한다.
decimal result;
if( !decimal.TryParse("1.2345E-02", NumberStyles.Any, CultureInfo.InvariantCulture, out result) )
{
     // 실패시 처리
}

일반 숫자를 지수 표기법으로 바꾸기

일반 숫자를 지수 표기법으로 바꾸는 것은 매우 쉽다. 해당 숫자 형식을 ToString(“e”) 해주면 된다.

decimal num = 10000000000m;
string ex = num.ToString("e");

C#/ Linked List

Linked List의 개념

자료구조에서 배열은 초기화할 때 메모리 크기가 정해지기 때문에 메모리 관리에 뛰어나고 사용도 쉽지만 유연함이 떨어진다. 이에 반해 연결 리스트는 연결이 언제 추가, 삭제될지 모르기 때문에 메모리 상에 이곳저곳에 퍼져있게 되어 메모리 관리의 효율성은 떨어지지만 새로운 연결을 추가하거나 기존 연결을 삭제하는 유연함은 대단히 높다.

연결 리스트는 자기 자신의 데이터 + 다음 노드의 포인터로 구성되는데, 그 포인터를 이용해서 순차적인 리스트를 구성한다.

C#에서의 Linked List

C#에서는 LinkedList<T> 를 통해 연결 리스트를 사용할 수 있다. AddFirst(), AddLast(), AddAfter(), AddBefore() 등 연결 리스트의 기본적인 개념을 이용한 메서드들이 제공되고 있는데, 기본적으로 List<T> 보다 성능이 떨어지므로 List<T>를 사용하는 것을 권장한다.

연결리스트는 개념만 이해하면 좋을 듯.

C#/ Using과 IDisposable

Using과 IDisposable

C# 에서 메모리는 가비지 컬렉터가 관리해 주지만, 그 외의 열린 파일, 스트림 같은 관리되지 않는 리소스들은 인식하지 못한다. 이런 리소스들은 프로그래머가 명시적으로 해제를 해줘야 하는데 그때 사용하는게 Dispose()이다.

가비지 컬렉터가 제어할 수 없는 리소스를 제어하는 클래스가 IDisposable을 상속 받아 Dispose()를 구현하고 using을 통해 사용하면 된다.

public class Book : IDisposable
{
    public void Dispose ()
    {
        // 리소스 해제
    }
}

public class Program
{
    public void ReadBook ()
    {
        using (Book novel = new Book())
        {
            // 실행문
        }
    }
}

위와 같이 코드를 구성하면 using 블록을 빠져나갈 때 프로그램이 Dispose()를 실행시켜줘서 리소스를 자동으로 해제하게 된다. using이 네임스페이스를 지정하는 것 외에 Dispose()를 실행시키는 것으로도 쓰이는 것이다.

C#/ DLL 사용하기

C#에서 만든 DLL 사용하기

C#에서 만들어진 DLL 사용하는 방법은 매우 쉽다. using 키워드로 해당 DLL 을 불러온 후에 일반 클래스 사용하듯이 사용하면 된다.

using MyDLL;

static void Main (string[] args)
{
    // MyDLL에 정의된 클래스로 객체 정의하기
    Calcurate cal = new Calcurate();

    int resultAdd = cal.add(1, 2);
    int resultMinus = cal.minus(5, 3);
    int resultMultiply = cal.multiply(2, 4);
    int resultDivide = cal.divide(6, 3);
}

C, C++에서 만든 DLL 사용하기

C나 C++로 만든 DLL은 System.Runtime.InteropServices을 using 한 후에, DLLImport라는 애트리뷰트와 extern이라는 키워드를 이용해서 사용해야 한다.

using System.Runtime.InteropServices;

public class SqliteDatabase
{
    [DllImport("sqlite3", EntryPoint = "sqlite3_open")]
    private static extern int sqlite3_open (string filename, out IntPtr db);

    ...

    void Open (string path)
    {
        IntPtr _connection;

        if (sqlite3_open (path, out _connection) != 0) 
        {
            throw new SqliteException ("Could not open database file: " + path);
        }
    }
}

extern은 해당 메서드의 구현을 바깥 –여기서는 DLL– 에 맡긴다는 의미다. extern 한정자가 붙은 메서드는 static으로 선언되어야 한다.

extern 키워드가 들어간 함수 이름과 매개변수는 DLL 코드 안에 정의된 것과 같아야 한다. 이렇게 정의된 메서드는 일반 메서드처럼 사용할 수 있다.

C#/ String과 StringBuilder

String과 StringBuilder

string은 char[]로서 immutable 타입이다. 이것이 무슨 말인고 하니 string에 ‘+’ 연산을 하면, 현재 string에 새로운 string에 더해지는게 아니라, 현재 string과 새로운 string을 더한 새로운 string을 만들어 낸다는 이야기다.

아래와 같은 코드는 loop를 돌 때마다 추가적으로 string을 만들어 낸다. string이 char[]임을 생각해보면 사실 당연한 결과다.

string num = "";

for (int i = 0; i < 10; i++)
{
    num += i.ToString();
}

이는 자원 낭비이므로 위와 같이 string을 반복적으로 연산할 일이 있을 때는 StringBuilder를 사용하도록 권장된다. StringBuilder는 string과는 사용법이 약간 다른데, 변수보다는 메서드를 사용하는 방식에 가깝다.

StringBuilder num = new StringBuilder();

for (int i = 0; i < 10; i++)
{
    num.Append(i.ToString());
}

StringBuilder는 ToString() 으로 string 타입으로 변환시킬 수 있다.

C#/ C#의 메모리 구조

C#의 메모리는 3가지 영역으로 구분된다.

데이터

데이터를 보관하는 영역, static 등이 해당된다. static 으로 선언된 데이터는 프로그램이 시작될 때 메모리에 올라왔다가 프로그램이 종료될 때까지 메모리에서 해제되지 않는다.

스택

값형 변수들이 올라오는 영역. 메서드가 종료되면 스택 메모리는 해제된다. 메모리의 스택 영역은 컴파일시에 결정된다.

참조형 변수들이 올라오는 영역. 엄밀히 말하면 참조형 변수들은 스택과 힙을 모두 사용하는데, 힙에는 데이터가 올라오고, 스택에는 힙의 메모리 주소가 올라온다. 메모리의 힙 영역은 런타임시에 결정된다.

복수의 스택이 힙 메모리의 주소만 갖고 있으면 하나의 데이터를 여러 곳에서 사용할 수 있는 형식이기 때문에 전체 메모리 관리에 좋다.

스택과 달리 메서드가 종료되도 사라지지 않는데 –물론 주소를 들고 있는 스택 메모리는 사라짐– 만일 힙 메모리의 주소를 갖고 있는 스택이 없으면 가비지 컬렉터가 수거해 간다.

뇌를 자극하는 C# 4.0 프로그래밍/ 가비지 컬렉션

가비지 컬렉터를 아시나요

  • C와 C++의 메모리 관리 문제점
    • 객체를 할당하기 위해 일일이 메모리 공간을 확보해야 하며, 객체를 할당한 후에는 힙을 가리키는 포인터를 잘 유지하고 있다가 객체를 다 사용하고 나면 해당 포인터가 가리키고 있는 메모리를 해제해줘야 한다. 이 때 프로그래머들이 실수로 객체의 메모리를 해제하는 것을 잊으면 메모리 누수가 발생한다.
    • 한편 메모리를 제대로 해제 했는데, 해제한 줄 모르고 그 포인터에 접근해서 코드를 실행하는 실수가 발생하는 경우도 많다. 그 포인터가 가리키는 메모리가 비어있을 수도 있지만 엉뚱한 코드가 그 자리를 대신 차지하고 있을 수도 있기 때문에 어떤 일이 벌어질지 예측하기가 어렵다.
  • 한편 프로그래머의 실수와는 별도로 C와 C++은 힙에 객체를 할당하기 위해 비싼 비용을 치뤄야 한다는 문제도 있다.
    • C, C++ 기반의 프로그램을 실행하는 C-런타임은 객체를 담기 위한 메모리를 여러 개의 블록으로 나눈 뒤, 이 블록을 링크드 리스트로 묶어서 관리한다. 가령 어떤 객체를 힙에 할당하는 코드가 실행되면 C-런타임은 메모리 링크드 리스트를 순차적으로 탐색하면서 해당 객체를 담을 만한 여유가 있는 메모리 블록을 찾느나. 적절한 크기의 메모리 블록을 만나면 프로그램은 이 메모리 블록을 쪼개서 객체를 할당하고 메모리 블록의 링크드 리스트를 재조정한다.
    • 정리하자면 단순히 메모리 공간에 데이터를 집어 넣는 것이 아니라 공간을 ‘탐색’하고 ‘분할’하고 ‘재조정’하는 오버헤드가 발생한다는 것
  • 반면 C#은 CLR이 자동 메모리 관리를 해주기 때문에 이런 문제들로부터 자유롭다. 이 자동 메모리 관리 기능의 중심에는 가비지 컬렉션이 있다. 가비지 컬렉션은 프로그래머가 무한한 메모리를 갖고 있는 것처럼 간주하고 코드를 작성할 수 있도록 한다.
  • 가비지 컬렉션을 담당하는 것을 가비지 컬렉터라고 하는데, 프로그래머가 객체를 할당해서 일을 하면 가비지 컬렉터는 객체 중에 쓰레기인 것과 쓰레기가 아닌 것을 완벽하게 분리해서 쓰레기들만 수거해 간다.
  • 가비지 컬렉터가 똑똑하게 일을 하지만 가비지 컬렉터 자체도 소프트웨어이기 때문에 CPU와 메모리 같은 컴퓨팅 자원을 소모한다. 우리가 작성한 코드도 사용해야 하는 그 자원을 가비지 컬렉터도 같이 사용해야 한다는 이야기. 만약 가비지 컬렉터가 최소한으로 이 자원을 사용하게 만들 수 있다면 프로그램의 성능을 아낀 자원의 양만큼 끌어올릴 수 있게 된다.
  • 참고) 기본적으로 C#으로 작성된 모든 코드는 CLR에게 관리되는 관리형 코드(Managed Code)에 속한다. 한편 비관리형 코드(Unmanaged Code)도 작성할 수 있는데, unsafe 키워드를 사용하면 된다. 대신 이 경우에는 가비지 컬렉션을 포함하여 CLR이 제공하는 서비스를 받을 수 없다.

개처럼 할당하고 정승처럼 수거하라

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

  • 비어 있는 힙에 첫 번째 객체를 할당하면 –object A = new object()– CLR은 ‘다음 객체를 할당할 메모리의 포인터’가 가리키는 주소에 A 객체를 할당하고 포인터를 A 객체가 차지하고 있는 공간 바로 뒤로 이동시킨다.

  • 여기서 객체를 또 할당하면 첫 번째 객체의 바로 뒤에 새로운 객체가 할당된다.

  • CLR은 객체가 위치할 메모리를 할당하기 위해 메모리 공간을 쪼개 링크드 리스트를 탐색하는 시간도 소요하지 않으며, 그 공간을 다시 나눈 뒤에 리스트를 재조정하는 작업도 필요로 하지 않는다. C-런타임에 비해 객체 할당 메커니즘이 단순한데다 효율적이다.
  • 값 형식 객체는 스택에 할당 되었다가 자신이 태어난 코드 블록이 끝나면 메모리로부터 바로 사라지고 참조 형식 객체들만이 힙에 할당되어 코드 블록과 관계없이 살아남는다. 만일 참조 객체를 만들면 객체의 내용물은 힙에 할당되지만 스택에 힙 메모리 주소의 참조가 생성된다.

  • 위 그림과 같은 상황에서 객체가 담긴 코드 블록이 끝나서 스택이 사라지면 힙에는 더는 접근할 수 없어서 사용할 수 없는 메모리가 남게 된다.

  • 객체를 잃은 채 힙에 남아 있는 객체는 쓰레기가 되고 이 쓰레기는 가비지 컬렉터가 집어가게 된다.
  • 한편 사라져 버린 객체처럼 할당된 메모리의 위치를 참조하는 객체를 루트(Root)라고 부르는데 루트는 스택에 생성될 수도 있고 정적 필드처럼 힙에 생성될 수도 있다. .NET 응용 프로그램이 실행되면 JIT 컴파일러가 이 루트들을 목록으로 만들고, CLR은 이 루트 목록을 관리하며 상태를 갱신한다. 이 루트가 중요한 이유는 가비지 컬렉터가 CLR이 관리하고 있던 루트 목록을 참조해서 쓰레기 수집을 하기 때문이다.

  • 가비지 컬렉터가 루트 목록을 이용해서 쓰레기 객체를 정리하는 과정은 다음과 같다.
    1. 작업을 시작하기 전에 가비지 컬렉터는 모든 객체가 쓰레기라고 가정한다. 즉, 루트 목록 내의 어떤 루트도 메모리를 가리키지 않는다고 가정한다.
    2. 루트 목록을 순회하면서 각 루트가 참조하고 있는 힙 객체와의 관계 여부를 조사한다. 막약 루트가 참조하고 있는 힙의 객체가 또 다른 힙 객체를 참조하고 있다면 이 역시도 해당 루트와 관계가 있는 것으로 판단한다. 이때 어떤 루트와도 관계가 없는 힙의 객체들은 쓰레기로 간주된다.
    3. 쓰레기 객체가 차지하고 있던 메모리는 ‘비어 있는 공간’이 된다.
    4. 루트 목록에 대한 조사가 끝나면 가비지 컬렉터는 이제 힙을 순회하면서 쓰레기가 차지하고 있던 ‘비어 있는 공간’에 쓰레기의 인접 객체들을 이동시켜 차곡차곡 채워 넣는다. 모든 깨체의 이동이 끝나면 깨끗한 상태의 메모리를 얻게 된다.

세대별 가비지 컬렉션

  • 버스에서 빨리 내리려는 승객은 출입구 쪽에 있고 늦게 내리려는 승객은 출입구 반대편에 있는 것처럼 CLR의 메모리도 구역을 나누어 메모리에서 빨리 해제될 객체와 오래 살아남을 것 같은 객체들을 따로 담아 관리한다.
  • CLR은 메모리를 0, 1, 2의 3개 세대로 나누고 0세대에는 빨리 사라질 것으로 예상되는 객체들을, 2세대에는 오랫동안 살아남을 것으로 예상되는 객체들을 위치시킨다.
  • CLR은 객체의 나이가 어릴수록 메모리에서 빨리 사라지고, 나이가 많을수록 메모리에서 오래 살아 남는다고 간주한다. 여기서 나이는 가비지 컬렉션을 겪은 횟수를 의미하는데, 따라서 0세대에서는 가비지 컬렉션을 한 번도 겪지 않은 ‘갓 생성된’ 객체들이 위치하고 2세대에는 최소 2회에서 수차례 가비지 컬렉션을 겪고도 살아남은 산전 수전 다 겪은 객체들이 위치한다.
  • .NET 응용 프로그램이 시작되면 CLR은 다음과 같은 (비어 있는) 관리되는 힙을 확보한다. 이 힙에는 아직 어떤 객체도 할당되지 않은 상태이다.

  • 응용 프로그램이 일을 시작함에 따라 할당된 객체들로 힙이 차오른다.

  • 할당된 객체들의 총 크기가 0세대 가비지 컬렉션 임계치에 도달하면 가비지 컬렉터는 0세대에 대해 가비지 컬렉션을 수행하고 여기서 살아남은 객체들을 1세대로 옮긴다. 이로서 0세대는 깨끗하게 비워지며, 2세대는 아직 깨끗한 상태로 남아 있게 된다.

  • 응용 프로그램은 여전히 객체를 생성해서 일을 하고 새로 생성된 객체들은 0세대에 할당된다.

  • 다시 0세대 객체의 용량이 0세대 가비지 컬렉션 임계치를 넘어서면 가비지 컬렉터가 다시 가비지 컬렉션을 수행한다.

  • 0세대는 깨끗히 비워졌지만 또다시 응용 프로그램에 의해 새로운 객체들이 할당된다. 이번에는 1세대의 임계치가 초과됐기 때문에 1세대에 대해 가비지 컬렉션을 수행한다. 이때 가비지 컬렉터는 하위 세대에 대해서도 가비지 컬렉션을 수행하기 때문에 0세대와 1세대에 대한 가비지 컬렉션이 수행된다. 여기서 살아남은 0세대 객체들은 1세대로, 2세대에서 살아남은 객체들은 2세대로 옮겨 간다.

  • 다시 응용 프로그램이 일을 수행하고 0세대가 객체들로 차오른다. 각 세대의 메모리 임계치에 따라 가비지 컬렉션이 수행되고, 가비지 컬렉션이 반복됨에 따라 0세대의 객체들은 1세대로, 1세대의 객체들은 2세대로 계속 이동한다. 2세대로 옮겨간 객체들은 더는 다른 곳으로 옮겨가지 않고 그곳에 정착한다.
  • 2세대도 포화되어 2세대에 대한 가비지 컬렉션이 수행되면 가비지 컬렉터는 동시에 1세대와 0세대에 대해서도 가비지 컬렉션을 수행한다. 그래서 2세대 가비지 컬렉션을 전체 가비지 컬렉션이라고 부르기도 한다.

  • 2세대의 힙이 가득차게 되면 CLR은 응용 프로그램의 실행을 ‘잠시 멈추고’ 전체 가비지 컬렉션을 실행하여 여유 메모리를 확보하려고 하는데, 이때 응용 프로그램이 차지하고 있던 메모리가 크면 클수록 Full GC 시간이 길어지므로 응용 프로그램이 정지하는 시간도 그만큼 늘어나게 된다.

가비지 컬렉션을 이해했습니다. 우리는 뭘 해야 하죠?

객체를 너무 많이 할당하지 마세요

  • CLR의 객체 할당 속도가 빠르긴 하지만 너무 많은 수의 객체가 관리되는 힙의 각 세대애 대해 메모리 포화를 초래하고 이는 빈번한 가비지 컬렉션을 부르는 결과를 낳는다.

너무 큰 객체 할당을 피하세요

  • CLR은 보통 크기의 객체를 할당하는 힙과는 별도로 85kb 이상의 대형 객체를 할당하기 위해 ‘대형 객체 힙(Large Object Heap, LOH)’을 따로 유지한다. 커다란 객체를 소형 객체 힙에 할당하면 0세대가 빠르게 차오르므로 이는 좋은 방법이다.
  • 그런데 이 대형 객체 힙은 메모리를 0 바이트의 낭비도 없이 사용하는 소형 객체 힙(Small Object Heap, SOH)과 달리 큰 공간을 군데군데 낭비하게 된다. 또한 CLR이 LOH를 2세대 힙으로 간주하기 때문에 LOH에 있는 쓰레기 객체가 수거되려면 2세대에 대한 가비지 컬렉션이 수행되어야 한다. 2세대에 대한 가비지 컬렉션은 전 세대에 대한 가비지 컬렉션을 뜻하기 때문에 조심해야 한다.

너무 복잡한 참조 관계는 만들지 마세요

  • 위 그림과 같은 클래스간 복잡한 참조 관계는 가독성을 떨어뜨리기 때문에 지양해야 한다.
  • 한편 이렇게 참조 관계가 많은 객체는 가비지 컬렉션 후 살아 남았을 때 문제가 된다. 가비지 컬렉터는 가비지 컬렉션 후에 살아 남은 객체의 세대를 옮기기 위해 메모리 복사를 수행하는데, 이때 참조 관계가 복잡한 객체의 경우 단순히 메모리 복사를 하는데 끝나지 않고 객체를 구성하고 있는 각 필드 객체 간의 참조 관계를 일일이 조사해서 참조하고 있는 메모리 주소를 전부 수정하게 된다. 클래스 구조를 간단하게 만들었으면 메모리 복사만으로 끝났을 일이 탐색과 수정까지 하게 되는 것.
  • 또한 복잡한 참조 관계에서 쓰기 장벽을 통해 가비지 컬렉션을 당하면 안되는 객체를 보호하는 일을 하게 된느데 이 때 쓰기 장벽을 생성하는데 드는 오버 헤드가 크기 때문에 참조 관계는 최소한으로 만드는 것이 좋다.

루트를 너무 많이 만들지 마세요

  • 가비지 컬렉터는 루트 목록을 돌면서 쓰레기를 찾아내는데, 루트 목록이 적어지면 그만큼 가비지 컬렉터가 검사를 수행하는 횟수가 줄어드므로 더 빠릴 가비지 컬렉션을 끝낼 수 있다.