가비지 컬렉션이란 뭘까?
소프트웨어를 개발하면서 사용이 끝난 메모리가 적절히 해제되지 않으면 가비지(Garbage)'가 쌓일 수 있습니다.
사용되지 않는 메모리가 남아 가비지(Garbage)가 발생할 수 있는데 C 언어에서는 이러한 메모리를 직접 관리해야 하며, free() 함수를 통해 수동으로 해제합니다.
하지만 Java나 Kotlin과 같은 언어에서는 개발자가 메모리 해제를 직접 수행하지 않아도 됩니다. 이는 JVM의 가비지 컬렉터(Garbage Collector)가 자동으로 불필요한 메모리를 정리해주기 때문입니다.
[ JVM 메모리 구조 ]
--------------------------------------------------------
| Heap Memory |
|------------------------------------------------------|
| Young Generation |
| ---------------------------------------------- |
| | Eden Space | |
| ---------------------------------------------- |
| | Survivor Space 0 (S0) | Survivor Space 1 (S1) |
| ---------------------------------------------- |
| |
| Old Generation |
| |
--------------------------------------------------------
| Metaspace |
--------------------------------------------------------
설명:
Heap Memory: JVM에서 동적으로 생성된 객체들이 저장되는 공간입니다.
Young Generation (젊은 세대): 새롭게 생성된 객체들이 저장되는 영역으로, 가비지 컬렉션이 빈번하게 발생합니다.
Eden Space: 모든 신규 객체가 처음으로 할당되는 공간입니다.
Survivor Space 0 (S0) & Survivor Space 1 (S1): Eden Space에서 가비지 컬렉션 후 살아남은 객체들이 이동하는 공간입니다. 두 개의 영역이 번갈아 가며 사용됩니다.
Old Generation (노년 세대): Young Generation에서 살아남은 장수 객체들이 저장되는 공간입니다. 가비지 컬렉션이 덜 빈번하게 발생하지만, 한번 발생하면 시간이 오래 걸릴 수 있습니다.
Metaspace: 클래스 메타데이터와 메소드 정보 등이 저장되는 영역입니다. Java 8 이상에서 사용되며, 이전 버전의 **Permanent Generation (PermGen)**을 대체합니다.
불필요해진 객체를 명시적으로 표시하기 위해 null을 할당하는 경우가 일반적입니다.
예를 들어 다음과 같은 코드가 있다고 가정해봅시다.
Person person = new Person();
person.setName("Alice");
person = null;
// 가비지 발생
person = new Person();
person.setName("Bob");
위 코드에서 처음 생성된 person 객체는 더 이상 참조되지 않으므로 가비지가 됩니다. Java와 Kotlin에서는 이러한 메모리 누수를 방지하기 위해 가비지 컬렉터가 주기적으로 메모리를 회수합니다.
참고로, Java에서 System.gc() 메서드를 호출하여 가비지 컬렉션을 요청할 수 있지만, 이는 시스템 성능에 부정적인 영향을 줄 수 있으므로 권장되지 않습니다.
개념
가비지 컬렉션(Garbage Collection)은 프로그래밍 언어에서 메모리 관리를 자동화하는 기술입니다. 프로그램 실행 중에 더 이상 사용되지 않는 객체나 메모리 공간을 자동으로 감지하고 회수하여, 메모리 누수(memory leak)를 방지하고 시스템의 안정성과 효율성을 높입니다. 그리고 시스템 성능을 최적화하는 데 중요한 역할을 합니다.
이를 통해 개발자는 메모리 할당과 해제에 대한 부담을 덜고 코드 작성에 집중할 수 있습니다.
가비지 컬렉션은 주로 Java, C#, Python 등 고수준 프로그래밍 언어에서 사용됩니다.
왜 사용하지 않는 객체의 메모리를 청소할까?
사용하지 않는 객체의 메모리를 청소하는 이유는 메모리 누수(memory leak)를 방지하고 시스템의 안정성과 성능을 유지하기 위해서입니다. 프로그램이 실행되는 동안 생성된 객체들이 더 이상 필요 없게 되면, 해당 객체들이 차지하고 있는 메모리를 해제하여 다른 용도로 활용할 수 있어야 합니다. 그렇지 않으면 사용되지 않는 객체들이 메모리를 계속 점유하게 되어 메모리 부족 현상을 일으킬 수 있으며, 이는 프로그램의 성능 저하나 시스템 충돌등의 문제로 이어질 수 있습니다. 따라서 가비지 컬렉션을 통해 불필요한 메모리를 회수함으로써 효율적인 메모리 관리를 수행합니다.
가비지 컬렉터와 가비지 컬렉션의 차이는?
- 가비지 컬렉션(Garbage Collection): 프로그래밍 언어에서 더 이상 사용되지 않는 객체를 자동으로 식별하고 메모리를 회수하는 메커니즘이나 과정을 의미합니다. 이는 메모리 관리를 자동화하여 개발자가 직접 메모리를 해제하지 않아도 되게 해줍니다.
- 가비지 컬렉터(Garbage Collector): 가비지 컬렉션 과정을 실제 수행하는 모듈이나 컴포넌트를 지칭합니다. 즉, 가비지 컬렉션이라는 프로세스를 실행하는 주체입니다.
요약하면, 가비지 컬렉션은 개념 또는 프로세스이고, 가비지 컬렉터는 그 프로세스를 실행하는 엔진 또는 도구입니다.
메모리 누수가 발생하는 원인과 발생했을때 일어나는 문제
메모리 누수는 프로그램에서 할당된 메모리를 해제하지 않고 계속 사용하거나 접근할 수 없는 상태로 남겨두는 문제입니다. 메모리 누수가 발생하면 시스템 전체 성능에 부정적인 영향을 미칠 수 있으며, 장기적으로는 응용 프로그램의 불안정성과 비정상적인 종료로 이어질 수 있습니다. 여기에는 몇 가지 주요 원인과 발생할 수 있는 문제들이 있습니다.
1) 메모리 누수 발생 원인
1. 참조를 적절하게 해제하지 않음 : 객체나 자원을 사용한 후에 참조를 null로 설정하지 않거나, 적절하게 해제하지 않는 경우가 있습니다. 이 경우에는 가비지 컬렉터가 이 객체를 회수할 수 없어 메모리가 계속 쌓이게 됩니다.
2. 순환 참조 : 서로를 참조하는 객체 그룹이 있는 경우, 이 그룹이 더 이상 사용되지 않더라도 각 객체가 서로를 참조하고 있어 가비지 컬렉터가 이를 회수하지 못하는 상황이 발생할 수 있습니다.
3. 리소스 누수 : 파일 핸들, 네트워크 연결, 데이터베이스 커넥션 등의 리소스를 적절하게 해제하지 않은 경우 메모리 누수가 발생할 수 있습니다. 예를 들어, 파일을 열고 닫지 않거나, 데이터베이스 커넥션을 제대로 해제하지 않으면 해당 리소스는 계속해서 메모리를 차지하게 됩니다.
4. 캐시 관리: 캐시를 사용할 때 적절한 메모리 관리가 필요합니다. 캐시된 데이터가 오래 사용되거나 캐시를 정리하지 않으면 메모리 누수가 발생할 수 있습니다.
설명: 메모리 누수가 어떻게 발생하고 시스템에 어떤 영향을 미치는지 시각화합니다.
정상적인 메모리 사용량
^
| /\ /\ /\
| / \ / \ / \
|_____/ \____/ \____/ \____ 시간 -->
- 메모리 사용량이 증가하고 가비지 컬렉션으로 감소하며 일정 범위를 유지합니다.
메모리 누수 발생 시 메모리 사용량
^ /
| /
| /
| /
|_________/____________________________ 시간 -->
- 메모리 사용량이 지속적으로 증가하여 결국 시스템 자원이 고갈됩니다.
세부 설명
- 원인 표시:
- 코드에서 객체에 대한 참조를 해제하지 않아 메모리가 해제되지 않는 부분을 강조합니다.
- 예를 들어, 컬렉션에서 객체를 제거하지 않거나, 이벤트 리스너를 해제하지 않는 경우입니다.
- 결과 표시:
- 메모리 부족으로 인해 애플리케이션의 성능 저하, 응답 지연 또는 OutOfMemoryError와 같은 오류가 발생할 수 있습니다.
- 이는 시스템의 안정성을 해치고 사용자 경험을 저하시킵니다.
2) 메모리 누수로 인한 문제
1. 성능 저하 : 메모리가 누적되면 시스템 전체의 성능이 저하될 수 있습니다. 가비지 컬렉터가 더 많은 메모리를 회수하려고 시도하면 CPU 자원이 많이 소비될 수 있습니다.
2. 응용 프로그램 불안정성 : 메모리 누수가 지속되면 응용 프로그램이 예상치 못한 시점에 메모리 부족 오류(OutOfMemoryError)를 발생시킬 수 있습니다. 이는 프로그램의 비정상적인 종료로 이어질 수 있습니다.
3. 시스템 다운 : 메모리 누수가 심각하게 발생하면 운영 체제 수준에서도 시스템 다운을 초래할 수 있습니다. 특히 서버 환경에서는 이러한 문제가 심각한 영향을 미칠 수 있습니다.
4. 리소스 부족 : 메모리 누수로 인해 시스템 자원(메모리)이 고갈되면 다른 프로세스나 서비스에도 영향을 미칠 수 있습니다.
3) 예방과 해결 방법
- 정기적인 코드 리뷰 : 메모리 관리에 주의를 기울여야 합니다. 코드 리뷰를 통해 메모리 누수가 발생할 수 있는 부분을 사전에 예방할 수 있습니다.
- 프로파일링 도구 사용 : 메모리 사용을 모니터링하고 메모리 누수가 발생하는지 여부를 확인할 수 있는 프로파일링 도구를 사용하는 것이 좋습니다.
- 자원 해제 : 파일 핸들, 네트워크 연결 등을 사용한 후에는 반드시 자원을 해제하는 코드를 작성해야 합니다. try-with-resources 구문을 활용하여 자동으로 자원을 해제하는 방법도 있습니다.
- 캐시 관리 : 캐시된 데이터의 유효기간을 설정하고, 필요 없는 데이터는 정기적으로 정리하는 방법을 사용하여 메모리 사용을 최적화할 수 있습니다.
메모리 누수는 프로그램의 안정성과 성능에 큰 영향을 미칠 수 있는 중요한 문제입니다. 따라서 메모리 관리에 대한 이해와 주의가 필요합니다.
가비지 컬렉션의 동작 원리
1. 객체 생성과 메모리 할당 : 프로그램이 실행될 때, 객체들은 힙(heap) 메모리에 동적으로 할당됩니다. 이때 객체가 생성되면 가비지 컬렉터에 의해 추적됩니다.
2. 참조 계수(Reference Counting) : 일부 언어에서는 객체가 참조될 때마다 해당 객체의 참조 계수를 증가시키고, 참조가 없어질 때 감소시키는 방식을 사용합니다. 참조 계수가 0이 되면 해당 객체는 메모리에서 해제됩니다. 그러나 Java에서는 이 방식을 사용하지 않습니다.
3. Reachability : Java의 가비지 컬렉션은 Reachability(도달 가능성) 기반으로 동작합니다. 즉, 어떤 객체가 다른 객체에 의해 직접적이거나간접적으로 참조될 수 있으면, 그 객체는 도달 가능(reachable) 상태입니다. 도달 가능하지 않은 객체들은 가비지 컬렉터에 의해 처리됩니다.
4. 가비지 컬렉션 알고리즘 : Java의 가비지 컬렉터는 다양한 알고리즘을 사용하여 메모리를 관리합니다. 주요 알고리즘에는 다음과 같은 것들이 있습니다.
- Mark and Sweep : 사용되지 않는 객체를 표시(mark)하고, 표시된 객체들을 힙에서 제거(sweep)하는 방식입니다.
- Stop-the-world : 가비지 컬렉션 실행을 위해 JVM이 일시적으로 프로그램 실행을 멈추는 것입니다. 이는 모든 스레드가 일시 정지되는 단점이 있지만, 일반적으로 짧은 시간 동안 발생하며, 최근의 JVM에서는 이를 최적화하는 방법들이 계속 개발되고 있습니다.
- Generational : 힙을 새 객체와 오래된 객체로 나누어 관리하며, 오래된 객체는 많이 접근되어야만 가비지 컬렉션의 대상이 됩니다.
설명: 가비지 컬렉션이 어떻게 동작하는지를 단계별로 보여주는 흐름도입니다.
[객체 생성]
|
V
[Eden Space에 객체 할당]
|
V
[Reachability Analysis]
|
V
[Mark Phase]
|
V
[Sweep Phase]
|
V
[Compaction Phase (선택적)]
세부 설명
- 객체 생성: 프로그램에서 객체가 생성되며, 이때 JVM의 힙 메모리 중 Eden Space에 할당됩니다.
- Reachability Analysis (도달 가능성 분석):
- 가비지 컬렉터는 GC Roots로부터 시작하여 객체들이 참조되고 있는지 확인합니다.
- GC Roots에는 스택에 있는 지역 변수, 정적 변수 등이 포함됩니다.
- Mark Phase (표시 단계):
- 도달 가능한 객체들을 마크하여 표시합니다.
- 마크된 객체는 현재 프로그램에서 사용 중임을 나타냅니다.
- Sweep Phase (소거 단계):
- 마크되지 않은 객체들, 즉 도달 불가능한 객체들을 메모리에서 제거합니다.
- 이 과정에서 메모리가 해제되어 다른 객체들이 사용할 수 있게 됩니다.
- Compaction Phase (압축 단계, 선택적):
- 메모리 단편화를 줄이기 위해 남아있는 객체들을 한쪽으로 모읍니다.
- 이로써 큰 연속된 메모리 공간을 확보할 수 있습니다.
추가 요소
- Stop-the-World 이벤트:
- 가비지 컬렉션이 실행되는 동안 JVM은 애플리케이션의 실행을 일시 중지합니다.
- 이를 "Stop-the-World" 이벤트라고 하며, 짧은 시간 동안 발생합니다.
- 병렬 처리:
- 현대의 가비지 컬렉터는 여러 스레드를 사용하여 병렬로 작업을 수행합니다.
- 이를 통해 가비지 컬렉션의 효율성을 높이고, 애플리케이션의 중단 시간을 줄입니다.
설명: 객체가 생성되고 사용되다가 가비지 컬렉션에 의해 회수되는 전체 과정을 한눈에 보여줍니다.
[객체 생성] (참조 카운트: 1)
|
V
[객체 사용] (참조 카운트: 1)
|
V
[참조 해제] (참조 카운트: 0)
|
V
[가비지 컬렉션 대상]
|
V
[메모리 회수]
세부 설명
- 객체 생성:
- new 키워드를 통해 객체가 생성되고, 참조 변수에 할당됩니다.
- 참조 카운트는 1이 됩니다.
- 객체 사용:
- 프로그램에서 객체의 메서드 호출이나 속성 접근 등이 이루어집니다.
- 참조 해제:
- 객체에 대한 모든 참조가 제거됩니다. 예를 들어, 참조 변수에 null을 할당합니다.
- 참조 카운트는 0이 되며, 더 이상 객체에 접근할 수 없습니다.
- 가비지 컬렉션 대상 식별:
- 가비지 컬렉터는 참조되지 않는 객체를 가비지로 식별합니다.
- 메모리 회수:
- 가비지 컬렉션 과정에서 해당 객체의 메모리가 해제됩니다.
추가 요소
- 참조 카운트:
- 객체의 참조 수가 어떻게 변화하는지 표시하여 메모리 관리의 이해를 돕습니다.
- Finalization:
- 객체 소멸 전에 finalize() 메서드가 호출될 수 있으나, Java 9 이후로는 사용이 권장되지 않습니다.
- 대신 try-with-resources나 AutoCloseable 인터페이스를 사용하는 것이 좋습니다.
Java에서의 가비지 컬렉션 예제
가비지 컬렉션 예제는 일반적으로 Java 프로그램에서 특별한 설정 없이도 자동으로 이루어집니다. 개발자는 명시적으로 객체를 해제하거나 메모리를 관리할 필요가 없습니다. 가비지 컬렉션을 이해하는 데 도움이 되는 간단한 예제를 보여드리겠습니다.
public class GarbageCollectionExample {
public static void main(String[] args) {
// 객체 생성
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
// obj1과 obj2는 현재 도달 가능한 상태
// obj2를 null로 설정하여 참조 해제
obj2 = null;
// obj2는 더 이상 도달 가능하지 않음. 가비지 컬렉션의 대상이 됨.
// 가비지 컬렉터 호출 (명시적으로 호출하는 방법은 없으며, JVM이 적절한 시점에 실행)
System.gc();
}
static class MyClass {
// 간단한 내부 클래스
}
}
이 예제에서 `MyClass`는 간단한 클래스이며, `GarbageCollectionExample`에서 객체를 생성하고 참조를 해제한 후에 `System.gc()`를 호출하여 가비지 컬렉션을 유도합니다. 이는 실제로 프로그램에서는 권장되지 않는 방법입니다. Java의 가비지 컬렉션은 JVM에 의해 자동으로 관리되며, 보통은 개발자가 직접적으로 이를 제어할 필요가 없습니다.
메모리 누수 예방을 위한 Best Practices 다이어그램
설명: 메모리 누수를 예방하기 위한 코드 작성 패턴과 주의사항을 시각적으로 정리합니다.
Do's and Don'ts 목록
Do's (지켜야 할 사항)
1. 자원 해제:
- 파일, 데이터베이스 연결, 네트워크 소켓 등 자원을 사용한 후에는 반드시 해제합니다.
- try-with-resources 구문 사용:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// 파일 읽기 작업
} catch (IOException e) {
e.printStackTrace();
}
// BufferedReader는 자동으로 닫힙니다.
2. 컬렉션에서 객체 제거:
- 사용이 끝난 객체는 컬렉션에서 제거하여 참조를 해제합니다.
List<Object> list = new ArrayList<>();
Object obj = new Object();
list.add(obj);
// 작업 수행 후
list.remove(obj);
3. 약한 참조(WeakReference) 사용:
- 캐시나 맵에서 객체를 참조할 때 약한 참조를 사용하여 가비지 컬렉션이 가능하도록 합니다.
Map<Key, WeakReference<Value>> cache = new HashMap<>();
Don'ts (피해야 할 사항)
- 불필요한 전역 변수 사용:
- 필요 없는 객체를 전역 변수나 정적(static) 변수로 유지하지 않습니다.
- 무한한 객체 추가:
- 제한 없이 컬렉션에 객체를 추가하고 제거하지 않는 것은 메모리 누수를 유발합니다.
- finalize() 메서드 사용:
- finalize()는 예측 불가능하고 성능에 영향을 주므로 사용을 피해야 합니다.
- 쓰레드 풀 및 타이머 관리 부실:
- 쓰레드 풀이나 타이머를 사용한 후에는 적절히 종료하거나 해제해야 합니다.
가비지 컬렉션의 장점
- 메모리 누수 방지 : 사용하지 않는 객체를 자동으로 제거하여 메모리 누수를 방지합니다.
- 편리한 개발 : 개발자는 메모리 관리에 대해 걱정할 필요가 없어져서 개발 생산성이 향상됩니다.
- 시스템 성능 최적화 : 가비지 컬렉션은 JVM 내에서 메모리 사용을 최적화하여 시스템 성능을 향상시킵니다.
결론
Java의 가비지 컬렉션은 메모리 관리를 자동화하여 개발자가 메모리 누수와 같은 문제를 걱정하지 않도록 돕습니다. 이는 Java가 편리한 개발 환경을 제공하며, 고급 언어에서 자주 사용되는 기능 중 하나입니다.
'프로그래밍 언어 > Java' 카테고리의 다른 글
[Java] 멀티스레딩 및 동시성 (1) | 2025.01.05 |
---|---|
[Java] 디자인 패턴? (0) | 2025.01.05 |
[Java] 자바 최신 문법 (Java 17+) (1) | 2025.01.05 |
[JAVA] 자바에서 Comparable과 Comparator 객체 비교의 이해와 활용 (0) | 2024.06.11 |
[JAVA] 문자열 처리, 배열과 반복문, 조건문, 수학적 계산 개념 (0) | 2024.06.05 |