Управление на паметта и ресурсите


Взаимодействие със системата за почистване на паметта



страница5/7
Дата25.02.2017
Размер0.85 Mb.
#15739
1   2   3   4   5   6   7

Взаимодействие със системата за почистване на паметта





Намирам липсата ти на вяра обезпокоителна. Дарт Вейдър

.NET Framework предлага средства за взаимодействие със системата за почистване на паметта (garbage collector). Взаимодействието се осъщест­вява с помощта на статичните публични методи на класа GC, някои от които ще разгледаме в тази точка.

Почистване на паметта


Можем да предизвикаме стартирането на почистването на паметта с извикването на метода GC.Collect(). Извикването на този метод без параметри предизвиква пълно почистване на всички поколения памет. Извикването на overload варианта на същия метод с аргумент номер на поколение, предизвиква почистване на всички поколения, започвайки от 0 до указаното.

Помогнете на GC като не й помагате


Като правило (което както ще видите по-нататък си има изключения) се старайте да не помагате на системата за почистване на паметта. Тя е “произведение на изкуството”, внимателно проектирана така, че да гарантира висока ефективност при различен род приложения. Освен това, GC се самонастройва като следи поведението на заделяне на памет на вашето приложение.

Ако грижливо проектирате приложението си, няма да има нужда да мислите за GC, но ако си мислите, че се нуждаете да предизвикате почистване, значи нещо се е объркало. Трябва да се запитате какво сте направили, че е нужно да предизвикате почистване и от какво точно почистване се нуждаете – на поколение 0, 1 или 2?


Ако точно сега се нуждаете от почистване на поколение 0


Почистването на поколение 0 се случва достатъчно често и е сравнително “евтино”. GC използва темпото с което заделяте памет и големината на кеша на процесорите ви за да определи колко памет да ви позволи да заделите преди да стане изгодно да почисти поколение 0. Ако принудите GC да почисти паметта преди настъпването на този момент е възможно да му дадете прекалено малко времеви интервал, за да определи размера на заделената памет, необходима за следващото почистване и в крайна сметка да се окажете с повече почиствания на поколение 0 от колкото се нуждаете. Тъй като размерът на паметта на поколение 0 така или иначе не става прекалено голям, най-добре е да оставите GC да извършва автоматично почистването, както прецени за най-добре. Ако наблюдавате средно 1 почистване на поколение 0 в секунда, всичко е наред.

Ако точно сега се нуждаете от почистване на поколение 1


Първият проблем е, че GC.Collect() не обещава почистване на обекти от поколение 1. Следващия проблем е, че за да знаете размера на поколение 1 (иначе за какво ви е да го почиствате) трябва да наблюдавате темпото на оцеляване на обектите от поколение 0, така че в крайна сметка е доста сложно да разберете дали наистина се нуждаете от почистване на поколение 1. Последният проблем е, че поколение 1 е също сравнително “евтино” за почистване от GC (въпреки, че е по-скъпо от поколение 0, тъй като го включва в себе си) и отново е безсмислено в повечето случаи да предизвикате GC да го почисти.

Без да се впускаме в повече подробности, ако се нуждаете от почистване на поколение 1, не го предизвиквайте. Вместо това вижте дали не може да промените кода или алгоритмите, които използвате, така че да направите обектите недостижими колкото е възможно по-бързо. Вашата цел е да направите по-дълго живеещите обекти да станат със средна продължителност на живот, а последните с кратка, след което може да спрете да се тревожите за поколение 1. След като обектите ви вече са в поколение 0, както сами сте се уверили, няма нужда да предизвиквате GC изобщо. Ако наблюдавате едно почистване на поколение 1 средно на десетина секунди, всичко е наред.


Ако точно сега се нуждаете от почистване на поколение 2


Почистването на поколение 2 означава цялостно почистване на паметта, следователно е значително по-скъпо от това на поколения 1 и 2. Отново, имайте предвид, че GC.Collect(2) не ви обещава почистване на поколе­ние 2. Ако си мислите, че имате нужда от почистване на поколение 2, значи дизайна на приложението ви се нуждае от щателен преглед.

Ако наблюдавате почистване на поколение 2 на 100 секунди, всичко е наред.


Кога може да помогнете на GC


Има смисъл да извикате GC.Collect() ако някое, неповтарящо се често събитие се е случило току-що и това събитие е допринесло за смъртта на много стари обекти.

Класически пример за това е ако пишете desktop приложение и предоставите на потребителя голяма и сложна форма, асоциирана с много данни в нея. Потребителят е създал с помощта на тази форма голям XML или един или повече DataSet обекта. Когато формата се затвори, тези обекти са мъртви и GC.Collect() ще освободи паметта им.

Въпреки, че системата за почистване на паметта е самонастройваща, тя не може да предвиди абсолютно всеки шаблон за заделяне на памет и в горния случай най-вероятно няма да успее да предвиди, че умират много обекти от поколение 2 (обектите от поколение 0 и 1 ще са с голяма вероятност вече почистени). Само обектите, които първоначално са свързани с формата ще преминат в поколение 2. В този момент е много добре от гледна точка на производителността да се извика почистване на поколение 2.

Така че, когато неповтарящо събитие, включващо смъртта на много обекти се случи (например при завършване на инициализация на приложението или при затварянето на голям диалогов прозорец) може да вмъкнете GC.Collect() за да освободите паметта. Не го правете, ако обектите не са много.

Имайте предвид, че Microsoft настройват системата за почистване на паметта все повече и повече и в следващата й версия е възможно вашите извиквания към GC.Collect() да попречат на GC да работи ефективно вместо да помогнат. Ето защо е добра идея да ги обгърнете в условен метод, например:

#define HELP_GC
public sealed class GCHelper

{

[Conditional("HELP_GC")]



public static void Collect()

{

GC.Collect();



}

private GCHelper() {}

}


В примера се използва атрибутът System.Diagnostics.Conditional, с който се указва, че методът Collect() на класа GCHelper е условен метод и съществува само ако е дефиниран символът HELP_GC по време на компилация. В противен случай методът изчезва заедно с всички извиквания към него.

Финализаторите увеличават живота на обектите


В C++ e общоприето да се използват деструктори, за да се освобождава памет или по-общо ресурси. Както вече знаете, финализаторите не са деструктори, така че като изключим обгръщането на неуправляван ресурс, вашите класове не се нуждаят от финализатор. GC ще почисти боклука от членовете на вашия “мъртъв” обект без да е нужно да им зададете стойност null. Има смисъл да присвоите null на част от членовете на типа си, само ако искате те да бъдат почистени докато обектът ви е все още “жив”.



Дефинирайте финализатор само ако класът ви обгръща неуправляван ресурс!

Ако случая не е такъв, вие просто не се нуждаете от финализатор. Добавянето на такъв със сигурност изпраща обектите от вашия клас в по-горно поколение и увеличава работата на системата за почистване на паметта.

Имплементирайте IDisposable без финализатор


Когато знаете, че този, който ще ползва класа сте вие, и че няма да забравите да извикате Dispose() добавете финализатор само за DEBUG компилация за да сте сигурни, че викате Dispose() навсякъде, където е необходимо:

class SomeDisposable : IDisposable

{

#if DEBUG // Финализаторът съществува само в DEBUG build



~SomeDisposable()

{

Debug.Assert(mDisposed, "Dispose wasn't called!");



}

#endif


public void Dispose()

{

// ... имплементация



mDisposed = true;

GC.SuppressFinalize(this); // виж по-долу

}
private bool mDisposed = false;

}


За съжаление, горната техника не носи информация кой и къде е забравил да извика Dispose(). Ако обаче ползвате редовно оператора using, няма да ви се наложи да търсите виновния код.

Потискане на финализацията


Както вече видяхте в примерната имплементация на базов клас, предоста­вящ финализатор и имплементация на IDisposable, след извикването на метода Dispose() е добре (макар и незадължително) да потиснете финализацията като оптимизация. Потискането на финализация се извършва с помощта на метода GC.SuppressFinalize(), който приема като параметър инстанция на тип. Цената за извикване на този метод е просто промяната на 1 бит в заглавната част на обекта.

Изчакване до приключване на финализацията


Ако имате случай, в който сте помогнали на GC, като сте извикали GC.Collect(), можете да помогнете още малко, като извикате метода GC.WaitForPendingFinalizers(). Извикването на този метод ще принуди GC да обработи всички финализатори на маркираните за финализация обекти от извикването на GC.Collect(). Добрата новина е, че по този начин неуправляваните ресурси, обвити в почистените обекти ще бъдат унищожени (ресурсите също заемат памет). Лошата е, че ще ви е необходимо повече време за да приключи финализацията им. Общо взето, ако е настъпил добър момент за предизвикването на пълно почистване, вие би трябвало да сте склонни да отделите това време.

Регистриране на обекта за финализация


Ако поради някаква причина сте премахнали обекта си от опашката за финализиране (извиквайки GC.SuppressFinalize()), може да го доба­вите отново, като извикате GC.ReRegisterForFinalize(). Единствената смислена употреба на този метод е да съживите обекта, който се финализира в момента, извиквайки GC.ReRegisterForFinalize(this). Това може да се наложи да направите ако по време на финализация класът ви не успее да се финализира успешно и има нужда да опита пак след известно време.

Определяне поколението на обект


В случай, че сте решили да предизвикате почистване на паметта, може да определите в кое поколение се намират обектите, които искате да почистите, за да извикате GC.Collect() до това поколение. Извикването на GC.GetGeneration(object) ви връща поколението на обекта. Ако например сте решили да почистите обект и обектите от неговото поко­ление (и надолу до поколение 0), използвайте следния код:

public sealed class GCHelper

{

public static void CollectObjectGeneration(object obj)



{

if (obj != null)

{

GC.Collect(GC.GetGeneration(obj));



}

}

}



Максималното поколение в даден момент може да получите като извикате свойството GC.MaxGeneration().

Pinning


Английският глагол pin означава забождам, приковавам, притискам (обик­новено с топлийка/карфица). В контекста на взаимодействие със систе­мата за почистване на паметта, забождането на обект означава да не позволите на GC за известно време да мести обекта на друго място в паметта (което обикновено се случва при събиране на боклука на поколението, в което “живее” обекта). В този текст ще използваме думата pinning.

Pinning прилича малко на финализацията по това, че и двете съществуват, защото ни се налага да работим с неуправляван (native) код.

Обектите се pin-ват по три причини:


  • При създаване на инстанция на класа GCHandle с тип GCHandleType. Pinned (което едва ли ще ви се наложи да използвате)

  • При използване на ключовата дума fixed в C# (или __pin в Managed C++)

  • По време на взаимодействие с неуправляван код (Interop), някои аргументи се pin-ват от Interop (например, за да се подаде обект String като LPWSTR, Interop pin-ва буфера по време на изпълнението извиканата функция)

За обектите от малкия хийп (поколения 0, 1 и 2), pinning-а е единствения начин потребителят да успее да фрагментира хийпа.

За блока от памет за големи обекти (LOH), pinning-а в момента е нулева операция, тъй като в настоящата имплементация на Garbage Collector-ът, обектите в LOH не се пренареждат, както при поколения от 0 до 2. Разбира се, това е имплементационен детайл, на който не бива да разчитате.

Фрагментирането на хийпа е лошо. То кара GC да работи по-усърдно, като вместо просто да “прецежда” достъпните обекти, сега трябва да запомни кои “живи” обекти са pin-нати и да се опитва да вмъква обекти в свободните места между pin-натите обекти.

Когато ви се налага да pin-нете обект, имайте предвид следното:



  • Ако го направите за кратко време, операцията е “евтина”. Как може да прецените какво е кратко време? Ако по време на pinning-а не се случва събиране на боклука, операцията просто вдига бит в заглавната част на обекта и след приключването си го сваля. Но ако по това време се задейства GC “забодените” обекти не трябва да се местят. Следователно “кратко време” е времето, през което GC не забелязва, че обект е pin-нат. Това означава, че докато pin-вате обекти не трябва да се случват никакви или почти никакви заделяния на памет (които иначе биха могли да предизвикат нуждата от почистване на боклук).

  • Ако все пак често ви се налага да работите с буфери, които да pin-нете, преди да ги подадете като параметри на Interop функции например, можете да създадете пул от буфери и да предизвикате GC така, че обектите да минат в поколение 2. Тъй като обектите от поколение 2 се пренареждат доста по-рядко (а тези в LOH изобщо не се пренареждат), ще нанесете много по-малка “вреда” на GC.

Удължаване живота на променливите при Interop


Нека имаме клас, който обвива манипулатор към неуправляван ресурс и също така метод, който връща друг манипулатор, използвайки първия:

class ResourceWrapper : IDisposable

{

IntPtr hRes;



public IntPtr Method()

{

return SomeInteropFunction(hRes);



}

~ResourceWrapper() { … }

}


Нека сега си представим, че трябва да извикаме функция чрез Interop, която приема такъв манипулатор като параметър:

public void SomeMethod

{

using (ResourceWrapper rw = new ResourceWrapper())



{

PInvokeHelper.InvokeLibFunction(rw.Method())

}

}


На пръв поглед нещата изглеждат добре и вие може би си мислите, че променливата rw е “жива” до затварящата скоба на оператор using. Грешката е в това, че преди да се извика InvokeLibFunction се изчисляват нейните параметри, а именно манипулаторът, който очаква. Ето защо кодът в действителност би изглеждал така:

public void SomeMethod

{

using (ResourceWrapper rw = new ResourceWrapper())



{

IntPtr h = rw.Method();

PInvokeHelper.InvokeLibFunction(h);

}

}



Нека не забравяме, че всъщност операторът using е само “синтактична захар” и кодът в действителност е нещо такова:

public void SomeMethod

{

ResourceWrapper rw = new ResourceWrapper();



try

{

IntPtr h = rw.Method();



PInvokeHelper.InvokeLibFunction(h);

}

finally



{

((IDisposable)rw).Dispose();

}

}


Естествено компилаторът, както и JIT компилаторът могат да преценят, че тъй като резултатът от извикването на rw.Method() е прост стойностен тип (IntPtr просто обвива един int), референцията към rw след извикването на този метод е ненужна, следователно готова за събиране от GC (това нямаше да се случи, ако просто връщахме hRes от метода, но ние връщаме нов манипулатор, към който класът ResourceWrapper няма референция). Кодът в този случай би могъл да се пренареди по следния начин:

public void SomeMethod

{

ResourceWrapper rw = new ResourceWrapper();



IntPtr h = IntPtr.Zero;

try


{

h = rw.Method();

}

finally


{

((IDisposable)rw).Dispose();

}

// тук обектът вече е унищожен



PInvokeHelper.InvokeLibFunction(h);

}


Тъй като rw може да бъде унищожен, манипулаторът, върнат от извикването на Method е невалиден (ако например вторият манипулатор зависи от първия).

Как така е възможно някой да си помисли, че обектът е готов за почистване? Ами много просто – извикването на Method връща манипула­тор, към който никой няма референция. Още в този момент обектът е готов за GC, тъй като кодът по-долу не го използва, освен, за да се извика Dispose(). Ако не използвахме операторът using, обектът би могъл да бъде почистен веднага след напускането на метода Method.

Естествено подобни спекулации може да ви накарат да настръхнете и вероятно вече си мислите, че дори да не изпаднете в точно тази ситуация, може неволно да напишете код, който да я предизвика. Ситуации като горната, обаче обикновено са свързани с Interop, с който не се сблъсквате често, а освен това съществува решение на проблема.

Статичният метод GC.KeepAlive(object) приема обект като параметър и служи като индикация за компилатора, JIT компилатора и най-вече GC да не събира обекта до момента, в който се извика KeepAlive(). Първо­началният код с вмъкнат GC.KeepAlive() би изглеждал така:



public void SomeMethod

{

using (ResourceWrapper rw = new ResourceWrapper())



{

PInvokeHelper.InvokeLibFunction(rw.Method())

// може да бъдем сигурни, че InvokeLibFunction ще бъде

// извикана преди rw да бъде почистен



GC.KeepAlive(rw);

}

}





Сподели с приятели:
1   2   3   4   5   6   7




©obuch.info 2024
отнасят до администрацията

    Начална страница