프로그램 메모리 관리 방식
1) GC를 이용하는 언어들 - Java, Python, Go, JavaScript
- Java, Python, Go, JavaScript 에서 사용됨.
- compile time 이 아닌 실행 시간 중(Runtime)에 때때로
더 이상 쓰지 않는 메모리를 GC 가 알아서 정리해 준다.
- 개발하기 대단히 편리함.
- 운영할 때 성능상 문제가 되기도 함.
2) 수동으로 프로그래머가 메모리를 관리하는 언어들 - C / C++
- 개발자가 메모리 할당과 해제 작업을 지시한다.
- 최적의 속도. 실행 시 메모리 관리 부담 최소
- 개발하기가 GC를 사용하는 언어에 비해 상대적으로 어려움, 실수 및 버그가 많이 생길 수 있음.
3) 컴파일 시점에 메모리 관리 규칙을 검사 - RUST
- 소유권 규칙에 따라 컴파일 시점에 메모리 할당/해제 관리.
- 규칙에 어긋나면 컴파일되지 않음.
- 규칙을 잘 지키면 컴파일러가 잘 처리해 주는 셈
- 실행시간에 손해가 없으면서도, 실수할 여지가 없음
- 규칙 지키지 않으면 자동으로 컴파일 오류가 발생함.
소유권 (Ownership)
소유권의 개념
Rust 프로그래밍 언어에서 소유권(Ownership)은 메모리 관리를 위한 핵심 개념 중 하나이다.
이 개념은 Rust의 안전성과 효율성을 높이는 데 중요한 역할을 한다.
소유권의 규칙
- 각각의 값은 해당 값을 소유하는 변수(owner)를 가진다.
- 변수가 범위(scope)를 벗어나면, 그 값은 버려진다(drop).
- 한 번에 하나의 변수만이 값의 소유권을 가질 수 있다.
즉 위와 같이 특정 value에 대해서 하나의 소유자만 존재할 수 있다는 뜻이다.
아래와 같은 형태는 존재할 수 없다.
RUST에서 STACK & HEAP memory
Stack memory
- 스택 메모리는 데이터를 저장하고 검색하는 데 매우 빠르다.
이는 스택이 LIFO(Last In, First Out) 구조로 되어 있기 때문이다.
즉, 가장 최근에 저장된 데이터가 가장 먼저 추출된다.
- 스택에 저장되는 데이터의 크기와 수명은 컴파일 시간에 대부분 결정된다.
따라서, 스택 메모리는 정적인 메모리 할당을 사용한다.
- 함수 호출 시 생성되는 지역 변수들은 스택에 저장되며, 함수가 반환될 때 자동으로 해제된다.
- 스택은 상대적으로 작은 메모리 공간을 가지며, 고정된 크기를 가진다.
스택 오버플로우는 이 공간을 초과할 때 발생.
Heap memory
- Heap memory는 Stack memory에 비해 더 유동적이다.
런타임에 크기가 결정되는 데이터(예: 동적 배열, String 타입)는 힙에 저장된다.
- Heap은 Stack에 비해 더 큰 메모리 할당을 허용하며, 크기가 동적으로 조절된다.
- Heap에 저장된 데이터는 프로그래머가 직접 관리해야 한다.
Rust에서는 소유권(ownership)과 대여(borrowing) 시스템을 통해 메모리 안전성을 보장한다.
- Heap 메모리의 할당과 해제는 Stack에 비해 더 많은 시간과 자원을 필요로 한다.
각 메모리에 저장되는 데이터 타입의 종류
1) Stack 메모리에 저장되는 타입들
- 스택 메모리에 저장되는 타입들은 대체로 크기가 고정되어 있고,
컴파일 시간에 그 크기가 알려진 타입들이다.
기본 데이터 타입들
- 정수형(Integers) 타입들: i32, u32, i64, u64 등
- 부동 소수점 타입: f32, f64
- 불리언 타입: bool
- 문자 타입: char
고정 크기의 복합 타입들
- 배열: 예를 들어, [i32; 5] (5개의 i32 타입을 가진 배열)
- 튜플: 예를 들어, (i32, f64) (고정된 개수와 타입을 가진 튜플)
- 구조체와 열거형: 모든 필드가 스택에 저장될 수 있는 타입일 때
예를 들어 아래와 같이 Stack에 저장되는 데이터들은 자연스럽게 "값복사"가 발생한다.
2) 힙 메모리에 저장되는 타입들
힙 메모리에 저장되는 타입들은 런타임에 크기가 결정되거나 크기가 크고 변할 수 있는 타입들이다.
동적 컬렉션들
- 문자열: String (동적으로 크기가 변할 수 있는 문자열)
- 벡터: Vec <T> (동적으로 크기가 변할 수 있는 배열)
박싱 된 타입들
- Box <T>: 힙에 저장되는 단일 값을 포인팅 하는 스마트 포인터
- 기타 스마트 포인터 타입들: Rc <T>, Arc <T>, RefCell <T> 등
특정 복합 타입들
- 구조체와 열거형: 하나 이상의 필드가 힙에 저장되어야 하는 타입일 때
(예: String, Vec <T>를 필드로 가지는 경우)
하지만, stack 메모리에 저장되는 데이터들과 다르게, Heap에 저장되는 데이터들은 값복사가 되지 않는다.
이유는 바로 RUST 규칙 중에 하나인 "한 번에 하나의 변수만이 값의 소유권을 가질 수 있다."라는 조건 때문이다.
만약에 s2 에만 접근한다면, 컴파일은 문제없이 성공하게 된다.
이미 s1에 데이터였던 String 데이터의 소유권이 s2에게 넘어갔기 때문이다.
즉, 위의 상황을 도식화하면 아래의 그림과 같다.
처음에는 Stack에 존재하는 포인터변수가 "hello" 데이터에 대한 소유권을 가지고 있다.
그러다가 s2 변수가 선언되고 해당 String 데이터에 대한 소유권을 가져간다.
그러면서 자연스럽게 s1의 소유권은 소멸된다.
물론, 아래와 같이 clone() 함수를 사용하면 복사가 가능하다.
하지만 메모리 관점에서는 아예 다른 데이터를 생성시킨다고 볼 수 있다.
위의 그림과 같이 Heap 영역에 아예 새로운 영역을 할당받아서
s2가 해당 데이터를 소유하게끔 하는 방식이다.
'RUST' 카테고리의 다른 글
[RUST] Error (0) | 2024.01.03 |
---|---|
[RUST] ENUM (0) | 2024.01.02 |
[RUST] 구조체(structure) (0) | 2023.12.29 |
[RUST] RUST 메모리 관리 규칙 - Slice (0) | 2023.12.28 |
[RUST] RUST 메모리 관리 규칙 - 임대(lease) (0) | 2023.12.27 |