Memory leak(메모리 누수)

Janice._.oooh·2021년 11월 24일
1

Android/Java

목록 보기
2/8
post-thumbnail

Memory leak in JAVA


메모리 누수란,
더이상 필요하지 않은 리소스가 RAM에서 해제되지 않고 계속 점유 하고있는 것을 말합니다.

JAVA에서 메모리 누수가 발생하면, 더이상 사용하지 않는 객체가 Garbage Collection(GC)에 의해 회수되지 않고 애플리케이션에 할당된 메모리에 계속 누적되어 Crash 또는 OMM(Out Of Memory)가 발생합니다.



Garbage Collection


그럼 Gargabe Collection(Garbage Collector, GC)은 무엇일까요?
GC는 JVM(Java Virtual Machine)환경에서 메모리 관리를 위한 방식입니다.

C언어의 경우 코드를 이용하여 직접 메모리를 할당하고 해제시켜야하지만,
JAVA/Kotlin은 개발자가 직접 관리할 필요 없이 JVM의 GC가 불필요한 메모리를 정리해 줍니다.

char* read_buffer(FILE *file, const int size)
{
  int foo = 0;
  char* buf = (char*) malloc(size);
 
    if (buf == NULL)
    {
      return NULL;
    }
    if (fread(buf, 1u, size, file) != size)
    {
      return NULL;  // 읽기 실패!
    }
    return buf;
}

foo와 같은 지역변수는 함수 내에 선언해서 사용하는 변수로, 함수가 종료될 때 자동으로 사라집니다.
buf변수는 malloc함수를 통해 Heap영역의 메모리를 할당 받아 사용되고 메모리 할당을 받으면 명시적으로 메모리 해제를 해주어야 합니다.

만약 fread함수에서 파일 읽기가 실패하면 return NULL을 하고 read_buffer함수가 종료되는데, 이때 바로 메모리 누수가 발생합니다. Heap메모리 할당을 받은 buf변수가 free를 통해 메모리가 해제되지 않았기 때문입니다.
이 경우 프로그램이 종료되어도 해당 메모리 공간을 다시 사용할 수 없으며 누적된다면 시스템 전체의 메모리 부족 현상이 발생 할 수 있습니다.

해결방법:

if (fread(buf, 1u, size, file) != size)
    {
      free(buf);  // 메모리 해제
      return NULL;  // 읽기 실패!
    }

Garbage Collection의 과정

1. Stop The World

GC을 위해 JVM이 애플리케이션의 실행을 멈추는 작업입니다.
GC가 실행될 때는 GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개됩니다.

모든 쓰레드들의 작업이 중단되면 애플리케이션이 멈추기 때문에, GC의 성능 개선을 위해 튜닝을 한다고 하면 보통 stop-the-world의 시간을 줄이는 작업을 뜻합니다. JVM 또한 이러한 문제를 해결하기 위해 다양한 실행 옵션을 제공하고 있습니다.

2. Mark and Sweep

Stop The World를 통해 모든 작업이 중단된 뒤,
GC는 스택의 모든 변수 또는 Reachable객체가 각각 어떤 객체를 참조하는지 탐색하여 사용되는 메모리와 사용되지 않는 메모리로 구분하는 Mark과정을 진행합니다.
이후에 Mark되지 않은 즉, 사용되지 않는 객체들을 메모리에서 제거하는데 이러한 과정을 Sweep이라고 합니다.



Memory leak case


Static

static View vista;

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	
	// Variable estática con referencia al contexto de actividad
	vista = new View(this);
}

vista는 static변수로 Activity Context를 참조하고 있습니다.
이 경우, 애플리케이션이 계속 실행중이라면 Activity가 onDestroy()되어도 메모리가 해제되지 않습니다.

해결방법:

@Override
protected void onDestroy() {
    super.onDestroy();
    
    // 메모리를 null로 만들기.
    vista = null;
}

Inner class

기본적으로 Java의 일반적인 객체는 strong reference입니다. 보통 GC 대상을 선정하는 과정에서 reachable, unreachable로 나뉘게 되는데 root set으로부터 시작된 참조 사슬에 포함되어 있으면 reachable 객체, 참조 사슬에서 자신을 참조하고 있는 reachable 객체가 없을 경우 unreachable 객체로 나뉘게 됩니다.

물론 WeakReference, SoftReference, PhantomReference 등을 적용하면 GC 과정에서 사용자의 개입이 일어날 수 있지만 이 글에선 논외로 설명하겠습니다.

// outer class
public class LeakActivity extends Activity {

  // Anonymous class: non-static inner class
  private final Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // do work
    }
  }
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    handler.postDelayed(new Runnable() {
      @Override
      public void run() {}
    }, 60000);  // 1분 뒤, 작업 요청 (=work delay)
    
    finish();  // Activity 종료.
  }
}

Java에서 non-static inner class의 경우 outer class에 대한 참조를 가집니다.
위 예시코드에서도 Handler(non-static inner class)LeakActivity(outer class)에 대한 참조를 가지고 main thread에서 생성되었으므로, main thread의 Looper 및 Message Queue에 바인딩 됩니다.

finish()를 수행하면 LeakActivity가 종료되지만, Message Queue에 delay된 작업이 남아있음으로써 Handler의 LeakActivity에 대한 참조가 남아있기 때문에 LeakActivity는 GC의 대상이 되지 않습니다.

add.) Anonymous class도 non-static inner class와 동일하게 outer class에 대한 reference를 가지게 됩니다.

해결방법:
non-static inner class는 outer class에 대한 참조를 가지지만, static inner class는 outer class에 대한 참조를 가지지 않습니다. 그리고 추가로 Handler 내부에서 Activity의 메서드나 변수 등 리소스를 참조해야 될 경우 WeakReference를 활용해야 합니다.

// outer class
public class NonLeakActivity extends Activity {
  private NonLeakHandler handler = new NonLeakHandler(this);
  
  // static inner class
  private static final class NonLeakHandler extends Handler {
    private final WeakReference<NonLeakActivity> ref;
    
    public NonLeakHandler(NonLeakActivity act) {
      ref = new WeakReference<>(act);  
    }
    
    @Override
    public void handleMessage(Message msg) {
      NonLeakActivity act = ref.get();  // Activiy 리소스 참조
      if (act != null) {
        // do work  
      }
    }
  }  // End: NonLeakHandler
  
  // Anonymous class: static inner class
  private static final Runnable runnable = new Runnable() {
    @Override
    public void run() {}
  }
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    handler.postDelayed(runnable, 60000);
    finish();
  }
}  // End: NonLeakActivity 

Inner Class 의 경우 Activity의 Lifecycle에 동일하게 생성 및 종료가 보장된다면 non-static inner class로 정의해도 되지만 그렇지 않은 경우 static inner class로 정의함으로써 메모리 누수를 방지 할 수 있습니다.


Background

비동기 작업Activity가 종료된 뒤에도 백그라운드에서 계속 실행되므로 Activity에 접근 할 수 있습니다.

Thread thread;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  thread = new Thread() {
    @Override
    public void run() {
      if (!isInterrupted()) {
        // Referencia al contexto de la actividad
      }
    }
  };
thread.start();
}

해결방법:

@Override
protected void onDestroy() {
  super.onDestroy();
  thread.interrupt();  // Tread 중단.
}

이벤트 핸들링

SensorManager sensorManager;
Sensor sensor;

private void registrarSensor () {
  sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
  // register 등록
  sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}  

해결방법:

private void desregistrarSensor () {
  if (sensorManager != null && sensor != null) {
    // register 등록 취소
    sensorManager.unregisterListener(this, sensor);
  }
}



Reference)

0개의 댓글