자바에서는 힙 메모리에서 사용하지 않는 객체를 회수하는, 즉 메모리를 회수하는 방식을 채택하는데 이것이 Garbage Collection
이고, 이는 Garbage Collector
를 통해서 수행되는데, 과정은 다음과 같다. 다음과 같다.
1. 힙 내에의 객체중에 가비지를 찾는다.
2. 찾은 가비지를 처리해서, 힙의 메모리를 회수
그러면 가비지 컬렉터는 어떻게 가비지를 찾을 수 있을까?
가비지를 찾는 것 이전에, 객체의 참조 상태를 먼저 살펴보는게 맞는 순서인것 같다.
객체의 참조 상태는 크게 4가지의 상태로 나눌 수 있다.
public class Main {
static Cook staticCook;
public static void main(String[] args) throws IOException {
// 메서드(스태틱) 영역의 변수에 의한 참조
staticCook=new Cook();
// 일반적인 스택 메모리의 스택 프레임의 변수에 대한 참조
Cook cook=new Cook();
// 힙 내의 다른 객체에 의해서 참조
// Person이 생성자에 의해서 참조된다.
Person person=new Person("person1");
}
}
자바에서는 main 메서드로부터 여러 메서드를 호출하면서 동작한다. 이 메서드들이 호출 될 때마다, 메서드 내의 지역변수들과 내부의 메서드들이 스택 프레임의 형태로 올라간다.
위처럼 스택 프레임에 올라간 지역변수가 실제로 힙 메모리에 할당되어서, 참조되는 바로 이것이 이 경우이다.
맨위의 코드중에서는 Cook cook=new Cook();
부분이 해당 될 것이다.
자바에서는 static 변수를 선언하면 메서드 영역에 선언 된다. 이 변수에 객체가 할당되면 당연하게도 힙메모리에 할당이 되는데 이것이 바로 메서드 영역의 변수에 의한 참조이다.
public class Main {
static Cook staticCook;
public static void main(String[] args) throws IOException {
// 메서드(스태틱) 영역의 변수에 의한 참조
staticCook=new Cook();
....
위의 경우가 바로 그 경우다.
이 경우는 힙 메모리에 올라가있는 객체에 의해서 다시 새로 참조되는 경우다.
public class Person {
private String name;
private Cook cook;
// 힙 내의 다른 객체에 의해서 참조
// Person이 생성자에 의해서 참조된다.
public Person(String name){
this.name=name;
this.cook=new Cook();
}
}
위처럼 Person객체가 생성되었을 때, cook객체 역시생성되고, Person객체는 cook객체를 참조한다.
java에서도 더욱 빠른 실행을 해야하는경우(임베디드,ai등)에는 c,c++등의 네이티브 코드를 이용하는 경우가 있는데, 이 때 네이티브와 Java사이에서 인터페이스 역할을 하는 것이, JNI라고한다.
이 때도 네이티브 객체를 생성하고 참조하는 경우가 있다.
public class JniExample {
static {
System.loadLibrary("nativeLib"); // C/C++ 네이티브 라이브러리 로드
}
private long nativeHandle; // 네이티브 객체 참조
public native void createNativeObject(); // C++에서 객체 생성
public static void main(String[] args) {
JniExample example = new JniExample();
example.createNativeObject(); // JNI 메서드 호출
}
}
위의 4가지의 참조중, 힙 메모리에 내부 객체의 참조를 제외한 나머지의 경우는 Rootset이다. Reachable인지를 판단하는데 Rootset은 매우 중요한 역할을 한다.
출처: https://d2.naver.com/helloworld/329631
위처럼 rootset에서 참조되어있지 않은 빨간색 객체들은 접근을 안하는 경우이다. 따라서 GC는 Unreachable
로 객체를 판별하고 이를 회수 대상으로 지정한다.
그렇다면 Unreachable상태는 어떻게 될까?
Cook cook=new Cook();
cook.cooking();
cook=null;
위처럼 스택 프레임의 cook변수가 힙 메모리에 객체를 참조하고 사용후 cook변수를 null로 참조했다. 이 때문에 기존에 참조하던 힙메모리에 올라가있는 객체는 참조가 끊어진다.
따라서 해당 객체에 접근할 수 없고, 이 때문에 gc는 unreachable상태로 판별한다.
public class Main {
public static void main(String[] args) {
createCook(); // (1) createCook() 실행 후 종료됨
System.gc(); // (2) GC 실행 요청
}
public static void createCook() {
Cook cook = new Cook(); // (A) Heap에 Cook 객체 생성
} // (B) 메서드 종료 → cook 변수 사라짐 → Unreachable 상태
}
public class Main {
public static void main(String[] args) {
WeakReference<Cook> weakCook = new WeakReference<>(new Cook()); // (1) WeakReference 사용
cook=null;
System.gc(); // (2) GC 실행 요청
if (weakCook.get() == null) {
System.out.println("Cook 객체가 GC에 의해 제거됨!");
}
}
}
위처럼 사용자가 WeakReference 클래스를 통해서 Reachablity를 weak하게 지정할 수 있다.
Sample대신 Cook Obejct라고 생각해보자
WeakReference
클래스에서 Cook
Object를 참조해서, 위의 코드가 실행될 것이다.
이때, cook=null을 사용하면 아래와 같이 된다.
weak reference
가 아닌 단순히 객체에서 참조하는 경우에는 cook=null
을 한다고 해서 reachablity가 바뀌지 않는다.
하지만 weak reference
에서는 다르다.
위처럼 weak reachable상태로 바꾸고, GC가 동작할 때, WeakReference Object
의 참조를 null로 바꾸어서 두 객체사이의 참조를 끊는다.
따라서 cook
객체는 아무 객체나, Rootset에서 참조하지 않기 때문에, Unreachable
상태로 판별해서, 회수 대상이 된다.
Reachability는 크게 5가지가 존재하며, 이는 GC가 객체를 처리하는 기준이 된다. 이를 Strengths of Reachability
라고 부른다.
strongly reachable
, softly reachable
,
weakly reachable
, phantomly reachable
,unreachable
상태가 있다.
strongly reachable
,weakly reachable
,unreachable
은 앞에서 이미 어느정도 살펴보았으니, 나머지 두가지에 대해서 살펴보자
softly reachable
은 weakly와 다르게, 무조건 GC를 하는 것이 아닌 해당 객체의 사용빈도에 따라서 회수 할지를 결정한다.
이는 자주 사용하지 않는 것이 회수되므로,훨씬 효율적이라고 볼 수 있다.
(마지막 strong reference가 GC된 때로부터 지금까지의 시간)
> (옵션 설정값 N) * (힙에 남아있는 메모리 크기)
softly객체는 위의 수식이 남족하지 않는다면 회수한다.
직관적으로 사용시간이 길수록, 남겨둬야하고, 남은 메모리가 작을수록 회수하는게 맞으니까, 시간에 대한 range가 작아질 수밖에 없으므로, 위 수식을 이해할 수 있다.
Phantomly Reachable
객체를 GC에 의해서 제거하기전에, 특정작업을 수행하고 싶을 때 사용한다.
public static void main(String[] args) {
ReferenceQueue<Resource> queue = new ReferenceQueue<>(); // (1) 팬텀 참조 큐 생성
Resource resource = new Resource();
PhantomReference<Resource> phantomRef = new PhantomReference<>(resource, queue); // (2) 팬텀 참조 생성
resource = null; // (3) Strong Reference 해제
System.gc(); // (4) GC 실행 요청
// 팬텀 참조 대기열에서 제거된 객체 확인
if (queue.poll() != null) {
System.out.println("Phantomly Reachable 상태 감지됨! 객체 정리 가능");
}
}
위처럼 반드시 ReferenceQueue
를 이용해야한다. 왜냐하면 GC대상이지만, 제거가 되면 안되고, 작업을 수행해야하기 때문에, 해당 객체를 저장해둬야하기 때문이다.
다음과 같은 예시에서 사용이 가능하다.
1. 네이티브 메모리 해제 (Direct Memory)
C/C++ 네이티브 메모리를 사용하면, Java의 GC가 이를 자동으로 정리하지 않음.
PhantomReference를 사용하여 GC가 객체를 제거하기 전에 네이티브 메모리를 해제 가능
2. 대형 객체(이미지, 비디오) 캐싱 최적화
대형 객체(예: 이미지, 비디오, AI 모델)를 캐싱할 때, 메모리 부족 시 GC가 객체를 제거해야 함.
객체가 제거되기 직전에 캐시에서 해제할 수 있도록 PhantomReference를 활용 가능
Heap 영역에서 크게 객체를 저장해두는 부분을 Yong Generation
과 Old Generation
으로 나눈다.
만약 모든 객체를 하나의 영역으로 관장한다면, GC를 수행할 때 효율이 떨어질 것이다.
왜냐하면 대부분의 객체는 일시적으로 생성되고, 빠르게 소멸되기 때문에, 오래 사용하는 객체와 함께 같은 영역에 둔다면 GC비용은 올라갈 것이다.
그래서 객체의 생존기간에 따라서 Young
과 Old
로 나누어서 관리 하게 되는 것이다.
Minor GC
라고 한다. 상대적으로 Old 영역에는 GC가 덜 일어나는 객체들이 있으므로, 객체를 계속 보관하고 있어야할 가능성이 높다.
따라서 Young영역에 비해서 크게 메모리가 할당된것은 약간 자명한 이야기이다.
이처럼 Old 영역의 객체가 Young 영역의 객체를 참조하는 경우가 생길것이다.
minor GC는 상대적으로 자주 일어 나므로, 일어나기전의 GC를 해야하는지, 즉 Reachable한지를 판별할 때 만약 위 같은 경우에서는 Old영역을 다 살펴봐야할 것이다.
그렇기에, Old영역에 있는 객체가 Young영역에 있는 객체를 참조할 때마다 Card테이블 형태로 두어서, 관리하여, 바로바로 참조되었는지를 확인 할 수 있다.
Young영역에는 크게 1개의 Eden영역, 2개의 Survivor 영역으로 나뉘고, 이 내부에서 Minor GC가 발생한다.
Minor GC는 아래와 같은 과정으로 일어난다.
1. 객체가 Eden에 생성된다.
2. Eden영역이 꽉차서 Minor Gc가 실행된다.
3. Eden영역에서 Unreachable한 상태의 객체는 메모리를 해제하고, 살아 남은 객체는 1개의 Survivor 영역으로 이동한다.
(기존에 survivor영역에 있던 객체도 같이 이동 시켜서 다른 하나는 비어있도록 유지한다.)-> 다음 Minor GC가 발생했을 때 살아남은 객체를 이동시킬 공간을 확보할 수 있으며, 메모리 단편화를 방지하고 GC의 효율성을 유지할 수 있다.
4. 1~3을 반복해서 특정조건이 되면 Old영역으로 보낸다.
여기서 특정조건이란 각 객체가 이동되면서 age를 부여해주고 이를 ++해주는데, age가 특정 조건 이상을 넘어가는 경우이다.
Major GC는 Old영역의 메모리가 부족해지면 발생한다.
Major GC는 자주일어나지 않는 대신에 일어나면 시간이 엄청 오래걸린다.
영역이 상대적으로 넓기 때문에 당연하다
보통 Young에 비해서 10배이상의 시간이 걸린다고 한다.
위처럼 Reachable한지 아닌지를 판단하는 과정을 Mark
단계이다.
GC Root에서 시작하여 모든 연결된 객체에 마크를 설정한다.
예를 들어, Cook cook = new Cook();
처럼 생성된 객체는 해당 변수나 다른 객체가 참조하고 있다면 마크됨.
위의 사용자가 Reachablity의 정도를 보고, mark를 진행한다.
Mark 단계에서 표시되지 않은(Unmarked) 객체는 더 이상 GC Root로부터 도달할 수 없으므로, Unreachable 객체로 판단된다.
이 단계에서는 Heap 전체를 순회하면서 마크되지 않은 객체들의 메모리를 해제한다.
출처: https://mangkyu.tistory.com/118
Python은 메모리 관리를 자동으로 수행하며, 사용되지 않는 객체를 자동으로 제거하여 메모리 누수를 방지한다. 이 과정은 Garbage Collection(가비지 컬렉션)이라 불리며, Python에서는 주로 참조 카운팅(reference counting)과 순환 참조 감지(cycle detection)를 통해 수행된다.
각 객체는 자신을 참조하는 객체의 개수를 refcount로 유지한다.
참조 수가 0이 되면 즉시 메모리에서 제거된다.
예시
import sys
a = []
print(sys.getrefcount(a)) # 기본적으로 2 이상 (getrefcount 자체가 참조함)
b = a
print(sys.getrefcount(a)) # 참조 수 증가
del b
print(sys.getrefcount(a)) # 참조 수 감소
참고로 위상 정렬이랑 비슷한 형식이다.
보통 refrence counting을 이용하기 때문에 python은 GIL을 사용해서 각 작업당 하나의 쓰레드만 이용할 수 있도록 해야한다.
그래야 정확한 counting이 되기 때문이다.
GC의 신이다