Чому збирання сміття лише підмітає купу?


28

По суті, я до цього часу дізнався, що збирання сміття назавжди видаляє будь-яку структуру даних, на яку зараз не вказано. Але це лише перевіряє купу на такі умови.

Чому він також не перевіряє розділ даних (глобалі, константи тощо) та стек? Що це за купа, що це єдине, що ми хочемо, щоб сміття збирали?


21
"підмітати купу" безпечніше, ніж "ударити стек" ... :-)
Брайан Ноблеуч

Відповіді:


62

Збирач сміття виконує сканування стека - щоб побачити, які речі в купі використовуються (вказують) на речі, що знаходяться на стеку.

Збирач сміття не має сенсу розглянути можливість збирання пам’яті стека, оскільки таким чином керувати не є таким чином: Все, що знаходиться в стеці, вважається «використаним». І пам'ять, що використовується стеком, автоматично відновлюється при поверненні з викликів методів. Управління пам’яттю простору стека настільки просте, дешеве і просте, що ви не хочете займатися збиранням сміття.

(Існують такі системи, як smalltalk, де кадри стека - це першокласні об'єкти, що зберігаються в купі та зібраному смітті, як і всі інші об'єкти. Але це не популярний підхід в наші дні. JVM Java і CLR Microsoft використовують апаратний стек і суміжну пам'ять .)


7
+1 стек завжди доступний, тому немає сенсу його підмітати
щурячий вирод

2
+1 дякую, взяв 4 повідомлення, щоб відповісти правильну відповідь. Я не знаю , чому ви повинні були сказати , що все в стеці «вважається» , щоб бути в використанні, то є при використанні його по крайней мере, сильного почуття , як купа об'єктів до сих пір використовується в використанні - але це реальна чіплятися з дуже гарна відповідь.
psr

@psr він означає, що все в стеці є сильно доступним і не потрібно збирати, поки метод не повернеться, але це (RAII) вже явно керовано
ratchet freak

@ratchetfreak - я знаю. І я просто мав на увазі, що слово "вважається", ймовірно, не потрібне, добре робити більш чіткі заяви без нього.
psr

5
@psr: Я не згоден. " Вважається, що використовується" є більш правильним як для стека, так і для купи, з дуже важливих причин. Те, що ви хочете, - відмовитися від того, що більше не буде використано; що ви робите, це те, що ви відкидаєте те, що недосяжно . Ви можете мати доступні дані, які вам ніколи не знадобляться; коли ці дані зростають, у вас є витік пам'яті (так, вони можливі навіть на мовах GC'ed, на відміну від багатьох людей). І можна стверджувати, що витоки стека трапляються також, і найпоширенішим прикладом є непотрібні кадри стека в хвостово-рекурсивних програмах, що виконуються без усунення хвостових викликів (наприклад, на JVM).
Blaisorblade

19

Поверніть своє запитання. Справжнє мотивуючий питання полягає в тому, за яких обставин ми можемо уникнути витрат на вивезення сміття?

Ну, по- перше, те , що є витрати на збір сміття? Є дві основні витрати. По-перше, ви повинні визначити, що живе ; що вимагає потенційно багато роботи. По-друге, ви повинні ущільнити отвори , які утворюються, коли ви звільните щось, що було виділено між двома речами, які ще живі. Ці діри марнотратні. Але ущільнення їх теж дороге.

Як ми можемо уникнути цих витрат?

Зрозуміло, що якщо ви можете знайти схему використання сховища, в якій ніколи не виділяєте щось довготривале, потім виділяєте щось недовговічне, потім виділяєте щось довговічне, ви можете усунути вартість дірок. Якщо ви можете гарантувати, що для деякого підмножини вашої пам’яті кожен наступний розподіл буде коротшим, ніж попередній у цьому сховищі, то в цьому сховищі ніколи не буде жодних дірок.

Але якщо ми вирішили проблему з дірочками, то і ми вирішили проблему з вивезенням сміття . У вас є щось у тому сховищі, яке ще живе? Так. Чи було все виділено до того, як воно довгожило? Так - це припущення - це те, як ми усунули можливість дір. Тому все, що вам потрібно зробити, - сказати, "чи є останнім виділення в живих?" і ти знаєш, що в цьому сховищі все живе.

Чи є у нас набір асигнувань на зберігання, де ми знаємо, що кожне наступне виділення є короткочасним, ніж попереднє? Так! Кадри активації методів завжди руйнуються в тому зворотному порядку, як вони були створені, оскільки вони завжди короткочасні, ніж активація, яка їх створила.

Тому ми можемо зберігати рамки активації на стеку і знати, що їх ніколи не потрібно збирати. Якщо на стеці є якийсь кадр, весь набір фреймів під ним є довговічнішим, тому їх не потрібно збирати. І вони будуть знищені в тому зворотному порядку, як вони були створені. Таким чином, вартість вивезення сміття виключається для кадрів активації.

Ось чому ми маємо тимчасовий пул на стеці в першу чергу: тому що це простий спосіб здійснення активації методу без стягнення штрафу за управління пам'яттю.

(Звичайно, вартість сміття для збору пам'яті , на яку посилаються посилання на кадри активації, все ще є.)

Тепер розглянемо систему потоків управління, в якій кадри активації не руйнуються в передбачуваному порядку. Що станеться, якщо короткотривала активація може призвести до довготривалої активації? Як ви можете собі уявити, у цьому світі ви більше не можете використовувати стек, щоб оптимізувати необхідність збирання активацій. Набір активацій може знову містити отвори.

C # 2.0 має цю функцію у вигляді yield return. Метод, який робить прибутковість, буде активовано пізніше - наступного разу, коли викликається MoveNext, - і коли це станеться, не передбачувано. Тому інформація, яка зазвичай знаходиться на стеку для активаційного кадру ітераторного блоку, замість цього зберігається на купі, де вона збирається сміттям під час збирання нумератора.

Аналогічно, функція "асинхронізування / очікування", що надходить у наступних версіях C # і VB, дозволить вам створити методи, активації яких "поступаються" та "поновлюються" у чітко визначених точках під час дії методу. Оскільки кадри активації більше не створюються та не руйнуються передбачувано, вся інформація, яка раніше зберігалася в стеку, повинна зберігатися в купі.

Це просто випадковий випадок історії, що ми випадково вирішили на кілька десятиліть, що мови з кадрами активації, які створені та знищені суворо впорядкованим чином, були модними. Оскільки сучасним мовам дедалі більше не вистачає цього властивості, сподівайтеся побачити все більше мов, які спрямовують продовження на збирання сміття, а не на стек.


13

Найбільш очевидною відповіддю, і, можливо, не найбільш повною, є те, що купа - це розташування даних екземплярів. Під даними екземпляра ми маємо на увазі дані, що представляють екземпляри класів, ака-об'єкти, які створюються під час виконання. Ці дані за своєю суттю динамічні, і кількість цих об'єктів, а отже, і обсяг пам'яті, яку вони займають, відомий лише під час виконання. ЗНАЙДАЛО деяка біль у відновленні цієї пам'яті або тривалі програми, що споживають з часом, споживають усю пам'ять.

Пам'ять, яку споживають визначення класів, константи та інші статичні структури даних, по суті, навряд чи збільшиться без перевірки. Оскільки в пам'яті існує лише одне визначення класу на невідому кількість екземплярів цього часу цього класу, має сенс, що такий тип структури не загрожує використанню пам'яті.


5
Але купа - це не місце “даних про екземпляр”. Вони можуть бути і на стеці.
svick

@svick, звичайно, залежить від мови. Java підтримує лише об'єкти, виділені купою, і Vala досить чітко розмежовує між розподіленими купою (класом) і розподіленими стеками (структура).
пухнастий

1
@fluffy: це дуже обмежені мови, ви не можете припустити, що це взагалі справедливо, оскільки жодна мова не була чітко визначеною.
Матьє М.

@MatthieuM. Це було свого роду моєю точкою.
пухнастий

@fluffy: так чому класи виділяються в купі, а структури виділяються в стеці?
Темний тамплієр

10

Варто мати на увазі причину, по якій ми збираємо сміття: адже іноді важко знати, коли розбирати пам'ять. У вас справді є лише ця проблема з купою. Дані, виділені у стеку, з часом будуть розміщені, тому насправді там не потрібно робити збирання сміття. Речі в розділі даних, як правило, вважаються розподіленими протягом життя програми.


1
Він не тільки буде розміщений "зрештою", але він буде розміщений в потрібний час.
Борис Янков

3
  1. Розмір цих даних передбачуваний (постійний, крім стека, і стек, як правило, обмежений кількома МБ) і, як правило, дуже малий (принаймні порівняно із сотнями МБ, які можуть виділяти великі програми).

  2. Динамічно виділені об'єкти, як правило, мають невеликий часовий проміжок, в який вони доступні. Після цього немає жодного способу на них можна посилатися. На відміну від записів у розділі даних, глобальних змінних тощо: Часто є фрагмент коду, який безпосередньо посилається на них (подумайте const char *foo() { return "foo"; }). Зазвичай код не змінюється, тому посилання залишається на тому, щоб залишитися, і інша посилання створюватиметься кожного разу, коли функція буде викликана (що може бути в будь-який момент, наскільки знає комп'ютер - якщо ви не вирішите проблему зупинки, тобто ). Таким чином, ви не могли звільнити більшу частину цієї пам'яті, так як вона завжди була б доступною.

  3. У багатьох мовах, зібраних сміттям, все, що належить програмі, що ведеться, виділяється купою. У Python просто немає жодного розділу даних і не виділених стеків значень (є посилання, що є локальними змінними, і є стек викликів, але жодне значення не має в тому ж сенсі, що і intв C). Кожен об’єкт знаходиться на купі.


"У Python просто немає жодного розділу даних". Це не зовсім суто так. Ні, істинно, і помилково виділено в розділі даних, наскільки я це розумію: stackoverflow.com/questions/7681786/how-is-hashnone-calculated
Джейсон Бейкер

@JasonBaker: Цікава знахідка! Це не має жодного ефекту. Це деталізація реалізації та обмежена вбудованими об'єктами. Це не кажучи вже про те, що не очікується, що ці об’єкти будуть переміщені ніколи протягом життя програми, ні, і вони також невеликі за розміром (менше 32 байт, я думаю,).

@delnan Як Ерік Ліпперт любить вказувати, для більшості мов існування окремих областей пам’яті для стека та купи є деталлю реалізації. Ви можете реалізовувати більшість мов, не використовуючи стек взагалі (хоча продуктивність може постраждати, якщо ви зробите це), і все ж відповідати їх технічним умовам
Jules

2

Як сказала низка інших відповідей, стек є частиною кореневого набору, тому він сканується на посилання, але сам по собі не "збирається".

Я просто хочу відповісти на деякі зауваження, які означають, що сміття у групі не має значення; це відбувається, тому що це може призвести до того, що більше сміття на купі вважатиметься доступним. Добросовісні автори VM та компіляторів або скасовують нуль, або іншим чином виключають мертві частини стека зі сканування. IIRC, деякі VM мають таблиці, що відображають діапазони ПК на бітові карти стека-слотів, а інші просто скасують слоти. Я не знаю, яку техніку в даний час надають перевагу.

Один термін, який використовується для опису цього конкретного питання, є безпечним для простору .


Було б цікаво знати. По-перше, думка полягає в тому, що видалення пробілів є найбільш реалістичним. Подорож по дереву виключених ділянок може зайняти більше часу, ніж просто сканування нулів. Очевидно, будь-яка спроба ущільнення стека загрожує небезпекою! Зробити цю роботу звучить як процес згинання / схильності до помилок.
Брайан Кноблаух

@Brian, Насправді, обмірковуючи це ще трохи, для введеного VM вам все одно потрібно щось подібне, тож ви можете визначити, які слоти є посиланнями на відміну від цілих чисел, плавців тощо. Також, щодо ущільнення стека, див. "CONS НЕ ПОВЕРНУЄ свої аргументи "Генрі Бейкер.
Райан Калпеппер

Визначення типів слотів та перевірка їх належного використання може і, як правило, проводиться статично, або під час компіляції (для віртуальних машин, що використовують надійний байт-код), або час завантаження (де байт-код походить від недовіреного джерела, наприклад, Java).
Жуль

1

Дозвольте зазначити кілька основних помилок, з якими ви та багато інших помилялися:

"Чому збирання сміття лише підмітає купу?" Це навпаки. Лише найпростіші, найконсервативніші та найповільніші збирачі сміття підмітають купу. Ось чому вони такі повільні.

Швидкі збирачі сміття лише підмітають стек (і, за бажанням, деякі інші корені, як-от глобали для покажчиків FFI та регістри для живих покажчиків), і лише копіюють покажчики, доступні об'єктами стека. Решту викидають (тобто ігнорують), зовсім не скануючи на купі.

Оскільки купа приблизно в 1000 разів більша, ніж стек (и), такий GC-сканування стіків, як правило, набагато швидше. ~ 15мс проти 250мс на звичайних розмірах. Оскільки це копіювання (переміщення) об'єктів з одного простору в інший, його в основному називають напівпростірним колектором копіювання, йому потрібна 2x пам’ять і тому в основному не можна використовувати на дуже маленьких пристроях, любить телефони з не великою кількістю пам'яті. Він ущільнюється, тож це дуже привабливий кеш-пам'ять, на відміну від простих сканерів з маркуванням та зачисткою.

Оскільки це рухаються покажчики, FFI, посвідчення особи та посилання є складними. Ідентичність зазвичай вирішується випадковими ідентифікаторами, посиланнями через вказівники переадресації. FFI є складним, оскільки сторонні об'єкти не можуть стримувати покажчики до старого простору. Покажчики FFI, як правило, зберігаються на окремій арені купи, наприклад, з повільним позначенням та змітанням, статичним колектором. Або банальний малок з відмовою. Зауважте, що malloc має величезні накладні витрати, а відплата ще більше.

Mark & ​​sweep є тривіальним для впровадження, але він не повинен використовуватися в реальних програмах і особливо не повинен викладатися як стандартний колектор. Найвідоміший з таких швидких копіювальних копіювальних колекціонерів називається колектором з двома пальцями Cheney .


Питання, швидше за все, стосується того, які частини пам'яті збирають сміття, а не конкретних алгоритмів збору сміття. В останньому реченні особливо випливає, що ОП використовує "підмітання" як загальний синонім "збирання сміття", а не конкретний механізм здійснення збору сміття. Враховуючи це, ваша відповідь наштовхується на те, що лише найпростіші сміттєзбірники збирають купу, а швидкі збирачі сміття замість сміття збирають стек і статичну пам’ять, залишаючи купу рости і рости, поки не вичерпається пам'ять.
8bittree

Ні, питання було дуже конкретним і розумним. Відповіді не такі. Slow mark & ​​sweep GC мають дві фази: етап кроку сканування коренів на стеку та фазу розгортки сканування купи. Швидке копіювання GC має лише одну фазу, скануючи стек. Легко як це. Оскільки, мабуть, тут ніхто не знає про належні збирачі сміття, на це питання потрібно відповісти. Ваша інтерпретація дико не підходить.
рубан

0

Що виділяється на стеку? Локальні змінні та зворотні адреси (в С). Коли функція повертається, її локальні змінні відкидаються. Не потрібно, навіть згубно, підмітати стопку.

Багато динамічних мов, а також Java або C # реалізовані на мові системного програмування, часто в C. Можна сказати, що Java реалізована за допомогою функцій C і використовує локальні змінні C, а отже, збирач сміття Java не повинен змітати стек.

Існує цікаве виняток: збирач сміття Chicken Scheme в дійсно підмітати стек (таким чином), так як його реалізація використовує стек у вигляді збору сміття простору першого покоління: см Курячий ескізний Вікіпедію .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.