메모리 관리(Memory Management)

황승우·2023년 7월 23일
0

1. 메모리 관리(Memory Management)

  • 모든 프로그램은 메모리에 적재가 되어야 실행이 가능하다. 그렇기 때문에 한정된 메모리를 여러 프로세스가 함께 효율적으로 사용하기위해 메모리 관리 방법이 필요하다.
  • 메모리 관리는 기본적으로 OS에서 기능을 제공하며
    1. 프로그래밍 과정 중 메모리 접근 제공

    2. 효율적인 메모리 관리

    3. 메모리 보호

      에 대한 기능을 제공한다.

  • 즉, 메모리를 할당하고 제거하며 보호하는 활동을 메모리 관리라고 한다.

OS Level에서의 메모리 관리는 어떻게 이루어지나?

  • 메모리 구조

    논리적 주소(=가상주소)

    • 코드가 저장된 공간과 프로그램에서 사용하는 자료구조.

      물리적 주소

    • 적재하는 실제 주소. 메모리 칩이나 디스크 공간.

      메모리 관리 장치(MMU)

    • 논리적 주소 -> 물리적 주소 변환을 담당. 즉, 바인딩을 담당.

    • 변환 방법으론 고정 분할, 동적 분할, 페이징, 세그멘테이션 등이 있다.

    • 바인딩이란?

      바인딩

      • 논리적 주소 -> 물리적 주소를 매핑시켜주는 작업.

      • 언제 변환하냐에 따라 바인딩 종류가 나뉨.

        > 링커(linker)
        > 
        > 
        > 컴파일러가 원시 코드를 파일로 생성하면 이 파일에 라이브러리와 다른 파일들을 결합.
        > 
        > exe 같은 파일이 링커를 통해 생성.
        > 
        > **로더(loader)**
        > 
        > 지정 위치에서 시작하여 메모리에 프로그램을 배치. 흔히 얘기하는 로딩을 해주는 역할.
        > 

        컴파일 시간에 바인딩(Compile time binding)

      • 메모리에 적재될 위치를 컴파일러가 알 수 있는 경우

        • int a;를 선언했을 때 a라는 변수의 메모리 주소는 100으로 설정할 때
        • 코드 상에서 메모리 주소를 설정했기때문에 위치가 변하지 않음
      • 프로그램 전체가 메모리에 올라가야함

        적재 시간에 바인딩(Load time binding)

      • 메모리 적재 위치를 컴파일 시점에 모르면 대체 가능한 상대 주소를 생성

        • 프로그램은 주소가 0번부터 시작된다고 가정한다.
        • 0번을 기준으로 코드 상 주소를 0+1000+200으로 상대 주소를 만든다
      • 적재 시점(Load time)에 시작 주소를 반영하여 코드 상 상대 주소를 다시 재설정한다.

      • 프로그램 전체가 메모리에 올라가야함

        실행 시간에 바인딩(Run time binding)

      • 실행 시간(Running)까지 주소 바인딩(할당) 함

        • 프로세스가 Running -> ready 상태가 되어 다시 Running을 할 경우 메모리 주소가 바뀜
        • 매번 실행될 때마다 주소가 바뀐다
      • 하드웨어(MMU: Memory Management Unit)의 도움이 필요

      • 대부분 OS가 사용

  • 스와핑
    • 프로세스 할당이 끝나면 보조 기억장치로 프로세스를 보내고(스왑 아웃), 새로운 프로세스를 메모리에 적재하는 것(스왑 인).
    • 디스크에 저장된 프로세스를 메모리로 올리고 메모리에 적재된 프로세스는 준비 큐에 대기시키는 중기 스케줄링에 해당된다.
    • 항상 우선순위가 높은 프로세스 공간을 만들 수 있어 시스템에 유연성을 높임.

메모리 할당 방법

  • 연속 메모리 할당

    • Fixed Partitioning

      • 메모리 공간의 크기를 정한 후 메모리가 할당이 필요할 때마다 분할된 정해진 영역으로 할당해주는 방법
      • 물리적 주소 = 기준 레지스터(PBR) + 논리적 주소
      • 내부 단편화(Internal Fragmentation)의 문제가 생김
        • 프로세스가 실제 사용해야할 메모리보다 더 큰 메모리를 할당받아 메모리가 낭비되는 현상
    • Dynamic Partitioning

      • 필요한 만큼 메모리를 할당하는 것.

      • 초기에는 전체가 하나의 영역

      • 프로세스를 어디에 배치해야하는가에 대한 방법론 필요

        • 최초적합 방법(first-fit)
          • 충분히 큰 첫번째 공간에 할당.
          • 공간 활용률이 떨어짐.
          • 오버헤드가 낮음
        • 최적적합 방법(best-fit)
          • 프로세스가 들어갈 수 있는 공간 중 가장 작은 공간을 할당
          • 크기 순으로 정렬되어 있지 않으면 전체를 검색해야함.(오버헤드가 큼)
          • 공간 활용률은 높으나 할당하기 위해 많은 시간이 소요
          • 작은 크기의 파티션이 많이 발생함
        • 최악적합 방법(worst-fit)
          • 가장 큰 사용 가능 공간에 할당.
          • 최적적합과 마찬가지로 정렬되어 있지 않으면 전체를 검색해야함
          • 작은 크기의 파티션의 발생을 줄일 수 있음
          • 큰 크기의 파티션을 확보하기 힘듦
        • 순차 최초적합 방법(Next-fit)
          • 최초 적합 전략과 유사
          • 마지막으로 탐색한 위치부터 탐색
          • 메모리 영역의 사용빈도 균등화
          • 낮은 오버헤드
      • 외부 단편화(External Fragmentation)의 문제가 생김

        • 가변 분할 방식에서 메모리에 프로세스가 적재되고 제거되는 일이 반복되면서, 여유 공간이 충분함도 불구하고 이러한 여유 공간들이 조각으로 흩어져 있어(Scattered Holes) 메모리에 프로세스를 적재하지 못해 메모리가 낭비되는 현상

        • 해결 방법

          • 메모리 통합 방법(Coalescing holes)
            • 프로세스가 메모리 사용이 끝나고 나가면 수행
            • 낮은 오버헤드
          • 메모리 압축 방법
            • 모든 빈 공간을 하나로 통합
            • 메모리 압축은 주소변환(논리주소->물리주소)이 정적이고, 컴파일이나 적재할 때 바인딩이 실행되면 물리주소가 고정되기 때문에 압축을 수행할 수 없음.
              • 압축하는 동안 시스템은 모든 일을 중지해야함.(실시간, 대화형 시스템에 부적합)
              • 압축 작업을 자주해야해서 자원소모가 큼.
      • Buddy System

        • 내부 단편화와 외부 단편화를 해결하기 위한 방법

          • 메모리 사이즈를 2^{L} 부터 2^{U}까지 할당.
            • 2^{L} = 할당 가능한 가장 작은 메모리 사이즈
            • 2^{U} = 할당 가능한 가장 큰 메모리 사이즈(최대 메모리 = 일반적으로 모든 메모리)
          • 메모리 할당과 해제를 반복하며 인접메모리를 합치고(2^{N}으로 변환 가능 시) 계속 할당해주는 방식.
        • 현재는 virtual memory에 바탕을 둔 페이징과 세그멘테이션 방식때문에 자주 쓰이는 방식은 아님.

  • 비연속(분산) 메모리 할당

    • 메모리 공간이 연속적으로 할당되어야 한다는 제약조건을 없애는 메모리 관리 전략

    • 분산 메모리에서는 프로세스가 메모리에 분산되어 올라가기 때문에 가상주소를 실제 메모리 주소로 매핑을 해야한다.

    • 가상주소(Virtual address) = 논리주소(Logical address)연속된 메모리 할당을 한다고 가정한 주소.

    • 실제주소(Real address) = 절대주소실제 메모리에 적재된 주소.

    • Fixed Partitioning : Paging

      • 논리 메모리는 고정크기의 페이지, 물리 메모리는 고정크기의 프레임 블록으로 나누어 관리
      • 프로세스가 사용하는 공간을 논리 메모리에서 여러 개의 페이지로 나누어 관리하고, 개별 페이지는 순서에 상관없이 물리 메모리에 있는 프레임에 매핑되어 저장
      • MMU(Memory Management Unit)의 재배치 레지스터 방식을 활용해 CPU가 마치 프로세스가 연속된 메모리에 할당된 것처럼 인식.
      • 내부단편화가 발생.(페이지 크기가 정해져 있기 때문에 하나의 페이지 크기를 사용하지 못하는 경우)
      • 두 번의 메모리 접근 때문에 오버헤드가 발생(page table에 접근 + 실제 physical address에 접근)
        • TLB  ( Translation Look-aside Buffer )
    • Dynamic Partitioning : Segmentation

      • 논리 메모리와 물리 메모리를 같은 크기의 블록이 아닌, 서로 다른 크기의 논리적 단위인 세그먼트로 분할
      • 프로그램을 논리적 블럭으로 분할(stack, heap, main code 등)하고 이 블럭이 세그먼트(segment)

      • 외부 단편화가 발생

2. 각 언어별 메모리관리 동작원리

  1. 개발자가 직접 관리

    • Ex) C / C++
    // 포인터 변수 a에 SomeClass의 인스턴스를 할당
    SomeClass* a = new SomeClass();
    // a라는 변수의 메모리를 해제
    delete a;
    **// 포인터 변수 a에 int 크기 만큼의 메모리를 할당
    int* a = (int*)malloc(sizeof(int));
    // a라는 변수의 메모리를 해제
    free(a);**
    • 메모리 해제를 까먹는 휴먼 에러가 발생.
    • 프로그램에 직접적인 에러 발생
  2. GC(Garbage Collection)의 등장

    • Ex) Java, Javascript, Python

    • 목적은 프로그램에서 더는 사용하지 않는 메모리를 삭제

    • 삭제한 뒤에는 메모리를 압축하여 메모리 파편화를 방지.

    • 방식

      1. 개발자가 직접 호출
        • C# 등의 언어에서는 개발자가 필요하면 GC를 직접 호출할 수 있도록 하고 있다.
        • 고급 개발자들만 보통 사용하며, 일반적으로는 직접 호출하지는 않는다.
      2. 프로그램이 호출
        • 가장 일반적인 방식이다.
        • 언어의 런타임 등에서 힙 메모리의 소진을 확인하면 호출한다.
    • GC는 보통 동작할 때 모든 스레드를 멈춘뒤 작업하는데, 이를 보고 Stop The World라고 부름.

    • 방법

      • 1. Reference Counting

        • 레퍼런스 카운팅은 python, swift 등에서 현재도 사용하고 있는 방식
        • 객체가 사용될 때 카운팅 변숫값을 1 올리고, 객체를 사용하지 않을 때 값을 1 내린다.
        • 이후 카운트 값이 0이 되면 내부 로직에서 delete 명령어를 호출해 자동으로 메모리에서 해제

        • 순환참조의 발생 때문에 일반적으로는 레퍼런스 카운팅을 활용해 관리하되, 힙 메모리가 가득 차는 순간 GC를 돌려 발생한 메모리 누수를 잡는다.
        • 장점
          1. 구현이 쉽다.
          2. 성능상으로 GC보다 빠르고 효율적이다.
          3. 힙 메모리가 소진될 때 동작하는 GC와 달리 카운트가 0이 되면 즉시 삭제하기 때문에 메모리를 즉시 재사용할 수 있다.
      • 2. Mark And Sweep

        • javascript에서 사용하고 있는 방식

        • 사용하지 않는 메모리를 표시(Mark)하고 삭제(Sweep)한다.

        • 보통, GC Root라는 객체를 두고 이 객체에 연결된 메모리를 재귀적으로 탐색하며 사용여부를 판단한다.

        • GC Root는 스레드, 함수, 변수 등 코드상에 존재하는 다양한 객체들이다.

          1. GC Root에서 연결된 노드들을 재귀적으로 탐색한다.
          2. 탐색 된 메모리를 표시(Mark)한다.
          3. 이후 표시되지 않은 메모리를 해제한다.
      • 3. Scavenge

        • 해제와 동시에 메모리 압축이 가능한 방법이다.

        • 메모리 공간을 크게 두 공간으로 잡아, 한 공간에만 할당을 하고 이후 꽉 찼을 시 메모리를 옮기며 남아있는 메모리를 해제하는 방식

        • 메모리의 추적 방식은 최상위 스택 포인터가 존재하는데, 이 스택 포인터와 연결된 메모리만 Reachable(사용한다)하다고 판단한다.

          1. 메모리가 꽉 찰 경우, Reachable한 메모리를 현재 공간(From Space)에서 다른 공간(To Space)으로 메모리를 이동시킨다.
          2. 현재 공간 (From Space)에 남아있는 메모리를 모두 해제한다.
          3. 이후 두 공간의 역할을 바꿔 다시 실행한다.
      • 4. Generational

        • Generational GC는 아래 두 가설을 참으로 가정하고 탄생한 알고리즘
          1. 대부분의 객체는 보통 금방 GC의 대상이 된다.
          2. 오래된 객체에서 젊은 객체로의 참조는 매우 적게 발생한다

        1. 객체가 처음 생성되면 1세대 메모리 공간에 할당한다.
        2. 힙 메모리가 꽉 찼을시, GC를 호출하고 살아남은 객체들을 2세대로 보낸다.
        3. 2세대에서는 age threshold를 이용해 특정 조건 이후 GC를 호출하고 이때 살아남은 객체를 3세대로 보낸다.
        • 이때 2 -> 3세대로 옮기는 객체들은 WriteWall이라 부르는 곳에 메모리 주소를 기록한다.
        • 젊은 객체(1, 2세대)가 오래된 객체(3세대)를 참조할 시 이 WriteWall에 적힌 메모리 주소를 보고 참조한다.

3. Rust의 차별점

  • Rust는 위와 달리 메모리 관리를 프로그래머도, 프로그램(GC)도 아닌 컴파일러가 관리
  • 대신 프로그래머는 컴파일러가 정확히 판단할 수 있게 엄격한 규칙에 따라 변수를 사용해야함.
  • **Resource Acquisition is Initialization (RAII)**의 패턴으로 생성.
    • 코프를 벗어날 때 메모리를 해제시켜주자는 패턴(기존의 C++)
  • 기존에 있던 어떠한 개념과도 차별되는 Ownership
    • Ownership에 3가지 규칙
      - Rust의 모든 값(value)은 owner 라 불리는 변수들을 갖고있다.
      - 하나의 값은 하나의 owner 만 가질 수 있다.
      - owner 가 scope 밖으로 나가게 되면 그 값도 사라진다.

      scope 예시

      {                       // s는 아직 유효하지 않다. 호출시 Error
          let s = "hello";    // 여기서부터 s가 유효하다.
                              // 여기서도 s는 유효하다.
      }                       // 이제 s는 유효하지 않다. 호출시 Error
  • Ownership의 Move
let s1 = String::from("hello");
let s2 = s1;println!("{}, world!", s1);

println!("{}, world!", s1);
error[E0382]: use of moved value: 's1'

  • 메모리 해제 시 둘다 메모리 해제를 실행하려는 경우 메모리 중복해제 에러가 생길 수 있음.
  • Rust는 이러한 잠정적인 에러를 막기위해 다른 변수에게 메모리가 복사된 변수를 부르는 것을 허용하지 않는다.
  • Rust에서는 위와 같은 과정을 Swallow copy가 아니라 move라고 표현.
  • 참고로 Rust는 프로그래머가 명시하지 않는 한 절대 deep copy를 하지 않는다. 때문에 자동복사가 일어나는 곳에서도 런타임 퍼포먼스에 영향을 받을 걱정은 하지 않아도 된다.

함수의 Ownership


fn main() {
    let s = String::from("HELLO"); // 변수 s가 문자열의 owner가 됨
    print_data(s);   // 변수 s가 파라미터로 전달되면서 소유권이 move됨.
    // 여기서부터는 변수 s 사용 못함.
}
 
fn print_data(data: String) {  // 파라미터 data가 문자열의 새 owner가 됨
    println!("{}", data);      // 문자열 사용
}                              // 여기서 문자열 메모리 drop 함

--------------------------------------------------------------------------------
fn main() {
    let mydata = get_data();   // 함수 리턴값을 받으면서 소유권 받음
    println!("{}", mydata);
}
 
fn get_data() -> String {
    let s: String = "Data".to_owned(); // 변수 s가 owner
    s                                  // 리턴하면서 소유권 이전
}
--------------------------------------------------------------------------------

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    let (s2, len) = calculate_length(s2);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String
    (s, length)
}
  • Rust는 Ownership을 기반으로 메모리를 관리한다.
  • 오너십을 가진 변수 scope에서 빠져나갈 경우 Drop 함수가 자동으로 호출된다. 이 때 메모리 해제가 이루어진다.
  • Ownership을 전달하지 않은 채로 그 변수의 바인딩이 해지되면 그 객체는 소멸.
  • 하나의 값(메모리공간)의 Ownership은 하나의 변수만이 가질 수 있으므로 중복해제 에러가 일어나지 않는다.
  • Ownership은 String 타입과 같이 할당 받을 메모리의 크기가 정해져 있지 않은 타입들에 대해서만 적용된다.
  • 함수 단위에서도 Ownership은 같은 방식으로 동작한다.

추가적인 기능

  • 이 뿐 아니라 Rust는
    • Null 포인터를 허용하지 않으므로 Null 포인터 참조 해제 버그 방지
    • 필요할 때 자동으로 크기를 조정하여 Buffer Overflow를 방지하는 벡터 및 문자열과 같은 데이터 구조가 포함.(기본 라이브러리)
      • Buffer Overflow = 지정한 메모리 크기보다 큰 변수가 입력되어 복귀주소를 알 수 없을 경우 프로그램이 비정상적으로 종료하는 현상
    • 등의 메모리 관리 측면에서 안정성을 제공한다.

참고

https://codingcoding.tistory.com/211

https://technote-mezza.tistory.com/92

https://techblog-history-younghunjo1.tistory.com/511

https://rebro.kr/178

https://www.crocus.co.kr/1376

https://velog.io/@jisoolee11/메모리-관리

https://velog.io/@sawol/메모리-관리

https://daeun28.github.io/컴퓨터공학-스터디/post18/

https://deepu.tech/memory-management-in-programming/

https://kimchunsick.me/2022-01-21-memory-management-method/

https://boiler.buzzni.com/2017/02/27/ios-reference-count.html

https://inpa.tistory.com/entry/JAVA-☕-가비지-컬렉션GC-동작-원리-알고리즘-💯-총정리

https://www.linkedin.com/pulse/rust-memory-safety-how-prevents-common-memory-related-prabhat-/

https://open-support.tistory.com/entry/Rust-에서-메모리-관리가-장점인-이유의-예제-소스

https://medium.com/@kwoncharles/rust-러스트의-꽃-ownership-파헤치기-2f9c6b744c38

https://plas.tistory.com/133

http://rust-lang.xyz/rust/advanced

profile
백엔드 개발자

1개의 댓글

comment-user-thumbnail
2023년 7월 23일

좋은 정보 감사합니다

답글 달기