가비지 콜렉션은 왜 힙을 스윕합니까?


28

기본적으로, 지금까지 가비지 수집이 현재 가리키고 있지 않은 데이터 구조를 영원히 지우는 것을 배웠습니다. 그러나 이것은 그러한 조건에 대해서만 힙을 검사합니다.

왜 데이터 섹션 (글로벌, 상수 등)이나 스택도 확인하지 않습니까? 가비지 수집을 원하는 유일한 힙은 무엇입니까?


21
"더 많이 쓸다"는 "더 많이 쌓다"보다 안전합니다 ... :-)
Brian Knoblauch

답변:


62

가비지 콜렉터 스택을 스캔하여 현재 스택의 항목이 사용중인 힙의 항목을 확인합니다.

가비지 수집기는 스택이 그렇게 관리되지 않기 때문에 스택 메모리 수집을 고려하는 것은 의미가 없습니다. 스택의 모든 것이 "사용 중"으로 간주됩니다. 그리고 스택에서 사용하는 메모리는 메소드 호출에서 돌아올 때 자동으로 회수됩니다. 스택 공간의 메모리 관리는 매우 간단하고 저렴하며 쉬워서 가비지 수집을 원하지 않습니다.

(스택 프레임은 힙에 저장된 일류 객체이고 다른 모든 객체와 같이 가비지 수집되는 스몰 토크와 같은 시스템이 있습니다. 그러나 요즘 인기있는 접근 방식은 아닙니다. Java의 JVM과 Microsoft의 CLR은 하드웨어 스택과 연속 메모리를 사용합니다. .)


7
+1 스택은 항상 완전히 도달 할 수 있으므로 쓸데없는 감각입니다.
ratchet freak

2
+1 감사합니다. 정답을 맞추기 위해 4 개의 게시물을 작성했습니다. 당신은 스택에 모든 사용중인 것으로 "간주"되는 말을했다 왜 나는 몰라 입니다 사용중인 힙이 아직 사용 객체로 적어도 강한 감각으로 사용 -하지만 그건의 실제 nitpick입니다 아주 좋은 답변입니다.
psr

@psr은 스택에있는 모든 것들에 강력하게 접근 할 수 있고 메소드가 리턴 될 때까지 수집 할 필요가 없지만 (RAII)는 이미 명시 적으로 관리된다는 것을 의미합니다.
ratchet freak

@ratchetfreak-알아요 그리고 나는 단지 "고려 된"이라는 단어가 필요하지 않다는 것을 의미했습니다. 그것없이 더 강한 진술을하는 것은 괜찮습니다.
psr

5
@psr : 동의하지 않습니다. "사용중인 것으로 간주 "는 매우 중요한 이유로 스택과 힙 모두에 대해 더 정확합니다. 당신이 원하는 것은 다시 사용되지 않는 것을 버리는 것입니다. 당신이하는 일은 도달 할 수 없는 것을 버리는 것입니다 . 필요하지 않은 도달 가능한 데이터가있을 수 있습니다. 이 데이터가 커지면 메모리 누수가 발생합니다 (예, 많은 사람들이 생각하는 것과 달리 GC 언어로도 가능합니다). 그리고 테일 리커버리 프로그램에서 필요없는 스택 프레임 (예 : JVM)없이 실행되는 스택 리크도 스택 누수가 발생한다고 주장 할 수 있습니다.
Blaisorblade

19

질문을 돌리십시오. 실제 동기 부여 질문은 어떤 상황에서 가비지 수집 비용을 피할 수 있습니까?

음, 첫째, 무엇 이다 가비지 컬렉션의 비용은? 두 가지 주요 비용이 있습니다. 먼저, 당신 은 살아있는 것을 결정해야합니다 . 잠재적으로 많은 작업이 필요합니다. 둘째, 아직 존재하는 두 가지 사이에 할당 된 것을 자유롭게 할 때 형성 되는 구멍압축 해야합니다 . 그 구멍은 낭비입니다. 그러나 그것들을 압축하는 것도 비싸다.

이러한 비용을 어떻게 피할 수 있습니까?

수명이 긴 무언가를 할당 하지 않는 스토리지 사용 패턴을 찾은 다음, 수명이 짧은 것을 할당 한 다음 수명이 긴 무언가를 할당하면 구멍 비용을 제거 할 수 있습니다. 스토리지의 일부 서브 세트에 대해 이후의 모든 할당이 해당 스토리지의 이전 할당보다 수명이 짧다는 것을 보장 할 수있는 경우 해당 스토리지에 아무런 구멍이 없습니다.

그러나 구멍 문제를 해결했다면 가비지 수집 문제도 해결했습니다 . 해당 스토리지에 아직 살아있는 것이 있습니까? 예. 수명이 다하기 전에 모든 것이 할당 되었습니까? 예. 그 가정은 우리가 구멍의 가능성을 없애는 방법입니다. 따라서 "가장 최근 할당이 살아 있습니까?"라고 말하면됩니다. 스토리지에 모든 것이 존재 한다는 것을 알고 있습니다.

모든 후속 할당이 이전 할당보다 수명이 짧다는 것을 알고있는 스토리지 할당 세트가 있습니까? 예! 메소드의 활성화 프레임은 메소드를 작성한 활성화보다 항상 수명이 짧기 때문에 작성된 반대 순서로 항상 소멸됩니다.

따라서 활성화 프레임을 스택에 저장하고 수집 할 필요가 없음을 알 수 있습니다. 스택에 프레임이 있으면 그 아래에있는 전체 프레임 세트가 더 오래 지속되므로 수집 할 필요가 없습니다. 그리고 그들은 창조 된 순서와 반대 순서로 파괴 될 것입니다. 따라서 가비지 수집 비용이 활성화 프레임에서 제거됩니다.

이것이 바로 스택에 임시 풀이있는 이유입니다. 메모리 관리 패널티를 유발하지 않으면 서 메소드 활성화를 쉽게 구현할 수 있기 때문입니다.

(물론 메모리를 수집하는 쓰레기의 비용은 언급 여전히이 활성화 프레임에 참조로.)

이제 활성화 프레임이 예측 가능한 순서로 파괴 되지 않는 제어 흐름 시스템을 고려하십시오 . 단기 활성화가 장기 활성화를 일으킬 수있는 경우 어떻게됩니까? 아시다시피이 세상 에서는 더 이상 스택을 사용하여 활성화를 수집 할 필요성을 최적화 할 수 없습니다. 활성화 세트에는 구멍이 다시 포함될 수 있습니다.

C # 2.0에는이 기능이 형식으로 yield return있습니다. 수율 리턴을 수행하는 메소드는 나중에 (다음에 MoveNext가 호출 될 때) 다시 활성화되며, 그 시점에 예측할 수 없습니다. 따라서 반복자 블록의 활성화 프레임에 대해 일반적으로 스택에있는 정보는 힙에 저장되며, 여기서 열거자가 수집 될 때 가비지 수집됩니다.

마찬가지로, C # 및 VB의 다음 버전에서 제공되는 "비동기 / 대기"기능을 사용하면 메소드 동작 중에 활성화가 "수율"및 "재개"된 정의 된 지점에서 "수율"및 "재개"하는 메소드를 작성할 수 있습니다. 활성화 프레임이 더 이상 예측 가능한 방식으로 생성 및 소멸되지 않기 때문에 스택에 저장되던 모든 정보는 힙에 저장되어야합니다.

우리가 수십 년 동안 엄격하게 정렬 된 방식으로 생성되고 파괴 된 활성화 프레임을 가진 언어가 유행했다고 결정한 것은 역사의 우연 일뿐입니다. 현대 언어에는이 속성이 점점 더 부족하기 때문에 스택이 아니라 가비지 수집 힙에 연속성을 유지하는 언어가 점점 더 많아 질 것으로 예상합니다.


13

가장 명백한 대답은 아마도 전체가 아닌 것은 힙이 인스턴스 데이터의 위치라는 것입니다. 인스턴스 데이터는 런타임에 생성되는 클래스 인스턴스 (일명 개체)를 나타내는 데이터를 의미합니다. 이 데이터는 본질적으로 동적이며 이러한 개체의 수와 따라서 차지하는 메모리의 양은 런타임에만 알려져 있습니다. 이 메모리가 약간 아프거나 장시간 실행되는 프로그램은 시간이 지남에 따라 모든 메모리를 소비합니다.

클래스 정의, 상수 및 기타 정적 데이터 구조에 사용되는 메모리는 본질적으로 확인되지 않은 증가 가능성이 없습니다. 해당 클래스의 알 수없는 런타임 인스턴스 당 메모리에 단일 클래스 정의 만 있기 때문에 이러한 유형의 구조는 메모리 사용에 위협이되지 않습니다.


5
그러나 힙은 "인스턴스 데이터"의 위치가 아닙니다. 그들은 스택에도있을 수 있습니다.
svick

@svick 물론 언어에 따라 다릅니다. Java는 힙 할당 객체 만 지원하며 Vala는 힙 할당 (클래스)과 스택 할당 (struct)을 명확하게 구분합니다.
푹신한

1
@fluffy : 언어가 매우 제한적이므로 언어가 정확하지 않기 때문에 일반적으로 사용한다고 가정 할 수 없습니다.
Matthieu M.

@MatthieuM. 그건 내 요점이었다.
푹신한

@fluffy : 왜 클래스는 힙에 할당되고 구조체는 스택에 할당됩니까?
Dark Templar

10

가비지 콜렉션이있는 이유를 기억할 가치가 있습니다. 때로는 메모리 할당 해제시기를 알기가 어렵 기 때문입니다. 실제로 힙에만이 문제가 있습니다. 스택에 할당 된 데이터는 결국 할당이 해제되므로 실제로 가비지 수집을 수행 할 필요가 없습니다. 데이터 섹션의 내용은 일반적으로 프로그램 수명 동안 할당 된 것으로 간주됩니다.


1
그것은 '결국 적으로'할당 해제 될뿐만 아니라 적시에 할당 해제 될 것입니다.
Boris Yankov

3
  1. 이들의 크기는 예측 가능하며 (스택을 제외하고는 일정하며 스택은 일반적으로 몇 MB로 제한됨) 일반적으로 매우 작습니다 (적어도 수백 MB의 큰 응용 프로그램이 할당 할 수 있음).

  2. 동적으로 할당 된 객체는 일반적으로 도달 할 수있는 시간이 짧습니다. 그 후에는 다시 참조 할 수있는 방법이 없습니다. 데이터 섹션의 항목, 전역 변수 및 이와 대조되는 경우가 종종 있습니다. 자주 직접 참조하는 코드가 있습니다 (생각 const char *foo() { return "foo"; }). 일반적으로 코드는 변경되지 않으므로 참조가 유지되고 함수가 호출 될 때마다 (컴퓨터가 알고있는 한 언제든지 중지 할 수있는 문제를 해결하지 않는 한 언제든지) 다른 참조가 작성됩니다. ). 따라서 항상 도달 할 수 있기 때문에 대부분의 메모리를 비울 수 없었 습니다.

  3. 많은 가비지 수집 언어에서 실행중인 프로그램에 속하는 모든 것이 힙 할당됩니다. 파이썬에는 데이터 섹션이 없으며 스택 할당 값이 없습니다 (로컬 변수가 있고 참조가 있고 호출 스택이 있지만 intC 와 같은 의미의 값은 없습니다 ). 모든 객체는 힙에 있습니다.


"Python에는 데이터 섹션이 없습니다." 이것은 엄격하게 사실이 아닙니다. None, True 및 False는 내가 이해하는대로 데이터 섹션에 할당됩니다. stackoverflow.com/questions/7681786/how-is-hashnone-Calculated
Jason Baker

@JasonBaker : 재미있는 발견! 그래도 효과는 없습니다. 구현 세부 사항이며 내장 객체로 제한됩니다. 즉, 그 객체가 해제되지 않을 것으로 예상된다 언급 아니다 이제까지 어쨌든 프로그램의 수명에, 아니다, 또한 크기가 작다 (각 바이트 미만 32, 나는 추측에는 요).

@delnan Eric Lippert는 대부분의 언어에서 스택과 힙에 대한 별도의 메모리 영역이 구현 세부 사항임을 지적하는 것을 좋아합니다. 스택을 전혀 사용하지 않고도 대부분의 언어를 구현할 수 있지만 (실제로 성능이 저하 될 수 있지만) 여전히 해당 사양을 준수해야합니다
Jules

2

다른 많은 응답자들이 말했듯이 스택은 루트 세트의 일부이므로 참조를 검색하지만 "수집"되지는 않습니다.

스택의 가비지가 중요하지 않다는 것을 암시하는 의견에 응답하고 싶습니다. 힙에 더 많은 가비지가 도달 가능한 것으로 간주 될 수 있기 때문입니다. 양심적 인 VM 및 컴파일러 작성자는 null을 처리하지 않거나 스택의 죽은 부분을 스캔에서 제외합니다. IIRC, 일부 VM에는 PC 범위를 스택 슬롯 라이브 비트 맵에 매핑하는 테이블이 있고 다른 VM에는 슬롯이 없습니다. 현재 어떤 기술이 선호되는지 모르겠습니다.

이 특정 고려 사항을 설명하는 데 사용되는 용어는 공간 안전 입니다.


알고 흥미로울 것입니다. 첫 번째 생각은 공백을 없애는 것이 가장 현실적이라는 것입니다. 제외 된 영역의 트리를 순회하면 널을 통한 스캔보다 시간이 오래 걸릴 수 있습니다. 분명히 스택을 압축하려는 시도는 위험에 처해 있습니다! 그 일을하는 것은 마음을 구부리거나 오류가 발생하기 쉬운 과정처럼 들립니다.
Brian Knoblauch

@Brian, 사실, 그것에 대해 좀 더 생각하면, 유형이 지정된 VM의 경우 어쨌든 이와 같은 것이 필요하므로 정수, 부동 소수점과는 대조적으로 참조하는 슬롯을 결정할 수 있습니다. 또한 스택 압축에 대해서는 "CONS should 헨리 베이커의 논증은 아니다.
Ryan Culpepper

슬롯 유형을 결정하고 적절하게 사용되는지 확인하는 것은 컴파일 타임 (신뢰할 수있는 바이트 코드를 사용하는 VM의 경우) 또는로드 시간 (바이트 코드가 신뢰할 수없는 소스 (예 : Java)에서 온 경우)에서 정적으로 수행 될 수 있고 일반적으로 수행됩니다.
Jules

1

당신과 다른 많은 사람들이 잘못 알고있는 몇 가지 근본적인 오해를 지적하겠습니다 :

"가비지 수집은 왜 힙만 쓸어 버리나요?" 그것은 다른 길입니다. 가장 단순하고 가장 보수적이며 느린 가비지 수집기 만 힙을 청소합니다. 그래서 그들은 너무 느립니다.

빠른 가비지 수집기는 스택 (및 선택적으로 FFI 포인터의 일부 전역 포인터 및 라이브 포인터의 레지스터와 같은 다른 루트)을 스윕하고 스택 개체가 도달 할 수있는 포인터 만 복사합니다. 나머지는 힙에서 전혀 스캔하지 않고 버려집니다 (즉 무시 됨).

힙이 스택보다 약 1000 배 크기 때문에 이러한 스택 스캔 GC는 일반적으로 훨씬 빠릅니다. 일반 크기의 힙에서 ~ 15ms 대 250ms 객체를 한 공간에서 다른 공간으로 복사 (이동)하기 때문에 대부분 반 공간 복사 수집기라고합니다 .2x 메모리가 필요하므로 메모리가 많지 않은 전화와 같은 매우 작은 장치에서는 사용할 수 없습니다. 압축 방식이므로 간단한 마크 앤 스윕 힙 스캐너와 달리 캐시 친화적 인 라테 온입니다.

움직이는 포인터이므로 FFI, ID 및 참조는 까다 롭습니다. 신원은 일반적으로 임의의 ID, 전달 포인터를 통한 참조로 해결됩니다. FFI는 까다 롭습니다. 이물질은 오래된 공간에 대한 포인터를 뒤로 유지할 수 없기 때문입니다. FFI 포인터는 일반적으로 느린 마크 앤 스윕, 정적 수집기와 같은 별도의 힙 아레나에서 유지됩니다. 또는 사소한 사소한 malloc. malloc은 엄청난 오버 헤드를 가지고 있으며 훨씬 더 많은 것을 고려합니다.

Mark & ​​Sweep은 구현하기가 쉽지 않지만 실제 프로그램에서는 사용해서는 안되며 특히 표준 수집기로 가르쳐서는 안됩니다. 이러한 빠른 스택 스캔 복사 수집기 중 가장 유명한 것은 Cheney Two-finger 수집기 입니다.


문제는 특정 가비지 수집 알고리즘이 아닌 메모리의 어떤 부분이 가비지 수집되는지에 관한 것 같습니다. 마지막 문장은 특히 OP가 가비지 수집을 구현하는 특정 메커니즘이 아니라 "가비지 수집"의 일반 동의어로 "스위프"를 사용하고 있음을 나타냅니다. 이를 고려할 때, 가장 간단한 가비지 수집기 만 가비지를 수집하고, 가비지 수집기 대신 가비지 수집기가 스택과 정적 메모리를 수집하여 메모리가 부족할 때까지 힙을 늘리고 늘리는 것으로 대답합니다.
8bittree

아니요, 그 질문은 매우 구체적이고 영리했습니다. 대답은 그렇지 않습니다. Slow mark & ​​sweep GC에는 스택의 루트를 스캔하는 마크 단계와 힙을 스캔하는 스윕 단계의 두 단계가 있습니다. 빠른 복사 GC에는 스택을 스캔하는 단 하나의 단계 만 있습니다. 그렇게 쉬운. 적절한 가비지 수집기에 대해 아무도 여기에서 알지 못하기 때문에 질문에 대답해야합니다. 당신의 해석은 크게 벗어났습니다.
rurban

0

스택에 할당 된 것은 무엇입니까? 지역 변수 및 반환 주소 (C) 함수가 반환되면 로컬 변수가 삭제됩니다. 스택을 스윕하는 것이 필요하지 않고 심지어 해 롭습니다.

많은 동적 언어와 Java 또는 C #은 종종 C로 시스템 프로그래밍 언어로 구현됩니다. Java가 C 함수로 구현되고 C 로컬 변수를 사용하므로 Java의 가비지 콜렉터가 스택을 스윕 할 필요가 없다고 말할 수 있습니다.

흥미로운 예외가 있습니다. Chicken Scheme의 가비지 수집기 스택을 가비지 수집 1 세대 공간으로 사용하기 때문에 스택을 스윕 합니다 ( Chicken Scheme Design Wikipedia 참조) .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.