🧑🏻💻 1. Memory
메모리 : 실행된 애플리케이션이 상주하는곳
CPU : 명렬어 처리를 위한 하드웨어
1. Stack과 Heap에대해 설명해보시오
2. Stack과 Heap이 어디에서 저장되는가?
3. 어떤것이 Stack에 저장되고 어떤것이 Heap에 저장되는가?
4. Stack과 Heap 사이즈에 대해 설명해보세요
5. Stack과 Heap메모리의 Deallocate 방식에 대해 설명하세요
6. 원시 타입과 참조 타입 은 메모리 각각에 어디에 저장 되는가?
7. 원시 타입도 Heap에 저장될 수 있을까?
8. string은 Stack에 저장될까, Heap에 저장될까?
9. 값타입과 참조타입에 대해 설명해보세요
10. Call By Value & Call By Reference
11. 값 복사, 참조 복사에 대해 설명해보세요
12. 구조체와 클래스에 대해 설명해보세요
13. 구조체는 Stack에 저장될까 Heap에 저장 될까?
14. 구조체는 Heap에 생성 될 수 있나?
15. 그럼 전달에 있어서 값전달인가? 참조 전달인가?
16. 박싱과 언박싱을 해결하는 방법이 있을까요?
17. 박싱과 언박싱에 대해 설명하세요 그리고 그 성능에 대해 설명해보세요
14 가비지 컬렉터
📄 1. Stack과 Heap
1. Stack Heap
1 뭘 저장하나?
, 2 속도 (접근, 생성해제)?
, 3 데이터 접근법, 공유 범위?
, 4 최대 할당 사이즈는?
, 5 메모리 관리
비교 | Stack : 정적 메모리 | Heap : 동적 메모리 |
---|---|---|
① 저장 하는 것 | 함수내 지역변수, 매개변수, this 매개변수 참조 변수(주소값) 함수/메소드 호출 정보(Stack프레임) | 객체 그 자체 저장(객체 멤버 변수들도) (new) |
② 속도 (생성과 해제 & 데이터 접근) | 빠르다. | 느리다 |
③ 접근 방법과 공유 가능성 | 1. LIFO 순서 2. 함수내 블럭에서만 접근 가능 | 2. 무작위 접근 2. 프로그램간 공유가 가능하다. |
④ 최대 할당 사이즈 | 1. 컴파일 타임에 사이즈 제한 2. arr[]와 같은 문법 3. 약 1M 혹은 500M정도 작다. | 1. 고정 크기가 없지만 런타임에 결정 2. malloc 와 같은 문법 3. 제한이 거희 2GB도 가능하다. |
⑤ 메모리 관리 할당과 해제 | 자동 관리 블럭 내에서만 사용 가능 Stack 메모리는 빠르게 할당되고, 함수 종료시 자동으로 해제된다 | 1. UnMannaged 직접 관리 : 프로그래머가 직접 Heap 메모리 해제를 관리 2. Managed 자동 관리 : 가비지 컬렉터가 메모리의 참조를 검사하여 자동으로 관리 |
2. 그 외
- Data Memory : 프로그램이 종료될 때까지 지워지지 않을 데이터 저장
- Static Memory : 전역 변수, static, 상수
📄 2. Managed / Unmanaged
이 둘의 차이는 프로그래머가 하드웨어의 자원을 직접 제어할 수 있는지의 여부를 말한다.
① Unmanaged
-
C/C++ 대부분의 라이브러리가 Unmanaged다.
-
소멸자와 스마트 포인터 같은것을 (레퍼런스 타입의 메모리 주소) 사용해 메모리 Leakage를 관리 해줘야 한다.
-
C# 에서는 I/O FileStream, Database, Network같은 외부 자원들
② Managed
- C#, Java같은 언어가 Managed 언어다. 가비지 컬렉션으로 런타임이 알아서 메모리 관리를 해준다.
- C# 에서는 .NET 에서 생성한 모든 객체
1. C#의 Heap
2. C# 클래스 객체는 어떻게 Managed Heap에 표현되는가?
3. C# 파생클래스 객체의 메모리 표현에 관하여
📄 3. 값 타입 / 레퍼런스 타입
1. 값/참조타입
비교 | 값 타입 | 레퍼런스 타입 |
---|---|---|
① 해제 | Stack에서 해제 | Heap에서 가비지 컬렉터로 |
② 종류 | 구조체, 튜플, Span<T>, 정수, 부동소수점, Bool | 객체, Class, Object String, Array, Interface, Delegate, Exception |
③ 대입시 동작 | 할당, 함수 인수, 리턴시 복사됩니다. | "주소값 (레퍼런스 타입의 메모리 주소)"을 리턴 |
④ 상속 | 상속 받을 필요 없다 | 상속을 받는 기능 존재 |
⑤ 비교연산 | 모든 필드의 값들을 비교 struct나 다른 값 타입을 정의시 연산자 오버로딩으로 == & != 구현 | .Equal() : 놀랍게도 이것도 개체 멤버 내용물을 비교한다..ReferenceEqual() : 주소값 (레퍼런스 타입의 메모리 주소) 를 비교 |
**‼ 중요한점은 "주소값 (레퍼런스 타입의 메모리 주소)" 이녀석은 스택에 저장되지만 값타입은 아니다 ‼**
2. C#의 String
- string은 참조타입임에도 값 복사로 전달이 된다.
- 그런데 가비지 콜렉터에 의해 소멸 된다.
- Immutable
- = 연산자시, 내용이 바뀌는것이 아니라 새로운 문자열이 생긴다.
- == != 문자열 비교에서 일반적인 참조차입의 비교와는 다른 로직으로 구성되어있다.
- 일반적으로는 "주소값 (레퍼런스 타입의 메모리 주소)"을 비교하여 주소값이 다르면 다른것,이지만.
- heap 주소값이 다르더라도 내용이 완전히 같으면 같다고 판단한다.
📄 4. Struct / Class
1. 공통점
공통점 | Struct | Class |
---|---|---|
① filed, method | 가질 수 있음 | 가질 수 있음 |
2. 차이점
차이점 | Struct | Class |
---|---|---|
① 메모리 할당 방법 | 기본적으로 값 형식은 Stack에 할당되지만, 참조형식에 내포된 "인스턴스 값" 형식은 Heap에 들어감 | Heap |
② value & ref type | Value | Reference |
③ 멤버들의 기본 접근자 | Public | Private |
④ 매개변수 전달 방법 | Call By Value : 복사전달 Stack에 새로 복제본이 생김 | Call By Reference : 주소값 (레퍼런스 타입의 메모리 주소) |
⑤ 소멸자 사용 여부 | 사용 불가능 (함수 밖으로 나가면 해제) | Unmanaged : 사용 가능 Managed : C# 에서는 Disposable 인터페이스로 구현 |
⓺ 목적에 따른 코딩 스탠다드 룰 | 아무 함수 없이 순수하게 데이터 집합 | 멤버 변수 & 메소드 |
3. 구조체의 중괄호 초기화
EventReference { Path = path, Guid = GuidLookupDelegate(path) };
pair {first = f, secont = s};
📄 5. 인자 전달 방법
- A. 값을 값으로 전달하기
- 그냥 넣기
C#의 값타입 전달은 Call By Value 방식이다.
값타입을 인자로 전달하게 된다면 값의 DeepCopy로 복사하게 된다.
이러한 점때문에 큰 구조체를 인자로 넣게 된다면 값 복사에 따른 부하가 생길 수 있다. - B. 값을 참조로 전달하기
- ref로 넣기
위의 내용을 이어서 이야기하면 C#의 큰 구조체의 경우 값 복사에 따른 부하가 생길 수 있으므로 큰 구조체는 Ref로 전달해야 성능 이득이 있다. - C. 참조형식을 값으로 전달하기
- 인스턴스 주소의 복사본을 수신할 수 있다.
두 주소가 동일한 개체를 참조한다. - D. 참조를 참조로 전달하기
- 그냥 넣기
📄 6. 박싱 / 언박싱
런타임에 타입을 결정하는 기법
1). 박싱 / 언박싱의 문제점
① 박싱 : 값 타입 -> 레퍼런스 타입
으로 타입 캐스팅
- 함수 블럭 내 스택 메모리에 저장된 값 타입을 "힙에 저장"
- 박싱된 결과는 가비지 컬렉터의 대상이 된다, 따라서 자주 박싱할 경우 GC가 빈번히 발생
- Ex). ArrayList에 Struct대입시 박싱이 일어나 Struct는 값타입에서 참조타입으로 변해 Heap에 쌓이게 된다.
- 그 과정은 Stack에 저장된 값을 Heap으로 복사를 하고,
- 메모리의 크기는 값 타입 내에 들어있는필드들의 메모리 크기에 더하여
다른 Heap의 객체들처럼 타입 객체 주소값도 (레퍼런스 타입의 메모리 주소) 포함
② 언박싱 : 레퍼런스 타입 -> 값 타입
으로 타입 캐스팅
- 힙 메모리에 저장된 레퍼런스 주소의 내용을 스택 메모리에 복사
- 값 복사로 인한 연산량 증가
- 캐스팅을 통해 다음 과정이 진행된다
- 타입에 맞는 필드들의 주소를 가져온다. (언박싱)
- 주소에 있는 필드들의 값을 Stack의 값 타입 인스턴스 쪽으로 Heap에서 복사된다.
비용이 크기 때문에 가급적 사용하지 말아야 성능에 무리가 안간다.
박싱, 언박싱 과정을 통해서 Heap에 가비지가 쌓여 GC에 무리를 줄 수 있는 작업이기 때문이라
이를 해결하기 위해서는
"타입별 메소드 오버로드" 혹은 "제네릭"을 활용해 줘야 한다.
2). 제네릭을 통한 해결
① 장점
-
코드의 재사용성 :
- 어떤 타입이 들어오든 로직은 동일하게 처리 될 수 있다면, 클래스나 메소드의 일반화가 가능해진다.
- 이로써 "타입별 메소드 오버로드"를 통해 모든 타입에 대응해 함수를 계속 만들어야 하는 수고를 덜할 수 있다.
추상 대수학의 대수 체계와 유사한 부분이 있다. 어떤 집합 원소와, 연산에 대해 닫혀 있는 등. 공리 체계를 계속 유지되는 것처럼 예시로 `where T : struct` 같은 경우는 $(값 타입 집합, 연산)$ 과 같이 군(Group)개념을 구사할 수 있다. 물론.. 함수의 결과가 `void`인 경우는 뭐.. 집합에 대해 닫혀있다고는 말 못한다. *뉘앙스가 같다는거~*
-
런타임에 타입을 결정하는 책임을 컴파일러에게 위임 :
- 박싱과 언박싱은 런타임에서 타입을 정해주려고 발생하는 연산이다.
- 컴파일 타임에 타입을 사전에 명시된것을 처리하므로
"값타입의 제네릭"을 값타입으로 할당시 스택에 할당되어 메모리 할당이나 복사 비용이 필요 없다.
참조타입에서는 어쩔수 없이 힙에 저장되지만 이런 상황에서는 타입 안정성과 코드의 재사용성이라는 장점
-
타입 안정성을 제공한다. :
- 박싱과 언박싱이 일어나는 상황과 연결 짓자면.
object란 레퍼런스 타입을 사용하기 때문에 기존 타입과 다른 타입으로 캐스팅되어 발생하는 오류 (ClassCastException) 잠재적이다. Where T typeBound
을 사용한다면 값 타입인지, 참조타입인지, 클래스인지 구조체인지
계층적인 관계를 가지는 타입의 바운더리를 컴파일 타임에 정해줄 수 있어 오류 방지에 능하다.
- 박싱과 언박싱이 일어나는 상황과 연결 짓자면.
② 주의할점
- 제네릭은 기본적으로 실체화 불가 타입이다.
- 제네릭을 가지고 객체, 배열을 생성할 수 없으며 클래스 리터럴로 사용을 할 수 없다.
- 제네릭 클래스 또는 인터페이스 타입의 배열을 선언할 수 없음.
- 하지만 제네릭 타입의 배열 선언은 허용됩니다.
📄 7. 레퍼런스 타입 TO 레퍼런스 타입으로 Casting
1). object 캐스트로 레퍼런스 타입 전달
- '이미 힙에 저장된 레퍼런스 타입'을 '다른 레퍼런스 타입'으로 형변환(캐스팅) 한다고 해서
딱히 박싱 / 언박싱 같은 stack -> heap 혹은 heap -> stack 과정이 없다는것. - 참조타입 끼리 캐스팅은 가비지는 있지는 않음,
- 하지만! 예외처리 에러처리 과정에서 성능 감소가 있다.
이를 위해 As, Is
키워드 를 사용할 수 있다.
2). As, Is
을 통한 해결
-
as 연산자 :
레퍼런스 타입 -> 하양 레퍼런스 타입
- 상속관계의 클래스간 하양 캐스팅시 object 캐스팅 대신에 사용되는 더 안정적인 연산자
- 하양캐스팅수행 (만약 실패시 Null 반환)
-
is 연산자
- 캐스팅 성공 유무 확인을 결과가 bool으로 리턴된다.
- 조건문에서 타입 매칭으로 사용 하며, 값타입 참조타입 모두 사용 가능하다.
📄 8. Garbage Collector
Managed Programing Language가
Heap(동적 메모리)에 할당된 메모리를 *자동* 해제하는 방법
1). 가비지 : 쓰레기 객체
Counter c = new Counter();
c : Heap(동적 메모리)에 존재하는 Counter 객체를 가르키는 주소값 (레퍼런스 타입의 메모리 주소)
주소값는 (레퍼런스 타입의 메모리 주소) Stack(정적 메모리)에 존재한다.
new Counter() : Heap(동적 메모리)에 할당(점유)하고 있는 Counter 객체
이 겍체는 Heap(동적 메모리)에 존재한다.
c와 new Counter()은 서로 한몸인데... 만약 아래와 같이 주소값가 (레퍼런스 타입의 메모리 주소) NULL 주소값를 (레퍼런스 타입의 메모리 주소) 가르키게 된다면?
c = null; // 이때부터 new Counter()은 가비지에 처리 대상이 된다.
만들어 놨으면 Heap에 쌓이는데.. 정작 그 Heap을 참조하는 변수가 없어서 생기는 문제.
즉 메모리 공간이 할당되었지만 레퍼런스를 모두 잃었기 때문에, 더 이상 그 메모리 공간을 사용할 수 없는 상태가 된 것임.
2). 가비지 콜렉터
"객체를 가르키는 레퍼런스 변수를 잃어버려 사용되지 못하는 메모리" 를 찾아서 자동으로 해제(관리) 해주는 프로세스다.
-
① 작동 타이밍.
- Heap 부족 : 인스턴스 메모리 할당을 수용할 수 없을 정도로
Heap 메모리가 충분하지 않으면 가비지 컬렉터를 실행한다. - 메모리 단편화 : 단편화 현상이 빈번하여 실질적인 빈 공간은 있지만,
뜨문 뜨문 작디 작은 데이터만 들어갈 수 있는 공간이 산재해 있다면. GC가 자주 발생할 수 있다.- 이러한 이유로 Unity DOTS 가 주목받고 있다.
- 왜냐 하면 DOTS의 기능중 메모리 단편화가 발생하지 않는 아키텍쳐(데이터 지향 아키텍쳐)로 구현되어 있기 때문이다.
- Heap 부족 : 인스턴스 메모리 할당을 수용할 수 없을 정도로
-
② 알고리즘
- Mark and Sweep : CPU 자원을 요구하는 작업이다.
- Root(주소값 (레퍼런스 타입의 메모리 주소) 변수, 함수)로 부터 참조된 Heap의 모든 오브젝트를 순회하며
- Root으로 부터 순회 했던것은 "Mark" 즉, "Mark가 된것은 참조가 있다"는 의미다.
레퍼런스가 없는 오브젝트들은 "UnMarked" - Unity는 "UnMarked(레퍼런스가 없는)" 오브젝트를 "Sweep"을 통해 삭제하여 메모리를 확보합니다.
- 상호 참조
- 상호 참조중인 두 객체중 하나라도 참조가 된다면 Mark, 그렇지 않다면 Sweep을 통해 삭제
- Mark and Sweep : CPU 자원을 요구하는 작업이다.
3. 최적화
- ① 객체 풀링 : 메모리를 해제하는 것이 아닌 재활용을 한다 풀링된 자료구조
ListPool
,DictionaryPool
등등. - ② 올바른 스트링 연산 || StringBuilder 사용
- ③ 값 타입을 사용할 수 있으면 최대한 그렇게 하거라.
- ④ Stack Alloc Array
Span<T>
사용하기 이를 통해 스택할당된 배열을 사용할 수 있다.
4. 여담
- GetComponent 메서드는 에디터에서 실행될 때 항상 메모리를 할당하지만,
빌드된 프로젝트에서는 할당하지 않습니다.
참조
- Enum은 왜 가비지 컬렉트를 발생시킬까
- Span<T>
- C# Interview Questions : Stack Heap | Boxing Unboxing | Value & Reference Type
'Language > C#' 카테고리의 다른 글
| 니앙팽이 - C# | 2 | 참조 전달 [Ref & Out] (0) | 2024.01.18 |
---|---|
| 니앙팽이 - 이벤트(C#) | 5 | UnityEvent (0) | 2023.04.14 |
| 니앙팽이 - 이벤트(C#) | 4 | Eventhandler & Event & EventArgs (0) | 2023.02.09 |
| 니앙팽이 - 이벤트(C#) | 3 | 델리게이트 액션 (Action) (0) | 2023.02.03 |
| 니앙팽이 - 이벤트(C#) | 2 | 델리게이트(Delegate) (0) | 2023.02.03 |