답변:
가비지 콜렉터 는 스택을 스캔하여 현재 스택의 항목이 사용중인 힙의 항목을 확인합니다.
가비지 수집기는 스택이 그렇게 관리되지 않기 때문에 스택 메모리 수집을 고려하는 것은 의미가 없습니다. 스택의 모든 것이 "사용 중"으로 간주됩니다. 그리고 스택에서 사용하는 메모리는 메소드 호출에서 돌아올 때 자동으로 회수됩니다. 스택 공간의 메모리 관리는 매우 간단하고 저렴하며 쉬워서 가비지 수집을 원하지 않습니다.
(스택 프레임은 힙에 저장된 일류 객체이고 다른 모든 객체와 같이 가비지 수집되는 스몰 토크와 같은 시스템이 있습니다. 그러나 요즘 인기있는 접근 방식은 아닙니다. Java의 JVM과 Microsoft의 CLR은 하드웨어 스택과 연속 메모리를 사용합니다. .)
질문을 돌리십시오. 실제 동기 부여 질문은 어떤 상황에서 가비지 수집 비용을 피할 수 있습니까?
음, 첫째, 무엇 이다 가비지 컬렉션의 비용은? 두 가지 주요 비용이 있습니다. 먼저, 당신 은 살아있는 것을 결정해야합니다 . 잠재적으로 많은 작업이 필요합니다. 둘째, 아직 존재하는 두 가지 사이에 할당 된 것을 자유롭게 할 때 형성 되는 구멍 을 압축 해야합니다 . 그 구멍은 낭비입니다. 그러나 그것들을 압축하는 것도 비싸다.
이러한 비용을 어떻게 피할 수 있습니까?
수명이 긴 무언가를 할당 하지 않는 스토리지 사용 패턴을 찾은 다음, 수명이 짧은 것을 할당 한 다음 수명이 긴 무언가를 할당하면 구멍 비용을 제거 할 수 있습니다. 스토리지의 일부 서브 세트에 대해 이후의 모든 할당이 해당 스토리지의 이전 할당보다 수명이 짧다는 것을 보장 할 수있는 경우 해당 스토리지에 아무런 구멍이 없습니다.
그러나 구멍 문제를 해결했다면 가비지 수집 문제도 해결했습니다 . 해당 스토리지에 아직 살아있는 것이 있습니까? 예. 수명이 다하기 전에 모든 것이 할당 되었습니까? 예. 그 가정은 우리가 구멍의 가능성을 없애는 방법입니다. 따라서 "가장 최근 할당이 살아 있습니까?"라고 말하면됩니다. 스토리지에 모든 것이 존재 한다는 것을 알고 있습니다.
모든 후속 할당이 이전 할당보다 수명이 짧다는 것을 알고있는 스토리지 할당 세트가 있습니까? 예! 메소드의 활성화 프레임은 메소드를 작성한 활성화보다 항상 수명이 짧기 때문에 작성된 반대 순서로 항상 소멸됩니다.
따라서 활성화 프레임을 스택에 저장하고 수집 할 필요가 없음을 알 수 있습니다. 스택에 프레임이 있으면 그 아래에있는 전체 프레임 세트가 더 오래 지속되므로 수집 할 필요가 없습니다. 그리고 그들은 창조 된 순서와 반대 순서로 파괴 될 것입니다. 따라서 가비지 수집 비용이 활성화 프레임에서 제거됩니다.
이것이 바로 스택에 임시 풀이있는 이유입니다. 메모리 관리 패널티를 유발하지 않으면 서 메소드 활성화를 쉽게 구현할 수 있기 때문입니다.
(물론 메모리를 수집하는 쓰레기의 비용은 언급 여전히이 활성화 프레임에 참조로.)
이제 활성화 프레임이 예측 가능한 순서로 파괴 되지 않는 제어 흐름 시스템을 고려하십시오 . 단기 활성화가 장기 활성화를 일으킬 수있는 경우 어떻게됩니까? 아시다시피이 세상 에서는 더 이상 스택을 사용하여 활성화를 수집 할 필요성을 최적화 할 수 없습니다. 활성화 세트에는 구멍이 다시 포함될 수 있습니다.
C # 2.0에는이 기능이 형식으로 yield return
있습니다. 수율 리턴을 수행하는 메소드는 나중에 (다음에 MoveNext가 호출 될 때) 다시 활성화되며, 그 시점에 예측할 수 없습니다. 따라서 반복자 블록의 활성화 프레임에 대해 일반적으로 스택에있는 정보는 힙에 저장되며, 여기서 열거자가 수집 될 때 가비지 수집됩니다.
마찬가지로, C # 및 VB의 다음 버전에서 제공되는 "비동기 / 대기"기능을 사용하면 메소드 동작 중에 활성화가 "수율"및 "재개"된 정의 된 지점에서 "수율"및 "재개"하는 메소드를 작성할 수 있습니다. 활성화 프레임이 더 이상 예측 가능한 방식으로 생성 및 소멸되지 않기 때문에 스택에 저장되던 모든 정보는 힙에 저장되어야합니다.
우리가 수십 년 동안 엄격하게 정렬 된 방식으로 생성되고 파괴 된 활성화 프레임을 가진 언어가 유행했다고 결정한 것은 역사의 우연 일뿐입니다. 현대 언어에는이 속성이 점점 더 부족하기 때문에 스택이 아니라 가비지 수집 힙에 연속성을 유지하는 언어가 점점 더 많아 질 것으로 예상합니다.
가장 명백한 대답은 아마도 전체가 아닌 것은 힙이 인스턴스 데이터의 위치라는 것입니다. 인스턴스 데이터는 런타임에 생성되는 클래스 인스턴스 (일명 개체)를 나타내는 데이터를 의미합니다. 이 데이터는 본질적으로 동적이며 이러한 개체의 수와 따라서 차지하는 메모리의 양은 런타임에만 알려져 있습니다. 이 메모리가 약간 아프거나 장시간 실행되는 프로그램은 시간이 지남에 따라 모든 메모리를 소비합니다.
클래스 정의, 상수 및 기타 정적 데이터 구조에 사용되는 메모리는 본질적으로 확인되지 않은 증가 가능성이 없습니다. 해당 클래스의 알 수없는 런타임 인스턴스 당 메모리에 단일 클래스 정의 만 있기 때문에 이러한 유형의 구조는 메모리 사용에 위협이되지 않습니다.
가비지 콜렉션이있는 이유를 기억할 가치가 있습니다. 때로는 메모리 할당 해제시기를 알기가 어렵 기 때문입니다. 실제로 힙에만이 문제가 있습니다. 스택에 할당 된 데이터는 결국 할당이 해제되므로 실제로 가비지 수집을 수행 할 필요가 없습니다. 데이터 섹션의 내용은 일반적으로 프로그램 수명 동안 할당 된 것으로 간주됩니다.
이들의 크기는 예측 가능하며 (스택을 제외하고는 일정하며 스택은 일반적으로 몇 MB로 제한됨) 일반적으로 매우 작습니다 (적어도 수백 MB의 큰 응용 프로그램이 할당 할 수 있음).
동적으로 할당 된 객체는 일반적으로 도달 할 수있는 시간이 짧습니다. 그 후에는 다시 참조 할 수있는 방법이 없습니다. 데이터 섹션의 항목, 전역 변수 및 이와 대조되는 경우가 종종 있습니다. 자주 직접 참조하는 코드가 있습니다 (생각 const char *foo() { return "foo"; }
). 일반적으로 코드는 변경되지 않으므로 참조가 유지되고 함수가 호출 될 때마다 (컴퓨터가 알고있는 한 언제든지 중지 할 수있는 문제를 해결하지 않는 한 언제든지) 다른 참조가 작성됩니다. ). 따라서 항상 도달 할 수 있기 때문에 대부분의 메모리를 비울 수 없었 습니다.
많은 가비지 수집 언어에서 실행중인 프로그램에 속하는 모든 것이 힙 할당됩니다. 파이썬에는 데이터 섹션이 없으며 스택 할당 값이 없습니다 (로컬 변수가 있고 참조가 있고 호출 스택이 있지만 int
C 와 같은 의미의 값은 없습니다 ). 모든 객체는 힙에 있습니다.
다른 많은 응답자들이 말했듯이 스택은 루트 세트의 일부이므로 참조를 검색하지만 "수집"되지는 않습니다.
스택의 가비지가 중요하지 않다는 것을 암시하는 의견에 응답하고 싶습니다. 힙에 더 많은 가비지가 도달 가능한 것으로 간주 될 수 있기 때문입니다. 양심적 인 VM 및 컴파일러 작성자는 null을 처리하지 않거나 스택의 죽은 부분을 스캔에서 제외합니다. IIRC, 일부 VM에는 PC 범위를 스택 슬롯 라이브 비트 맵에 매핑하는 테이블이 있고 다른 VM에는 슬롯이 없습니다. 현재 어떤 기술이 선호되는지 모르겠습니다.
이 특정 고려 사항을 설명하는 데 사용되는 용어는 공간 안전 입니다.
당신과 다른 많은 사람들이 잘못 알고있는 몇 가지 근본적인 오해를 지적하겠습니다 :
"가비지 수집은 왜 힙만 쓸어 버리나요?" 그것은 다른 길입니다. 가장 단순하고 가장 보수적이며 느린 가비지 수집기 만 힙을 청소합니다. 그래서 그들은 너무 느립니다.
빠른 가비지 수집기는 스택 (및 선택적으로 FFI 포인터의 일부 전역 포인터 및 라이브 포인터의 레지스터와 같은 다른 루트)을 스윕하고 스택 개체가 도달 할 수있는 포인터 만 복사합니다. 나머지는 힙에서 전혀 스캔하지 않고 버려집니다 (즉 무시 됨).
힙이 스택보다 약 1000 배 크기 때문에 이러한 스택 스캔 GC는 일반적으로 훨씬 빠릅니다. 일반 크기의 힙에서 ~ 15ms 대 250ms 객체를 한 공간에서 다른 공간으로 복사 (이동)하기 때문에 대부분 반 공간 복사 수집기라고합니다 .2x 메모리가 필요하므로 메모리가 많지 않은 전화와 같은 매우 작은 장치에서는 사용할 수 없습니다. 압축 방식이므로 간단한 마크 앤 스윕 힙 스캐너와 달리 캐시 친화적 인 라테 온입니다.
움직이는 포인터이므로 FFI, ID 및 참조는 까다 롭습니다. 신원은 일반적으로 임의의 ID, 전달 포인터를 통한 참조로 해결됩니다. FFI는 까다 롭습니다. 이물질은 오래된 공간에 대한 포인터를 뒤로 유지할 수 없기 때문입니다. FFI 포인터는 일반적으로 느린 마크 앤 스윕, 정적 수집기와 같은 별도의 힙 아레나에서 유지됩니다. 또는 사소한 사소한 malloc. malloc은 엄청난 오버 헤드를 가지고 있으며 훨씬 더 많은 것을 고려합니다.
Mark & Sweep은 구현하기가 쉽지 않지만 실제 프로그램에서는 사용해서는 안되며 특히 표준 수집기로 가르쳐서는 안됩니다. 이러한 빠른 스택 스캔 복사 수집기 중 가장 유명한 것은 Cheney Two-finger 수집기 입니다.
스택에 할당 된 것은 무엇입니까? 지역 변수 및 반환 주소 (C) 함수가 반환되면 로컬 변수가 삭제됩니다. 스택을 스윕하는 것이 필요하지 않고 심지어 해 롭습니다.
많은 동적 언어와 Java 또는 C #은 종종 C로 시스템 프로그래밍 언어로 구현됩니다. Java가 C 함수로 구현되고 C 로컬 변수를 사용하므로 Java의 가비지 콜렉터가 스택을 스윕 할 필요가 없다고 말할 수 있습니다.
흥미로운 예외가 있습니다. Chicken Scheme의 가비지 수집기 는 스택을 가비지 수집 1 세대 공간으로 사용하기 때문에 스택을 스윕 합니다 ( Chicken Scheme Design Wikipedia 참조) .