C#에 관하여
C# 컴파일러는 확장자가 '.cs'인 소스파일을
이용하여 실행 파일을 만든다.
프로그램이 실행되면 CLR(Common Language Runtime)은
프로그램을 메모리에 올린 후 프로그램의 진입점을 찾는다.
CLR(Common Language Runtime)
C#으로 만든 프로그램이 실행되는 환경이다. 이름이 Common Lnaguage Runtime인 이유는, 이 런타임이 C#뿐 아니라 CLS(Common Language Specification) 규격을 따르는 모든 언어로 작성된 프로그램을 지원하기 때문이다. CLR은 단순히 각 언어로 작성된 프로그램의 실행뿐 아니라 서로 다른 언어로 작성된 언어 사이의 호환성을 제공하기도 한다.
CLR과 C# 컴파일에 관하여
위에서도 잠깐 언급한 바 있지만, C#으로 만든
프로그램은 CLR 위에서 실행된다.
이 CLR은 자바의 실행 황경인 자바 가상 머신과
비슷한 역할을 한다.
위의 그림에서처럼 CLR은 .NET 라이브러리와
함께 OS 위에 설치 된다.
네이티브 코드로 작성된 프로그램들은
운영체제가 직접 실행할 수 있지만,
네이티브 코드(Native Code)
CPU와 운영체제가 직접적으로 실행할 수 있는 코드들을 말한다.
C# 컴파일러가 만들어낸 실행 파일은 하드웨어가
이해할 수 없는 코드로 구성되어 있다.
즉, C# 컴파일러가 만들어낸 실행 파일은
곧바로 하드웨어가 실행할 수 없다.
C# 컴파일러는 C# 소스 코드를 컴파일 하여
IL(Intermediate Language)이라는
중간 언어로 작성된 실행파일을 만들어 낸다.
사용자가 이 파일을 실행시키면 CLR이
중간 코드를 읽어 들여
다시 하드웨어가 이해할수 있는 네이티브 코드로
컴파일 한 후 실행시킨다.
이것을 JIT(Just In Time) 컴파일이라고 부른다.
JIT(Just In Time) 컴파일
실행에 필요한 코드를 실행할 때마다 실시간으로 컴파일 해서 실행하는 것을 의미한다.
컴파일을 두 번씩이나 하는 이유
즉 C#은, C# 컴파일러가 IL 실행 파일을 만들어내고
다시 CLR이 네이티브 코드로 컴파일 한 후 실행한다.
이렇게 두 번의 컴파일 과정을 거치는 이유는
다음과 같다.
C#이 동작하는 환경이자 엔진인 CLR은 C#뿐만
아니라 다른 언어도 지원하도록 설계되었다.
서로 다른 언어들이 만나기 위한 지점이 바로
IL이라는 중간 언어이고,
이 언어로 쓰인 코드를 CLR이 다시 자신이 설치된
플랫폼에 최적화시켜 컴파일한 후 실행하는 것이다.
이 방식의 장점은 바로 플랫폼에 최적화된 코드를
만들어낸다는 것이며,
단점은 실행 시 이루어지는 컴파일 비용의 부담이다.
CLR의 역할과 기능
CLR은 단순히 C#이나 기타 언어들을 동작시키는
환경 기능 외에도,
프로그램의 오류(예외)가 발생했을 때 이를 처리하도록
도와주는 기능,
언어 간의 상속 지원, COM과의 상호 운영성 지원,
그리고 자동 메모리 관리 등의 기능을 제공한다.
이 중에서 자동 메모리 관리가 가비지 컬렉션이다.
가비지 컬렉션(Garbage Collection)
프로그램에서 더 이상 사용하지 않는 메모리를 쓰레기(Garbage)로 간주하고 수거(Collection)하는 기능을 말한다.
이는 아래에서 더 자세히 설명한다.
C#의 Boxing과 Unboxing
object 형식
C#의 object 형식은 모든 데이터를 다룰 수 있다.
모든 데이터 형식, 즉 기본 데이터 형식부터 복합
데이터 형식, 커스텀 데이터 형식 마저도
자동으로 object 형식으로부터 상속받게 된다.
즉, object 형식은 모든 데이터 형식의 조상이다.
박싱(Boxing)
object 형식은 참조 형식이기 때문에 힙에
데이터를 할당한다.
int 형식이나 double 형식은 값 형식이기 때문에
스택 영역에 데이터를 할당한다.
이때, 값 형식 데이터를 힙 영역에 할당하기 위해
참조 형식으로 변환하는 것을 박싱이라 한다.
예를 들어, int형 값을 object형으로 선언하여 사용하는 경우가 그러하다.
박싱이 일어난 경우, 실제 값은 힙 영역에 할당되고
그 참조 값은 스택 영역에 할당된다.
언박싱(Unboxing)
힙에 있던 값 형식 데이터를 값 형식 데이터에
다시 할당해야 하는 경우도 있다.
즉, 박싱된 값을 다시 값 형식 데이터로 옮기는
것을 의미한다.
위 코드에서 a는 20이 박싱되어 저장된 힙을
참조하고 있다.
b는 a가 참조하고 있는 메모리로부터 값을
복사하려고 하고 있다.
이때 박싱된 값을 값 형식 변수에 저장하는
과정을 언박싱이라고 한다.
즉, 힙 영역에 있던 값 형식을 스택 영역으로
복사해오는 것을 언박싱이라고 한다.
C#의 interface, abstract, virtual
interface 키워드
interface 키워드는 메소드, 이벤트, 인덱서, 프로퍼티만
가질 수 있다.
또한 구현부가 없으며 기본적으로 접근 제한 한정자를
사용할 수 없고 모든 것이 public으로 선언된다.
interface로 수식된 클래스는 인스턴스를 만들 수 없지만
참조형으로서 사용될 수 있다.
이를 상속하는 파생 클래스에서는 인터페이스에
선언된 모든 메소드 및 프로퍼티를 구현해야 하며,
이 메소드와 프로퍼티를 public으로 한정해야 한다.
abstract 키워드
abstract 키워드는 크래스, 메소드, 프로퍼티,
인덱서 및 이벤트와 함께 사용될 수 있다.
abstract 클래스는 불완전한 구현을 가질 수 있지만
클래스와 달리 인스턴스를 가질 수 없다.
즉, abstract 클래스는 불완전한 구현을 갖되 인스턴스는
만들지 못한다.
abstract으로 명시된 멤버는 abstract 클래스에서
파생되는 non-abstract 클래스에 의해 구현된다.
interface의 모든 멤버가 public으로 선언되는 반면
abstract는 모든 멤버가 private로 선언된다.
또한 abstract은 abstract 누락되거나 불완전한
구현을 가진 메소드를 가질 수 있다.
abstract 메소드는 private를 제외한 접근 한정자를
반드시 명시할 것을 강제한다.
abstract 메소드는 abstract 클래스를 사용하는
프로그래머가
그 기능을 정의하도록 강제하는 장치의 역할을 한다.
virtual 키워드
virtual 키워드는 메소드, 프로퍼티, 인덱서 또는 이벤트
선언을 수정하고,
파생 클래스에서 이를 재정의 하도록 허용하는 데 사용된다.
virtual 멤버의 구현은 파생 클래스의 override 멤버로
변경할 수 있다.
기본적으로 메소드는 virtual이 아니며, virtual 메소드가
아닌 메소드는 override할 수 없다.
무엇보다도, abstract와 interface와 달리 인스턴스를
생성할 수 있다.
세 키워드의 차이점과 사용 이유
설명 | interface | abstract | virtual |
객체를 생성할 수 있는가? | X | X | O |
자체 구현이 가능한가? | X | X | O |
구현의 강제성을 가지는가? | O | O | △ |
interface는 여러 클래스에 공통적인 기능을
추가하기 위해 사용한다.
interface는 기능 중심의 추상화를 지원한다고 생각한다.
abstract는 여러 클래스에서 공유할 기본적인
정의를 지정한다.
abstract은 계층적 구조에서의 추상화를 지원한다고 생각한다.
virtual은 파생된 클래스에서 override를
선택적으로 할 수 있다.
C#의 가비지 컬렉터
CLR의 메모리 할당
C#으로 작성한 소스 코드를 컴파일해서
실행 파일을 만들고
이 실행 파일을 실행하면, CLR은 이 프로그램을
위한 일정 크기의 메모리를 확보한다.
C-런타임처럼 메모리를 쪼개는 일은 하지 않는다.
그저 넓은 메모리 공간을 통째로 확보해서 하나의
관리되는 힙(Managed Heap)을 마련한다.
그리고 CLR은 이렇게 확보한 관리되는 힙 메모리의
첫 번째 주소에
"다음 객체를 할당할 메모리의 포인터"를 위치시킨다.
아직은 관리되는 힙에 아무 객체도 없다.
여기에 첫 번째 객체를 할당해 보겠다.
CLR이 다음 코드를 실행하면 "다음 객체를
할당할 메모리의 포인터"가 가리키는 주소에
A 객체를 할당하고 포인터를 A 객체가
차지하고 있는 공간 바로 뒤로 이동시킨다.
객체를 또 하나 만들어보겠다.
두 번째로 만드는 객체는 첫 번째 객체의 바로 뒤,
즉 "다음 객체를 할당할 메모리의 포인터"가
가리키는 곳에 할당된다.
보다시피 CLR은 객체가 위치할 메모리를 할당하기
위해 메모리 공간을 쪼개 만든
연결 리스트를 탐색하는 시간도 소요하지 않으며,
그 공간을 다시 나눈 뒤에 리스트를 재조정하는
작업도 필요로 하지 않는다.
그저 메모리에 할당하는 것이 전부다.
C-런타임에 비하면 CLR의 객체 할당 메커니즘은
단순하고 효율적이다.
다음 코드의 if 블록 안에서 참조 A는 스택과 힙,
어디에 존재하는가?
엄밀히 말하자면 실질적인 객체는 힙에 할당되고,
a는 객체 A가 위치하는 힙 메모리 주소를 참조한다.
이때 if 블록이 끝나게 되면 객체 A를 참조하고 있
a가 사라지게 된다.
아무도 참조하고 있지 않은 객체 A는
이제 코드 어디에서도 접근할 수 없기 때문에
더 이상 사용할 수 없다.
즉, 가비지 컬렉터의 수거 대상이 된다.
한편, 소멸된 a처럼 할당된 메모리의 위치 참조를
하는 객체를 일컬어 루트(Root)라고 부른다.
루트는 a의 경우 스택에 생성될 수도 있고,
정적 필드처럼 힙에 생성될 수도 있다.
.NET 애플리케이션이 실행되면 JIT 컴파일러가
이 루트를 목록으로 만들고,
CLR은 이 루트 목록을 관리하며 상태를 갱신한다.
이 루트가 중요한 이유는,
가비지 컬렉터가 CLR이 관리하던 루트 목록을
참조해서 쓰레기 수집을 하기 때문이다.
구체적으로 가비지 컬렉터가 루트 목록을 이용해서
쓰레기 객체를 정리하는 과정은 다음과 같다.
①
작업을 시작하기 전에, 가비지 컬렉터는 모든 객체(A, B, C, D, E, F)가 쓰레기라고 가정한다.
즉, 루트 목록 내의 어떤 루트도 메모리를 가리키지 않는다고 가정한다.
②
루트 목록을 순회하면서 각 루트가 참조하고 있는 힙 객체와의 관계 여부를 조사한다.
만약 루트가 참조하고 있는 힙의 객체가 또 다른 힙 객체를 참조하고 있다면이 역시도 해당 루트와 관계가 있는 것으로 판단한다(A, C, D, F). 이때 어떤 루트와도 관계가 없는 힙의 객체들(B, E)은 쓰레기로 간주된다.
③
쓰레기 객체가 차지하고 있던 메모리는 이제 '비어있는 공간'이 된다.
④
루트 목록에 대한 조사가 끝나면, 가비지 컬렉터는 이제 힙을 순회하면서 쓰레기가 차지했던 '비어 있는 공간'에 쓰레기의 인접 객체들(A, C, D, F)을 이동시켜 채워 넣는다. 이 작업을 '재배치'라고 한다.
모든 객체의 이동이 끝나면 다음과 같은 메모리 상태가 된다.
세대별 가비지 컬렉션
CLR의 메모리는 구역을 나누어,
메모리에서 빨리 해제될 객체와 오래도록
살아남을 객체들을 따로 담아 관리한다.
구체적으로 이야기하면 CLR은 메모리를
0, 1, 2의 3개의 세대로 나누고
0세대에는 빨리 사라질 것으로 예상되는
객체들을,
2세대에는 오랫동안 살아남을 것으로
예상되는 객체들을 위치시킨다.
CLR은 객체의 나이가 어릴수록 메모리에서
빨리 사라지고
많을수록 오랫동안 살아남는다고 간주한다.
여기서 나이라 함은 가비지 컬렉션을 겪은
횟수를 말한다.
따라서 0세대에는 가비지 컬렉션을 한 번도
겪지 않은 '갓 생성된' 객체들이 위치하고
2세대에는 최소 2회에서 수차례 동안 가비지
컬렉션을 겪은 객체들이 위치한다.
1세대에는 0세대에서 2세대로 넘어가는
과도기의 객체들이 위치한다.
예를 들어 .NET 애플리케이션이 시작되면 CLR은
다음과 같이 비어있고 관리되는 힙을 확보한다.
이 힙에는 아직 어떤 객체도 할당되지 않았다.
하지만 애플리캐이션이 일을 시작함에 따라
다음 그림처럼 할당된 객체들로 힙이 구성된다.
할당된 객체들의 총 크기가 0세대 가바지
컬렉션이 임계치에 도달하면
가비지 컬렉터는 0세대에 대해 가바지
컬렉션을 수행하고,
여기서 살아남은 객체들은 1세대로 옮긴다.
애플리케이션은 여전히 객체들을 새로
생성해서 일을 한다.
새로 생성된 이 객체들은 당연히 0세대에
할당된다.
1세대에는 이전 가비지 컬렉션에서 살아남은
객체들이,
0세대에는 새로 생성된 객체들이 위치한다.
0세대 객체의 용량이 0세대 가비지 컬렉션
임계치를 넘어서면
가비지 컬렉터는 또 다시 0세대에 대해
가비자 컬렉션을 수행한다.
0세대는 깨끗하게 비워졌지만 또 다시
애플리케이션에 의해
새로운 객체들이 힙에 할당된다.
1세대도 0세대와 마찬가지로 임계치를
넘어선다면 가비지 컬렉션이 수행된다.
이때 가비지 컬렉터는 하위 세대에 대해서도
가비지 컬렉션으 수해하기 때문에
0세대와 1세대에 대한 가비지 컬렉션이 수행된다.
이때 0세대에서 살아남은 객체들은 1세대로,
1세대에서 살아남은 객체들은 2세대로 옮겨간다.
애플리케이션은 아직 실행 중이기 때문에
다시 객체를 할당하고 0세대가 생성된다.
각 세대의 메모리 임계치에 따라서
가비지 컬렉션이 수행되고,
가비지 컬렉션이 반복됨에 따라 0세대의
객체들은 1세대로,
1세대의 객체들은 2세대로 계속 이동한다.
하지만 2세대로 옮겨간 객체들은 더이상
다른 곳으로 옮겨가지 않는다.
그곳에 정착한다는 의미이다.
2세대도 포화되어 2세대에 대한 가비지
컬렉션이 수행되면,
가비지 컬렉터는 1세대와 0세대에 대해서도
가비지 컬렉션을 수행한다.
그래서 2세대 가비지 컬렉션을 Full GC,
즉 전체 가비지 컬렉션이라고도 한다.
0세대에서 가비지 컬렉션이 수행될 경우,
1세대와 2세대의 가비지 컬렉션은
수행되지 않는다.
1세대에서 가비지 컬렉션이 수행될 때는
0세대도 함께 가비지 컬렉션이 이루어지지만
2세대에서는 아무 일도 일어나지 않는다.
하지만 2세대 가비지 컬렉션이 일어나면
0, 1세대 모두에 대해서도 수행된다.
이처럼 힙의 각 세대는 2 -> 1 -> 0세대 순으로
가바지 컬렉션의 빈도가 높다.
이때문에 2세대의 객체들은 오랫동안 살아남을
확률이 높고,
따라서 가비지 컬렉터도 상대적으로 관심을
덜 주는 편이다.
반면에 0세대의 경우 새롭게 할당되는 객체들은
모두 이 곳에 할당되는 데다가,
빠르게 포화되기 때문에 가비지 컬렉터가
자주 방문하게 된다.
1세대의 경우 0세대와 2세대의 가운데 있으니
가비지 컬렉터의 빈도도 딱 그 정도 수준이다.
가비지 컬렉션 덕에 프로그래머는 컴퓨터의 메모리
용량이 무한대라고 간주할 수 있다.
하지만 생명력이 강한 객체를 애플리케이션 위에
마구 생성해놓으면
얼마 가지 않아 2세대 힙이 임계치에 빠르게
도달할 것이다.
이때 CLR은 애플리케이션의 실행을 잠시
멈추고 Full GC를 수행함으로써
여유 메모리 공간을 확보하려고 한다.
CLR이 Full GC를 할 때는 메모리 전체에 걸쳐
쓰레기를 수집하는데,
애플리케이션이 차지하고 있던 메모리가
크면 클수록 Full GC 시간이 길어지므로
애플리케이션이 정지하는 시간도 그만큼
늘어나는 문제가 생긴다.
이 문제는 우리가 가비지 컬렉션을 이해해야
하는 중요한 이유 중 하나이기도 하다.
가비지 컬렉션을 사용자가 개선하는 방법
가비지 컬렉션의 성능 문제를 개선하는 것은
결국 프로그래머의 몫이다.
CLR의 가비지 컬렉션 메커니즘에 대한
이해를 바탕으로
적절한 전략을 정립하는 것이 최선이라고
할 수 있다.
많이 알려진 전략 몇 가지는 다음과 같다.
- 객체를 너무 많이 할당하지 말 것.
- 너무 큰 객체 할당은 피할 것.
- 너무 복잡한 참조 관계는 만들지 말 것.
- 루트를 너무 많이 만들지 말 것.
객체를 너무 많이 할당하지 말 것
가장 기본적인 지침이다.
CLR의 객체 할당 속도가 빠르긴 하지만
너무 많은 수의 객체는
관리되는 힙의 각 세대에 대해 메모리 포화를
초래하고,
이는 빈번한 가비지 컬렉션을 부르는 결과를
낳게 된다.
즉, 객체 할당 코드를 작성할 때 꼭 필요한
객체인지와
필요 이상으로 많은 객체를 생성하는 코드가
아닌지의 여부를 고려하라는 의미이다.
너무 큰 객체 할당은 피할 것
CLR은 보통 크기의 객체를 할당하는 힙과는
별도로,
85KB 이상의 대형 객체를 할당하기 위한
"대형 객체 힙"을 따로 유지한다.
평소에 사용하는 힙은 대형 객체 힙에 대비되는
개념으로 소형 객체 힙이라고 부르기도 한다.
대형 객체를 소형 객체 힙에 할당하면
0세대가 빠르게 차오르게 되므로
가비지 컬렉션을 보다 자주 수행하게 되고,
이는 애플리케이션의 성능 저하를 초해라게
될 것이다.
이러한 이유 때문에 CLR이 대형 객체 힙을
별도로 유지하는 것에도 단점이 존재한다.
우선 대형 객체 힙은 동작 방식이 소형 객체 힙과는
작동 방식이 다르다.
소형 객체 힙에서는 "다음 객체를 할당할 포인터"가
위치한 메모리에 바로 객체를 할당하지만,
대형 객체 힙은 객체의 크기를 계산한 뒤 그만한
여유 공간이 있는지 힙을 탐색하여 할당한다.
가비지 컬렉션을 수행하고 난 뒤에 소형 객체 힙은
해제된 메모리 공간에 인접 개게들을 재배치 하지만,
대형 객체 힙은 해제된 공간을 그대로 둔다.
수 MB에서 수백 MB에 이르는 메모리를 복사하는
비용이 너무 비싸기 때문이다.
이 공간은 나중에 다른 객체들이 할당되겠지만,
메모리를 0바이트도 낭비 없이 사용하는
소형 객체 힙과는 달리
큰 메모리 공간을 군데군데 낭비하게 된다.
결국 대형 객체 힙은 할당 시의 성능 뿐만 아니라
메모리 공간 효율도
소형 객체 힙에 비해 크게 떨어진다.
대형 객체 힙은 동작 방식도 C-런타임과 비슷하고,
문제점 역시 비슷하다.
문제는 이뿐만이 아니다.
CLR이 대형 객체 힙을 2세대 힙으로 간주하기
때문에
대형 객체 힙에 있는 쓰레기 객체가 수거되려면
Full GC가 수행되고
이는 순간이나마 애플리케이션의 정지를 유발한다.
너무 복잡한 참조 관계는 만들지 말 것
이 전략은 가비지 컬렉셔 성능이 아닌
코드 가독성을 위해서라도 따라야 한다.
다음은 복잡한 관계의 클래스 선언 코드이다.
이를 도식화 하면다음과 같다.
그림으로 바꿔봐도 이해하기 어려운 복잡한
구조임에는 마찬가지다.
이렇게 참조 관계가 많은 객체는 가비지 컬렉션
후에 살아남았을 때가 문제다.
가비지 컬렉터는 가비지 컬렉션 후에 살아남은
객체의 세대를 옮기기 위해 메모리 복사를 수행한다.
이때 참조 관계가 복잡한 객체의 경우에는 단순히
메모리 복사를 하는 데서 끝나지 않는다.
객체를 구성하고 있는 각 필드 객체 간 참조 관계를
일일이 조사해서
참조하고 있는 메모리 주소 전부를 수정한다.
클래스 구조를 간단하게 만들었다면 메모리 복사만으로
끝났을 일을 탐색과 수정까지 끌어들이게 되는 것이다.
참조 관계가 복잡한 객체의 문제는 또 있다.
위 코드의 D 클래스를 예로 들자면 다음과 같다.
D 클래스는 생성된지 오래되어 2세대에 존재하고,
A 형식의 필드 a를 새로 생성한 객체로 업데이트
됐다고 가정하자.
이 경우 D의 인스턴스는 2세대에 위치하고
a 필드가 참조하고 있는 메모리는 0세대에
위치한다.
이때 루트를 갖고 있지 않은 a는
0세대 가비지 컬렉션에 의해 수거될 위험에
노출된다.
CLR은 이를 방지하기 위해 쓰기 장벽(Write Barrier)
이라는 장치를 통해
가비지 컬렉터로 하여금 a 필드가 루트를
갖고 있는 것으로 간주하게 해서
0세대 가비지 컬렉션을 모면하게 해준다.
이때 쓰기 장벽을 생성하는 데 드는 오버헤드가
꽤 크다는 것이 문제이다.
참조 관계를 최소한으로 만들면 이런
오버헤드를 줄일 수 있다.
루트를 너무 많이 만들지 말 것
가비지 컬렉터는 루트 목록을 돌면서 쓰레기를
찾아낸다.
루트 목록이 작아진다면 그만큼 가비지 컬렉터가
검사를 수행하는 횟수가 줄어들므로
더 빨리 가비지 컬렉션을 끝낼 수 있다.
따라서 루트를 가급적 많이 만들지 않는 것이
성능에 유리하다.
MSDN에서 설명하는 가비지 컬렉터
가비지 컬렉터의 장점
- 개발자가 수동으로 메모리를 해제할 필요가 없다.
- 관리되는 힙에 효율적으로 객체를 할당한다.
- 더 이상 사용되지 않는 객체를 회수라고 이러한 객체의 메모리를 비워, 이후 할당에서 이 메모리를 사용할 수 있도록 한다. 관리되는 객체는 자동으로 시작을 위한 정리된 콘텐츠를 받으며, 객체의 생성자가 모든 데이터 필드를 초기화할 필요가 없다.
- 객체가 다른 객체에 할당된 메모리 자체를 사용할 수 없도록 하여 메모리 보안을 제공한다.
메모리 기본 사항
다음 목록은 중요한 CLR 메모리 개념을
요약한 것이다.
- 각 프로세스에는 고유한 개별 가상 메모리 공간이 있다. 동일 컴퓨터의 모든 프로세스는 동일한 실제 메모리와 페이지 파일(있는 경우)을 공유한다.
- 기본적으로 32비트 컴퓨터에서는 각 프로세스에 2GB 사용자 모드 가상 주소 공간이 포함된다.
- 애플리케이션 개발자는 가상 주소 공간만 사용하고 실제 메모리는 직접적으로 조작하지 않는다. 가비지 콜렉터는 관리되는 힙에서 사용자 대신 가상 메모리를 할당 및 해제한다.
네이티브 코드를 작성 중인 경우 Windows 함수를 사용하여 가상 주소 공간 작업을 한다. 이러한 함수는 네이티브 힙에서 사용자 대신 가상 메모리를 할당 및 해제한다. - 가상 메모리는 다음 세 가지 상태일 수 있다.
시스템 상태 | 설명 |
Free | 메모리 블록에 가상 메모리에 대한 참조가 없으며, 메모리 블록을 할당에 사용할 수 있다. |
Reserved | 메모리 블록을 사용자의 작업에 사용할 수 있으며, 다른 할당 요청에는 메모리 블록을 사용할 수 없다, 하지만 커밋되기 전까지는 메모리 블록에 데이터를 저장할 수 없다. |
Commited | 메모리 블록이 실제 스토리지에 할당되어 있다. |
- 가상 주소 공간은 조각화될 수 있다. 즉, 주소 공간에 구멍(Hole)이라고 부르는 빈 블록이 존재한다. 가상 메모리 할당이 요청된 경우 가상 메모리 관리자는 할당 요청을 만족시킬 수 있도록 충분히 큰 단일 빈 블록을 찾아야 한다.
2GB의 여유 공간이 있는 경우에도 전체 여유 공간이 한 주소 블록에 있는 경우가 아니면 2GB가 필요한 할당이 실패할 수 있다. - 예약할 가상 주소 공간이나 커밋할 실제 공간이 충분하지 않을 경우 메모리 부족이 발생할 수 있다.
가비지 수집 조건
가비지 수집은 다음 조건 중 하나가
충족될 경우 발생한다.
- 시스템의 실제 메모리가 부족할 경우.
이는 OS의 메모리 부족 알림 또는 호스트에서 표시되는 메모리 부족을 통해 감지된다. - 관리되는 힙의 할당된 개체에 사용되는 메모리가 허용되는 임계값을 초과한 경우.
이 임계값은 프로세스가 실행됨에 따라 계속 조정된다. - GC.Collect 메소드가 호출된 경우.
가비지 컬레터가 지속적으로 실행되므로 이 메소드를 호출해야 하는 경우는 거의 없다. 이 메소드는 주로 특이한 상황 및 테스트에 사용된다.
<이것이 C#이다.>