본문 바로가기

유니티 개발 정보/개념

유니티 메모리 관리 - 2

원문이 약간 길어 3~4개로 나눠어서 올릴 예정입니다. 
해당 글 원문은 http://unitygems.com/memorymanagement/에서 보실수 있습니다.
번역중에 이상한 부분은 댓글 남겨주시면 확인 후에 수정하겠습니다.



참조형식과 힙


참조형식 변수는 참조형으로 메모리에 저장된 객체를 말한다. 새로운 클래스 인스턴스를 만들 때,
이들의 데이터 위치의 참조변수를 만들뿐만 아니라 데이터도 힙에 저장된다.
이 참조는 값형식으로 객체의 주소값을 가지는 변수이다.(참조변수) 클래스의 인스턴스를 만들기 위해서 new 키워드를 사용하는데, 이는 OS에게 객체의 타입에 맞는 메모리 공간과 객체를 초기화 하는데 필요한 코드를 수행하도록 요청한다. 밑에 Dog타입의 객체의 예시를 보자.

(역주: 위에서 말한 참조형식 변수는 Class, Object, String같은 클래스를 의미한다. 이는 힙에 저장되며
참조변수는 객체의 주소값을 의미하는데, 이는 스택에 저장된다. 
참조 형식 변수와 참조변수가 햇갈리지 않도록 주의할 것)



1
2
3
4
5
6
7
8
public class Dog{
     public string name { get; set;}
     public int age { get; set;}
     public Dog(string s, int n){
           name = s;
           age = n;
    }                         
}
01
02
03
04
05
06
07
08
09
10
11
public class Test : MonoBehaviour {
    Dog dog1 = new Dog("Rufus",10);
    Dog dog2 = new Dog("Brutus",8);
 
    void Update (){                                                 
        if(Input.GetKeyDown(KeyCode.Alpha1))
            print ("The dog1 is named "+dog1.name+" and is "+dog1.age);
        if(Input.GetKeyDown(KeyCode.Alpha2))
             print ("The dog2 is named "+dog2.name+" and is "+dog2.age);
    }
}

dog1과 dog2 변수는 메모리안에서 이들 객체와 연관된 데이터가 저장된 위치를 참조한다.



밑에 Instantiate 함수와 관련된 행동을 봐라. 
우리는 3D모델이 포함된 Enemy 프리팹과 약간의 컴포넌트와 스크립트를 가지고 있다고 생각하자.

01
02
03
04
05
06
07
08
09
10
11
using UnityEngine;
using System.Collections;
 
public class Test : MonoBehaviour{
    public GameObject enemyPrefab;
 
    void Update(){
        if(Input.GeyKeyDown(KeyCode.Space))
               Instantiate(enemyPrefab,new Vector3(0,0,0), Quaternion.identity);
    }
}

우리는 새로운 enemyPrefab의 인스턴스를 만들었고, 그것은 사용되고 있다. 우리는 새로 생성한 enemyPrefab의 인스턴스를 참조하지 않고 있기 때문에, 만약 인스턴스에 접근하기 원한다면 문제가 생길 것이다. 우리가 새롭게 생성한 객체에 어떤 행동을 적용하길 원한다면 우리는 그 객체를 참조하는 변수를 만들어야 한다.

1
2
3
4
5
6
7
void Update(){
    if(Input.GeyKeyDown(KeyCode.Space))
       GameObject enemyRef = Instantiate(enemyPrefab, new Vector3(0,0,0), Quaternion.identity);
        enemyRef.AddComponent(Rigidbody);
        Destroy(enemyRef);
     }
}

예를 들어, enemyRef는 선언되었고, 새로 생성된 객체의 주소값을 할당받았다. enemyRef는 이제 그 객체를 참조한다. 어떻게 우리가 Componet를 추가하는지 봐라. 이 변수는 객체의 위치를 알기때문에 객체의 public변수에 접근하거나 추가하는 것이 가능하다. 마지막 줄을 보면 객체가 파괴되는데 이런 방식으로 거의 코드를 짜지 않지만, 이것은 단지 예시를 위한 목적이다.

참조변수는 오직 if문{ } 안에서만 생존하는 automatic 변수이다.
그 밖에서는 참조변수는 더 이상 존재하지 않으며, enemy 객체를 다시 찾기위해서는
우리는 다음과 같은 방법을 사용할 수도 있다.
GameObject enemyRefAgain = GameObject.Find(“Enemy”);

참조형식 변수는 힙공간에 저장된다. 그들은 다음과 같다. 
(역주: 위에서도 말했지만 참조형식 변수의 주소값을 가지는 참조변수는 스택에 저장된다.)

  • class   
  • object 
  • string 
    (string이 변수처럼 보일지 몰라도, 사실은 Class String의 실제 객체이다.)
값형식 변수와 참조형식 변수의 주된 차이는 데이터가 저장된 곳을 가리키는 참조변수의 추가이다.
참조변수는 우리가 데이터를 찾기전에 첫번째로 접근해야한다.
이것은 클래스가 직접적으로 바로 접근할 수 있는 구조체보다 느리게 만든다.


다른 주요 문제는 다음과 같은 연산이다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEngine;
using System.Collections;
 
public class Memory : MonoBehaviour {
    DogC dogC1 = new DogC();
    DogC dogC2 = new DogC();
    int number1;
    int number2;
 
    void Update () {
        if(Input.GetKeyDown(KeyCode.Space)){
           number2=number1 = 10;
           number1 = 20;
           print (number1+" "+number2);
           dogC1.name = "Rufus";
           dogC1.age = 12;
           dogC2 = dogC1;
           dogC1.name = "Brutus";
           print (dogC1.name+" "+dogC1.age+" "+dogC2.name+" "+dogC2.age);
        }
    }
}

우리는 하나가 다른 하나를 수정하지 못한다는 것을 보았을 때 두개의 number변수는 독립적이라는 것을 알게 될 것이다.

반면에, 클래스에 다른 클래스를 대입할 때
하나의 수정은 다른 하나에 영향을 미친다.

이것은 dogC2 = dogC1를 할때 우리는 dogC1의 주소를 dogC2 참조 변수에 대입하기 때문이다.
메모리에서 dogC2의 원래 주소값을 잃어 버리고
dogC1또는 dogC2를 바꾸는 것은 같다.
우리는 하나의 지역을 가리키는 2개의 참조변수를 가지고 있다.


수명과 범위 그리고 가비지 컬렉션

참조형식 변수인 경우에, 수명을 가비지 컬렉터(GC)가 담당하기 때문에 불분명하다.
C와 C++의 옛시절로 돌아가보면, 동적으로 할당된 변수가 메모리로부터 제거되는 것은 프로그래머의 의무였다. 
(C에서는 Free(), C++에서는 ~(파괴자)에서 담당한)


C#에서, 가비지 컬렉터는 변수가 아직 사용되고 있는지, 그것(실행코드안에 있는)을 참조하고 있는 것이 있는지 없는지 확인할 것이다. 그리고는 자유롭게 사용가능한 메모리 지역을 표시할 것이다.
COM인터페이스 또는 다른 레퍼런스 카운팅 시스템에 익숙한 프로그래머는 종종 가비지 컬렉션의 이상한 개념을 발견한다. 왜냐하면 레퍼런스 카운팅 시스템에서 서로를 참조하는 두 객체를 가지는 것은
(부모가 자식을 참조하고, 자식이 부모를 참조하는것 같은) 메모리가 절대 해제되지 않을 수 있기 때문이다. 가비지 컬렉터는 이러한 제약을 가지지 않는다.


가비지 컬렉터는 큰 주제이고, 유니티는 콜렉션 프로세스의 수행에 관한 많은 특이점을 가지고 있다. - 만약 당신이 전통적인 C#에 익숙하다면 기본적으로 GC는 짧은 수명을 가지는 오브젝트에 대해 처리비용이 싸야된다고 예상하겠지만, 유니티에서는 그렇지 않다.

메모리 블럭이 가득찰 때마다, 시스템은 힙에 할당된 모든 객체에 대한 도달가능성(객체에 대한 참조가 있는지 없는지)을 확인해야 한다. 이것은 그들이 다른 코드로 부터 접근되고 있는지 혹은 실행되고 있는지를 확인하는 것을 의미한다. 만약 그들이 실행되고 있거나 접근되어지고 있다면 메모리에 유지될 것이며, 그렇지 않으면 해제가 될 것이다.
일반적인 .Net 가비지 컬렉션은 제너레이션(세대)안에서 발생한다. 이는 도달가능성 테스트와 관련된 처리를 최소화하도록 하는데, 새롭게 만들어진 객체(1세대)를 먼저 검사하고, 그래도 여전히 메모리가 부족하다면 오래된 객체(2세대, 3세대)를 검사한다.
유니티는 항상 모든 객체를 검사하고 그래서 가비지 컬렉션은 주된 과부하 요소이며,
일반적으로 .NET 언어에서 예상되는 것보다 훨씬 크다.

접근성 테스트는 가비지 컬렉션 뒤에서 작용하는 마법같은 것이다. - 
이는 컬렉터가 현재 어떤 코드가 실행되고 있는지 혹은 실행될 것인지를 알아내도록 한다. 
왜냐하면 컬렉션의 후보가 되는 객체를 참조하는, 라이브 참조변수가 있을 수도 있기 때문이다.
이는 함수안에서 클로저의 사용을 포함한다. - 그것은 매우 복잡하고, 매우 강력하며, 그러나 게임을 빠르게 하는 방법은 아니다.
클로저는 지역 변수에 대한 참조 또는 함수 내부 안에 정의된 익명의 함수안의 파라미터에 의해 만들어 진다. 익명의 함수가 호출될 때, 그 루틴이 실행했을 때와 동일하게 하기 위해서 변수의 값이 유지된다 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
List<Action> actionsToPerformLater = new List<Action>();
 
void DisplayAMessage(string message)
{
      var t = System.DateTime.Now;
      actionsToPerformLater.Add(()=>{
          Debug.Log(message + " @ " + t.ToString());
      });
}
 
void SomeOtherFunction()
{
      DisplayAMessage("Hello");
      DisplayAMessage("World");
      SomeFunctionThatTakesAWhile();
      DisplayAMessage("From Unity Gems");
 
      foreach(var a in actionsToPerformLater)
      {
           a();
      }
 
}

코드내부에서 DisplayAMessage 함수를 호출할 때 마다, 전달 받은 message와 현재 시스템 타임(t)에 대한 클로저를 생성한다. 우리는 끝에 있는 foreach 루프를 실행할 때마다.
디버그 메시지는 전에 리스트에 추가하기 위해 함수를 호출했을 때 전달된 파라미터와 함께 기록될 것이다.
클로저는 매우 강력한 도구이며 그것은 다음 글에 대한 주제가 될 것이다.

가비지 컬렉션은 시스템이 연산을 위해 메모리가 충분치 않다고 판단될 때 일어난다. 이는 버려진 객체가 상당한 기간 동안 수거되지 않을 수도 있다는 것을 의미한다. - 그러므로 객체가 더 이상 필요하지 않을 때 명시적으로 외부의 연결을 끊는 것 또는 외부 리소스를 해제하는 것은 흔히 있는 일이다.
(역주: 언제 가비지 컬렉션이 발동될지 모르기 때문에 수동으로 메모리를 해제한다는 의미이다.)

 명시적으로 내부의 객체(당신의 프로젝트에 있는)를 해제할 필요는 없다. 그러나 스트림(파일)과 시스템에 영향을 주는 데이터베이스 연결을 가지는 프로젝트의 외부에서 사용할 경우에는 필요하다.
예를 들어 파일을 닫지 않는 것은 다음에 파일에 접근할 때 실패하게 만든다.
파일은 여전히 버려진 객체에 의해 열려있기 때문이다.
가비지 컬렉션은 매우 비싼 연산이며, 게임에 영향이 없을 때 - 레벨을 로드할 때, 플레이어가 일시정지 메뉴로 들어갔을 때,  플레이어가 알지 못하는 많은 경우 - 수동적인 가비지 컬렉션의 가동은 적절하다. 
System.GC.Collect(); 명령어를 통해서 수동적으로 가비지 컬렉션을 작동시킬 수 있다.

참조타입 변수에 관해서, GameObject.Find()를 사용하는 것은 어떤 스크립트에서든 객체에 대한 참조를 리턴한다. 이처럼 객체를 찾기 위한 적절한 행동을 한다면, 프로그램안에서 어디서든 접근이 가능하다. 
게임 플레이 동안 클래스의 인스턴스를 찾는 방법에 대한 좀 더 자세한 설명은 GetComponent 튜토리얼에 나와있다.


'유니티 개발 정보 > 개념' 카테고리의 다른 글

코루틴(Coroutine)++  (3) 2013.10.08
유니티 메모리 관리 - 4(마지막)  (0) 2013.10.03
유니티 메모리 관리 - 3  (1) 2013.08.13
유니티 메모리 관리 - 1  (0) 2013.08.07
유니티 코딩 규칙  (4) 2013.07.22