Thursday, 29 August 2013

Знищення мусору в .NET (Garbage collection)

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

Отож давайте почнемо з того що це таке й для чого а вже далі передемо до прикладів.

.NET автоматично видаляє з памяті обєкти які більше нам не потрібні, і це дуже добре тому що ми не повинні слідкувати за цим низькорівневим процесом, моніторити скільки памяті займає той чи інший обєкт і чи випадково немає в нашій аплікації втрат памяті через обєкти які більше не використовуються. Ми просто пишемо код, використовуємо обєкти а про їх знищення дбає збирач мусору(Garbage collector).

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

   1: void Do()
   2: {
   3:     string str = "some info";
   4:     Console.WriteLine(str);
   5: }

В вище наведеному прикладі str буде кожного разу створюватись після виклику методу а знищена після того як метод ввиконався.

Все просто як двері)

Давайте тепер розберемось як GC знає про те що змінна може бути видалена. Як ми знаємо з попереднього посту всі обєекти стврюються в керованій купі, місце під них виділяє GC відповідно щоразу коли необхідно звільнити місце GC проходиться по цій купі і будує дерево звязків обєктів, Корінням цього дерева слугуються загальнодоступні обєкти та обєкти з якими ми зараз працюємо. Відповідно всі обєкти які не попали в це дерево вважаються непотрібними і будуть знищені.

Тепер давайте розглянемо трохи інший випадок, а що якщо ми маємо обєкт який має деструктор? в такому випадку перед тим як знищувати обєкт ми повинні викликати відповідний деструктор для цого обєкту. Так як ці обєкти відрізняються від тих які мжна просто видалити і вимагають додаткових затрат на виклик деструктора, було прийнято рішення зберігати посилання на такі обєкти в додатковій таблиці, тобто обєкт створюється в керованій купі, і при цьому посилання на нього додається в такзвану Finalization Queue(чергу обєктів з фіналізаторами). Отож підчас збирання інформації про те які обєкти треба знищити GC також дивиться в цю чергу і якщо обєкт міститься в ній це означає що для нього треба виконати “останнє бажання”. Всі посилання на такі обєкти з Finalization Queue переміщуються в іншу чергу(Freachable Queue) яка містить посилання на обєкти для яких треба викликати деструктор.

Тут важливо зауважити що посилання з Finalization Queue саме перемыщується в Freachable Queue. При наступному виклику збирання мусору, наш GC загляне в цю чергу і для всіх обэктыв викличе їхні деструктори. При тому він не чекатиме їхнього виконання а зразу ж викине їх з цієї черги так як вважатиме що вони відпрацювали. І вже лише після наступного проходу по мертвих обєктах обєкт для кого викликався деструктор не міститиме посилань на себе а ні в Finalization Queue а ні в Freachable Queue і відповідно його місце звільниться.

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

А давайте тепер розглянемо трохи не стандартний випадок, випадок коли у деструкторі(фіналізаторі), ми створимо посилання на обєкт який має видалятись тобто на this.

   1: ~Program()
   2: {
   3:      SomeGlobalObject.Property = this;
   4: }

В цьому випадку після того як відпрацює деструктор, зявиться ссилка на наш обєкт в якомусь глобальному обєкті, відповідно при наступному проходженні GC наш обєкт попаде в дерево обєктів які на даний час використовуються і відповідно він не буде знищений. Цей випадок називається воскресінням обєкту і цікавий він тим що обєкт який має деструктор і в принципі мав бути знищеним ожив і при цьому ссилка на нього видалена з Finalization Queue Freachable Queue, а це значить що деструктор для нього більше викликатись не буде, такий собі ходячий мертвець, який може бути в доситься невалідному стані тим не менше існуватиме допоки хтось на замінить його в SomeGlobalObject.Property або просто не запхає туди null.  Якщо в якосмусь меганезрозумілому випадку вам прийдеться так зробити то ви завжди можете зареєструвати такий обєкт на фіналізацію ще раз за допомогою методу GC.ReRegisterForFinalize(obj);. В такому випадку для обєкту ще раз буде викликаний деструктор.

   1: using System;
   2: using System.Threading;
   3:  
   4: namespace GC_01
   5: {
   6:     class Program
   7:     {
   8:         private volatile static object obj;
   9:  
  10:         static void Main(string[] args)
  11:         {
  12:             Do();
  13:             GC.Collect(0);
  14:             Thread.Sleep(100);
  15:             Console.WriteLine("tmp has to be removed.");
  16:  
  17:             Console.WriteLine();
  18:  
  19:             WalkingDead wd = new WalkingDead();
  20:             GC.Collect();
  21:             GC.WaitForPendingFinalizers();
  22:             Console.WriteLine("wd dctor was called but object was not removed. \n");
  23:  
  24:             GC.Collect();
  25:             Thread.Sleep(100);
  26:             Console.WriteLine(
  27:                 "WalkingDead dctor was NOT called second time as it's removed from Finalization and Freachable Queue\n");
  28:  
  29:             if (obj != null)
  30:                 Console.WriteLine("Obj is not null\n"); // This shows that our object exist.
  31:             else
  32:                 Console.WriteLine("Obj is null\n");
  33:  
  34:             Thread.Sleep(100);
  35:  
  36:             Console.WriteLine("GC.ReRegisterForFinalize(obj);");
  37:             GC.ReRegisterForFinalize(obj);
  38:             GC.Collect();
  39:             Thread.Sleep(100);
  40:  
  41:             Console.WriteLine("WalkingDead dctor WAS called as we reregister it for finalization.");
  42:         }
  43:  
  44:         internal class WalkingDead
  45:         {
  46:             public WalkingDead()
  47:             {
  48:                 Console.WriteLine("WalkingDead ctor");
  49:             }
  50:  
  51:             ~WalkingDead()
  52:             {
  53:                 if (obj != null) //Check if we are calling after resurection.
  54:                 {
  55:                     Console.WriteLine("--------------WalkingDead dctor---------------");
  56:                 }
  57:                 else
  58:                 {
  59:                     Console.WriteLine("WalkingDead dctor");
  60:                     obj = this;
  61:                 }
  62:             }
  63:         }
  64:  
  65:         internal class Temp
  66:         {
  67:             public Temp()
  68:             {
  69:                 Console.WriteLine("ctor");
  70:             }
  71:  
  72:             ~Temp()
  73:             {
  74:                 Console.WriteLine("dctor");
  75:             }
  76:         }
  77:  
  78:         static void Do()
  79:         {
  80:             Temp tmp = new Temp();
  81:         }
  82:     }
  83: }

Пропоную трохи посидіти і зрозуміти код, так як це трохи прояснить те про що я написав вище.

Оригінальний пост: http://batsihor.blogspot.com/2013/08/net-garbage-collection.html