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

Monday 12 August 2013

Створення обєктів в памяті

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

Type       

x86 size           

x64 size

object        12 24
object[]   16 + length * 4 32 + length * 8
int[]      12 + length * 4 28 + length * 4
byte[]       12 + length            24 + length
string        14 + length * 2 26 + length * 2

Отож давайте розглянемо чому object займає 12 байт і що в цих 12 байт входить.
Кожен обєкт в .NET має так званий оверхед, це службові дані, в даному випадку таких даних 8 байт. Перших чотири байти це SyncBlock наступны 4 байти MethodTable, ще 4 байти залишають пустими і в них ми можемо зберрегти дані(наприклад int, так як його розмір якраз 4 байти). Якщо ми захочемо записати ще якісь дані в обєкт, нам автоматично буде виділено ще 4 байти(х86), тобто як тільки ми перевищуємо поточну вмістимість обєкту нам автоматично виділяється ще 4 байти(х86) або більше в залежності від того обєкт якого розміру ми хочемо додати в обєкт.
Схематично пустий обєкт виглядає наступним чином:
EmptyObjectStructure
Про те що зберігаться в SyncBlock та MethodTable ми поговоримо трохи пішніше.
Сам обєкт розміщується в керованій купі, де за ним пильно слідкує збирач мусору(Garbage collector). Сама купа може містити довільну кількість обєктів, як тільки хтось з них стає не потрібним його видаляють а сама купа дефрагментується. Детальніше про купу видалення обєктів та інші речі буде в наступному пості,  поки що,  давайте зосередимось на розмірах обєктів, а саме перевіримо розміри пустого обєкті в коді:
   1: private static void Main(string[] args)

   2: {

   3:     long beforeObj = GC.GetTotalMemory(true);

   4:     object obj = new object();

   5:     long afterObj = GC.GetTotalMemory(true);

   6:     long diff = afterObj - beforeObj;

   7:  

   8:     Console.WriteLine("object: \t" + diff);

   9:     Console.ReadLine();

  10: }
Код сам по собі досить простий, винісши його в окремий метод отримаємо наступне:
   1: private static void ObjectSizes()

   2:         {

   3:             int size = 1000;

   4:             double diff;

   5:  

   6:             long beforeObj = GC.GetTotalMemory(true);

   7:             object obj = new object();

   8:             long afterObj = GC.GetTotalMemory(true);

   9:             diff = afterObj - beforeObj;

  10:  

  11:             Console.WriteLine("obj: \t\t" + diff);

  12:  

  13:             //---------------------------------------

  14:  

  15:             long beforeObjArray0 = GC.GetTotalMemory(true);

  16:             object[] array0 = new object[0];

  17:             long afterObjArray0 = GC.GetTotalMemory(true);

  18:             diff = afterObjArray0 - beforeObjArray0;

  19:  

  20:             Console.WriteLine("array0: \t" + diff);

  21:  

  22:             //---------------------------------------

  23:  

  24:             long beforeObjArray = GC.GetTotalMemory(true);

  25:             object[] arrayEmpty = new object[size];

  26:             long afterObjArray = GC.GetTotalMemory(true);

  27:             diff = afterObjArray - beforeObjArray;

  28:  

  29:             Console.WriteLine("arrayEmpty: \t" + (diff));

  30:             Console.WriteLine("Per object: \t" + ((diff - 16) / size));

  31:  

  32:             //---------------------------------------

  33:  

  34:             //PLEASE COMMENT CODE ABOVE AS GC DOESN'T ALLOW TO MEASURE NEXT array100 PROPERLY

  35:  

  36:             long before = GC.GetTotalMemory(true);

  37:             object[] array100 = new object[100]; //416 + 100 * 12

  38:  

  39:             for (int i = 0; i < 100; i++)

  40:             {

  41:                 array100[i] = new object();

  42:             }

  43:             long after = GC.GetTotalMemory(true);

  44:             diff = after - before;

  45:             

  46:             Console.WriteLine("array100: \t" + diff);

  47:             Console.WriteLine("Per object: \t" + (diff - 416) / 100);

  48:         }
В результаті отримаємо:
obj:              12       
array0:         16       
arrayEmpty: 4016   
Per object:   4        
array100:      1392  
Per object:    9.76
  

Останні два рядки не відображають реальні дані, для того щоб побачити обєм даних закоментуйте визначення розміру для попередніх обєктів.
Як бачимо дійсно пустий обєкт займає 12 байт. Масив обєктів на 4 байти більше, в ці 4 байти записано Element Type Pointer, 4 байти які для обєкту по замовчуванню вільні, містять розмір масиву. Та на даний час це не суттєво.
Я б хотів зосередитись більше на визначенні розмірів елементарних обєктів тому що, по перше це можуть спитати на інтервю а по друге це дуже просто і робити таких елементарних помилок не варто.
Отож маємо наступний класс:
   1: class EmptyWithMethods

   2: {

   3:     public void DO(int x)

   4:     { }

   5:  

   6:     public virtual void DOVirtual(int x)

   7:     { }

   8: }
Запитання досить просте, скільки займатиме цей обєкт в памяті?
З першого погляду можна подумати що дві змінні які приймають методи будуть давати нам додаткове навантаження відповідно 8 байт оверхед плюс 8 байт для двох інтів і в результаті матимемо 16 байт. Та й в принципі стає цікаво стільки займають і чи займають щось взагалі методи? Відповідь проста, пустий клас по замовчуванню займає 12 байт (4 з яких пусті і в них можна записати 1 int або 4 byte), методи відносяться до класу а не до обєтку, і як ми знаємо з книжок клас це як заготовка на основі якої створюється обєкт, отож опис класу і методів до розміру обєкту значення не мають, вони оголошені в одному місці і є спільними для всіх нащадків класу. В результаті скільки б методів не було в нашому класі, і скільки б вони параметрів не приймали наш обєт займає тих самих 12 байт! при чому 4 з них пусті.

   1: class OneInt32 { int x; }
Тут все також дуже просто як я вже писав обєкт має окрім 8 байт оверхеду ще 4 байти вільних в ці 4 байти якраз чудово вписується наш int отож наш обэкт буде мати розмір 12 байт з тою лише різницею що останні 4 байти будуть зайняті інтом навідміну від попереднього прикладу.
   1: class TwoInt32 { int x, y; }

Цей обєкт займе 16 байт так як окрім заповнених 4 пустих байт нам потрібно ще 4 байти на ще один int y.

   1: class Mixed1

   2: {

   3:     int x;

   4:     byte b1;

   5: }
Цей приклад спершу може здатись простим клас (8 байт) + x (4 байти) + b1 (1 байт) = 13 байт, але слід памятати що для х86 память виділяється кусками по 4 байти. Беручи це до уваги давайте порахуємо:
клас (8 байт) + x (4 байти) + b1 (1 байт) = 16 байт  тому що для зберігання обєкту виділиться мінімальний обсяг памяті в 4 байти і лише один з цих 4 байтів буде зайнятий а ще три лишаться вільними.
   1: class Mixed1

   2: {

   3:     int x;

   4:     byte b1, b2, b3, b4;

   5: }
Цей обєкт також займе лише 16 байт тому місце під змінні b1, b2, b3, b4 буде відповідно в байтах від 13-го до 16-го включно.
Знаючи ці основи можна легко обчислити розміри обєктів, а якщо знати в що перетворюються стандартні проперті, то можна вирахувати розмір і більш важких типів.

String

Уважний читач мав би звернути увагу на те що розмір стрічки зазначено 14 байт +… і реально в памяті він займає 14 байт, діло в тому що стрінги це особливий тип даних в .NET і він працює трохи по іншоиму а ніж звичайні обєкти. перших 8 байт ті самі що й в звичайному обєкті, в наступні 4 записано розмір стрічки, а останні 2 байти це unicode NULL terminator. Він використовується для того щоб передавати ці стрічки в Win32 API. Отакий то нюанс)

Overhead

Ну і на останок трохи про такзваний оверхед. Перші 4 байти це посилання на синкблок, який використовується для синхронізації в мультипотокових аплікаціях та містить деякі інші службові дані. Наступних 4 байти містять інформацію про тип, це фактично його опис. Детальніше про це планую написати в наступних постах.
Код на гітхабі: https://github.com/BatsIhor/ObjectSizeDemo/