반응형
<Java>
1. Prerequisite & Reference
- 메모리에 대한 이해.
- Java 에 대한 기초 지식.
- C나 C++에 대한 기초 지식.
- 프로그램 경험 ( Java 뿐만 아니라 직접 메모리를 할당하는 C 나 C++ 도 경험이 있다면 더 좋다. )
2. Intro
이 녀석은 알아서 메모리를 관리해주는 녀석인데 왜 이녀석을 알아야 하나요?
Garbage Collection 또는 Garbage Collector 로 잘 알려진 이 GC 라는 녀석은 자동으로 메모리를 관리해주기 때문에
C나 C++ 에 비해 메모리를 관리하는 수고를 덜어주는 녀석이 확실합니다.
하지만 이 GC 라는 녀석이 하는 일은 매우 Heavy하고 100% "우리가 기대하는데로" 작동하진 않습니다.
이녀석도 프로그램이라 정해진 로직으로 작동할 뿐이죠.
GC에도 여러가지 방식이 있는데 방식에 따라서 Application이 잠시 Stuck ( GC 하는 동안 멈춤 ) 되기도 하고,
GC를 하는 동안 성능이 떨어지는 등의 일도 발생하지요.
또한 잘못된 메모리 관리는 GC의 로직에 벗어나며 OutOfMemoryError 도 초래합니다.
이녀석은 Exception이 아닌 Error라서 복구하기도 매우 힘듭니다.
GC에 대한 이해는 결국 Memory 관리방법에 대한 전략으로 이어지며,
개발 결과물에 대한 Performance 로 결론 지어집니다.
Best Programmer 가 아니더라도, Good Programmer 가 되려면 GC에 대한 이해는 필수죠.
3. Information
GC가 뭐하는 녀석인지 우선 간단히 소개 좀 해주세요
먼저 C와 C++을 프로그래밍 해보신 분들이라면 malloc 과 free 을 신나게 써보셨을 것입니다.
그리고 malloc과 free 가 제대로 되지 않으면 시스템이나 어플리케이션이 뻗어버리는 현상도 많이 접하셨겠죠.
사실 저는 C와 C++ 언어가 Java 보다 어렵다고 생각하는 1인입니다.
무엇보다도 메모리와 관련된 포인터라던지 malloc, free 가 가장 어렵게 느껴집니다.
얼마의 메모리가 필요한 지 알아야 하고, 확장할 때 또 계산해줘야 하고, 포인터는 &* 등의 추가 연산자를 붙여서 처리해줘야 하고,
free 해주지 않으면 메모리는 무한정 늘어나 버릴테니까 말이죠.
고민해야 할게 이만저만이 아닙니다.
이런 측면에서 pointer, malloc, free 등의 어려운 점을 한방에 "어느 정도" 해결해줄 수 있는 Java의 GC 는 정말로
프로그래밍의 혁명 자체죠.
GC는 Garbage Collection 또는 Garbage Collector 로 알려져 있으며,
참조되지 않는 메모리, 이것을 쓰레기 ( Garbage ) 로 표현하는데, 이 쓰레기 메모리들을 자동적으로 free 해주는 녀석이라고 보면 됩니다.
사실 GC는 Garbage Collection이라는 이름 때문에 보통 free 의 역할만을 하는 것으로 알고 있는데요.
요 녀석이 객체의 메모리 할당, 사용중인 메모리의 인식, 사용 하지 않는 메모리의 인식의 역할도 하고 있습죠.
즉 GC 는 pointer, malloc, free 이 세가지 + 감시(?) 모두 수행하는 녀석입니다.
이 녀석의 도움때문에 우리가 할 것은 new 로 객체 생성..
그리고.......
이것이 끝이 되어버립니다. 아주 쉽죠? 그냥 생성해서 쓰고 방치해두면 저 청소부 녀석이 알아서 청소를 해줍니다.
메모리 할당과 해제를 이녀석이 알아서 해줘버리니 말이죠.
GC 대상이 new 로 생성되는 객체들인데 이 녀석들에 대해서는 몰라도 되나요?
좋은 질문입니다. 당연히 알아야 합니다.
GC 입장에서 모든 객체는 그 나름의 생명 주기가 있습니다.
1. Created ( 생성 )
2. In use or reachable ( 사용 중 )
3. Invisible ( 사용 중 but 접근 불가 )
4. Unreachable ( 사용 되지 않음 )
5. Collected ( GC 대상이 됨 )
6. Finalized ( Finalize 를 거친 상태 )
7. Deallocated ( 메모리 해제 된 상태 )
자 이 생명주기의 각각의 단계가 무엇을 의미하는지, 무엇을 하는지를 알아봅시다.
이 녀석들에 대한 파악은 앞으로의 코딩 스타일에 많은 영향을 미칠 것이기 때문이죠.
Created 생성
먼저 객체를 위한 메모리 공간을 Heap에 할당합니다.
그 다음 Super class 의 생성자 호출하죠.
initializer 및 instance variable의 initialize를 수행 한 후.
해당 객체의 생성자를 수행합니다.
[Initializer]
In Use or Reachable 사용중
객체가 생성되어 다른 객체에 의해 참조되어 있는 상태입니다.
이 상태를 Strongly referenced 상태라고 합니다.
Invisible 사용 중 but 접근불가
모든 객체가 이 상태를 거치는 것은 아닙니다만, 눈여겨 볼 필요가 있습니다.
Invisible 상태는 Strongly referenced 는 되어 있지만, 이 녀석을 직접적으로 접근할 수 없는 상태입니다.
여기서 주의할 점은 Invisible 객체가 항상 바로 GC 대상이 되지 않는다는 것이죠.
Invisible에 대해서는 정확히 파악이 어려우니 예제를 함께 보도록 하죠.
public void run(){
Object foo = new Object();
foo.doSomething();
while( true ){
// do something.
}
}
여기서 foo 라는 녀석은 적어도 run() function 이 return될때까지는 strong reference 를 가집니다.
이 상태가 바로 invisible 상태입니다. run() 안에서 생성되어 strong reference를 가졌지만 저 녀석을 다시 접근할 수 없는 상태에서.
while 문이 끝날 때까지는 계속 메모리가 유지되는 것이죠.
따라서 memory leak 을 유발할 수 있으며, 명시적으로 null 을 만들어 주는 것이 좋습니다.
null 이 되면 strong reference 가 해지되면서 Unreachable 상태로 돌아서기 때문이죠.
Unreachable 사용되지 않음
Strong reference가 존재 하지 않을 경우이며 Unreachable 상태의 객체는 GC 의 후보가 됩니다.
여기서 후보가 된다는 것은 바로 GC 가 된다는 게 아니라, GC 대상 큐에만 들어간다는 개념입니다.
엄밀히 이야기하면 이러한 객체들은 GC의 루트가 가지는 체인에 참조가 되는 형태이며,
GC 가 수행될 때 이 루트의 체인을 따라가며 메모리 해제를 하는 식이 되는 것이죠.
[순환참조인 녀석들은 그럼 영원히 GC가 안 되는 건가요?]
Collected GC 대상이 됨
이제 메모리 해제 단계의 도입부에 왔습니다.
이 상태에서는 GC가 해당 객체의 finlize() function 이 정의되어있는지 판단을 하게 되죠.
finalize 가 있다면 finalizer 라는 queue 에 넣어놓고, 없다면 바로 다음 단계인 Finalized 상태로 전환을 시킵니다.
Finalized Finalize 를 거친 상태
Finalizer에 의해 finalize가 실행된 후의 상태입니다.
앞서 Collected 에서 설명했지만, finalize 는 Collected 상태라고 바로 수행되는 것이 아니라,
Finalizer의 Queue에 들어가는 것이기 때문에 해당 객체의 finalize 가 Call 되는 timing은 보장되지 않습니다.
또한 JVM에 따라서 수행 시간도 다르다고 하네요.
결론적으로 GC를 수행함에 있어 객체에 finalize 가 구현 되어있다면 객체의 반환시간은 그만큼 더 늦어집니다.
게다가 finalize를 위한 객체의 메모리도 그만큼 증가하겠죠. 참고적으로 재생이라는 현상도 발생할 수 있습니다.
[재생 ( Resurrection )]
Deallocated 메모리가 해제된 상태
메모리의 반환이 끝난 상태로, GC의 동작이 마무리 되었습니다.
저런 좋은 녀석이 언제 문제가 되나요?
앞서 객체의 생명주기와 함께 살펴보았듯이 GC의 기준이 되는 것은 보통 Strong Reference 의 여부입니다.
이 Strong Reference 를 불필요하게 유지한다면, GC의 타겟이 되어야 할 객체들이 GC가 되지 않은체
메모리에서 딱~ 자리를 잡고 하는 일없이 삐대고 있게 되는 것이죠.
이런식으로 메모리가 증가하게 되면 결국 사용 가능한 메모리가 다 소진되어 버립니다.
메모리가 꽉 찼을 때는 보통 다음과 같은 두가지 경우가 발생합니다.
첫번째는 가상메모리 사용입니다.
여기서 가상메모리에 대해 자세히 다루지는 않겠습니다만, 가상메모리 사용에 관한 성능저하는 누구나 알고 있을 것이라 믿습니다.
RAM <-> Hard Disk 사이의 memory swap은 생각보다 큰 operation 이죠.
두번째는 OutOfMemoryError입니다.
이 녀석은 Exception과 다르게 한번 넘어간다고 해결될 녀석이 아닙니다.
이 녀석이 발생했을 경우 메모리 해지가 제대로 일어나지 않는다면, 이 녀석은 계속 불리게 되겠죠.
물론 Error 에 대한 예외처리를 하지 않았을 경우 어플리케이션이 다운될 수 있다는 것 역시 무시무시합니다.
( Android의 경우는 메모리 확보를 위해 중요도가 낮은 process 를 강제로 죽여버립니다. )
메모리를 다 사용하지 않더라도, 미숙한 메모리 관리는 지속적인 GC로 이어지게 됩니다.
GC 자체가 매우 무거운 operation입니다.
GC의 방식에 따라서 다른 동작은 모두 hold 하고 GC만을 수행하는 경우가 있는데, 이 경우는 GC가 끝날때까지 프로그램은 멈춥니다.
다른 동작과 GC가 하는 경우도 있지만, 이 경우에는 원래 프로그램의 성능이 매우 하락되죠. 엄청 버벅댄다는 이야기입니다.
[GC 방식에 대한 짧은 소개]
결국 메모리 관리를 제대로 못하는 것은
GC라는 좋은 Tool 을 가지고도 특성을 파악하지 못해 제대로 사용하지 못하고 있는 꼴이 되는 것이죠.
게임으로 말하자면 게임에 하나뿐인 유니크 에픽 무기를 가지고도
내구도가 0 인 체로 계속 사용하고 있는 꼴이라고 볼 수 있겠습니다. 감이 확 오시나요? :)
GC를 강제로 부를 수도 있나요?
예 강제로 부르는 방법은 분명 제공하고 있습니다.
System.gc() 나 Runtime.getRuntime().gc() 를 통해서 할 수 있죠.
하지만 권장되지 않습니다. 말 했던데로 GC가 소모하는 시스템 리소스의 양이 많은데다,
잘못된 프로그래밍은 버벅대기만 하는 프로그램을 만들 수도 있기 때문입니다.
게다가 GC 나름대로 GC를 발동시키는 알고리즘 ( 또는 타이밍 ) 이 있는데, 이 알고리즘이 매우 합리적이기도 하죠.
( 초보 개발자가 다루는 것보다는 훨~~~~씬 합리적일 것입니다. )
일부 고참 개발자들은 "절대 강제호출 하지 말라!" 라고 이야기하기도 합니다.
[GC 타이밍에 대한 짧은 소개]
4. Summary
- GC ( Garbage Collection ) 은 매우 heavy 한 작업이고, "우리가 기대하는 데로" 작동하지 않기 때문에 알아둘 필요가 있다.
- GC는 C나 C++의 pointer, malloc, free 의 역할을 모두 담당해주는 녀석으로, new 를 통해 heap 메모리에 생성된 객체가
사용되지 않을 때 ( Strong reference 가 없을 때 ) 그 객체를 free 해준다.
- GC에 대해 이해하기 위해서는 객체의 생성 주기를 알아야 한다.
- 객체의 생성 주기 안에는 순환참조, Invisible, 재생, finalize 등의 매우 rare 하지만 반드시 고려하고 알아야 할 사항들이 있다.
- GC는 만병통치약이 아니며 OutOfMemoryError 나 Performance degradation으로 이어질 수 있다.
- 강제로 GC 를 부르는 행위는 암묵적으로 금지되어 있다.
- GC를 이해하고 Memory 관리를 잘 하는 것은 Good Programmer 로서 반드시 갖춰야 할 덕목이다.
5. Reference.
- http://50001.com/sub/down/GarbageCollection.pdf
Garbage Collection 에 대해 예제와 함께 심도있게 다룬다. 후반부에 가면서 좀 어렵지만 한번쯤 꼭 읽어보길 권장하다.
- [자바 성능을 결정짓는 코딩 습관과 튜닝 이야기], 이상민 저, 한빛미디어
자바의 성능에 관련된 여러가지 사항들을 소개하는 책으로 자바 중급자정도가 읽으면 좋은 책.
반응형
댓글