Книга е още в много ранна фаза на написване


Почестване: финализация и събиране на боклука



страница25/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   21   22   23   24   25   26   27   28   ...   73

Почестване: финализация и събиране на боклука


Прогромистите знаят важността на инициализацията, но често забравят важ­ност­та на начина на приключване. Най-после, кой има нужда да чисти про­мен­лива int? С библиотеките, обаче “зарязването” на обект когато сте свършили с не­го не е винаги безопасно. Разбира се, Java има боклучар който да освободи па­метта на обектите, когато вече не са нужни. Сега да предположим много спе­циа­лен и необичаен случай. Да кажем че обект заема “специална” памет без из­полз­ване на new. Боклучарят знае само как се освобождава памет заета с new, та­ка че не знае как се освобождава “специална” памет. За такива случаи Java пред­лага метод наречен finalize( ) който може да дефинирате за ваш клас. Ето как се предполага да работи този метод. Когато боклучарят е готов да осво­бо­ди паметта на обекта ви,той първо вика finalize( ) и чак на следващото си ми­на­ва­не освобождава паметта. Така че ако речете да използвате finalize( ) той ви да­ва възможност да изпълните важни действия по времето на събирането на бок­лука.

Това съдържа потенциални неприятности, понеже някои програмисти, специално C++ програмисти, биха могли в началото да сбъркат finalize( ) с деструкторите в C++, които са функции които се викат винаги когато се разрушава обект. Важно е да се прави разлика между C++ и Java в този случай, понеже в C++ обектите винаги биват разрушавани (в програма без бъгове), докато в Java обектите не винаги минават през боклучаря. Или, с други думи:

Събирането на боклука не е разрушаване.

Ако запомните това, няма да имате тревоги. Това значи че трябва да пред­прие­ме­те някои действия сами преди събирането на боклука, когато вече не се нуж­дае­те от даден обект. Java няма деструктори или подобна концепция, така че тряб­ва вие да създадете подходящ метод за тази дейност. Например да пред­по­ло­жим че в процеса на създаване вашият обект се изобразява на екрана. Ако яв­но не изтриете образа на екрана той би могъл никога да не бъде почистен. Ако сло­жите подходящите неща за изтриването във finalize( ), тогава ако обектът ми­не през боклучаря образът ще се махне, но ако не — ще остане. Така че вто­ро­то нещо за запомняне е:

Обектите ви може и да не бъдат почиствани от боклучаря.

Може да установите че в програмата ви никога не се освобождава памет по­не­же тя никога не заема твърде много памет. Ако вашата програма завърши (нор­мал­но - б.пр.) и боклучарят не е освобождавал въобще памет, цялата на вед­нъж заемана от програмата памет ще бъде върната на операционната система при изхода от програмата. Това е хубаво нещо, защото събирането на боклука от­нема допълнително време и няма защо да се прави, ако не е необходимо.


За какво е finalize( )?


Може да помислите в този момент, че никога няма да използвате finalize( ) като метод за обща употреба в почистването. Какво хубаво дава той?

Третото нещо за запомняне е:

Събирането на боклука е само за паметта.

Тоест боклучарят съществува само за да освободи паметта, която вашата про­гра­ма вече не използва. Така че всяка активност асоциирана с боклучаря, най-ве­че finalize( ) метода, трябва да бъде само за паметта и нейното осво­бож­да­ва­не.

Значи ли това че ако вашият обект съдържа други обекти finalize( ) трябва явно да ги освобождава? Ами, не – събирачът на боклук се грижи за осво­бож­да­ва­не­то на обектите без значение как са създадени. Това показва, че нуждата от finalize( ) е ограничена до специални случаи, в които обект може да заеме памет по начин различен от създаването на обект. Но, може да забележите вие, всичко в Java е обект и как би станало това?

Би могло да се каже, че finalize( ) съществува, защото може да направите нещо като C-подобните начини за алокиране на памет Java. Това може да се случи най-вече чрез native methods, които са начин да се вика не-Java код от Java. (Те се обсъждат в приложение A.) C и C++ са единствените езици поддържани в мо­мента от собствените (т.е. на езика на конкретната машина, на която се из­пъл­няват - б.пр.) методи, но тъй като те могат да викат подпрограми на вся­как­ви езици, фактически може да се вика всичко. Вътре в не-Java кода, фамилията malloc( ) функции на C може да бъде викана за заемане на памети докато не из­ви­кате free( ) тази памет няма да бъде освободена, предизвиквайки съответните про­блеми. Разбира се, free( ) е C и C++ функция, така че ще я викате в нативен ме­тод втре във finalize( ).

След прочитането на това може би ще си помислите, че няма да използвате finalize( ) много. Прави сте, това не е мястото за обикновеното почистване. Та­ка че къде ще го правим нормално?

Вие трябва да почистите


За да се почисти обект, потребителят му трябва да предприеме съответните дей­ствия, когато е небходимо. Това звучи много праволинейно, но прилича на кон­цепцията в C++ за деструкторите. В C++ всички обекти се разрушават. Или по-скоро трябва да се разрушават. В C++ обектът се създава като локален, т.е на стека (невъзможно в Java), после деструкторът се изпълнява при за­тва­ря­ща­та фигурна ("голяма") скоба където свършва обхватът на обекта. Ако обектът е бил създаден с new (както в Java) деструкторът се вика когато програмистът из­ви­ка C++ операторът delete (който не съществува в Java). Ако програмистът за­бра­ви, деструкторът никога не се вика и стават "изтичания" на памет, плюс че дру­гите части на обекта никога не се почистват.

В контраст Java не позволява да се създават локални обекти – винаги трябва да из­ползвате new. Но в Java няма“delete” за освобождаване на обекти понеже има съ­бирач на боклук. Така че от опростенческа гледна точка би могло да се каже, че понеже има събирач на боклук Java няма деструктори. С напредването на че­те­нето на тази книга, обаче, ще се уверите, че боклучарят не премахва нуждата и полезността на деструкторите. (И никога няма да викате finalize( ) директно, та­ка че това не е подходящото решение.) Ако искате някакви други операции пре­ди излизане на обекта от обхвата все още трабва да извикате метод в Java, кой­то е еквивалентът на C++ деструктора без удобството му.

Едно от нещата за които finalize( ) може да бъде полезен е наблюдаването на про­цеса на събиране на боклука. Следващият пример проследява какво става и ре­зю­мира досегашното изложение за боклучаря:

//: c04:Garbage.java

// Demonstration of the garbage

// collector and finalization


class Chair {

static boolean gcrun = false;

static boolean f = false;

static int created = 0;

static int finalized = 0;

int i;


Chair() {

i = ++created;

if(created == 47)

System.out.println("Created 47");

}

protected void finalize() {



if(!gcrun) {

gcrun = true;

System.out.println(

"Beginning to finalize after " +

created + " Chairs have been created");

}

if(i == 47) {



System.out.println(

"Finalizing Chair #47, " +

"Setting flag to stop Chair creation");

f = true;

}

finalized++;



if(finalized >= created)

System.out.println(

"All " + finalized + " finalized");

}

}


public class Garbage {

public static void main(String[] args) {

if(args.length == 0) {

System.err.println("Usage: \n" +

"java Garbage before\n or:\n" +

"java Garbage after");

return;

}

while(!Chair.f) {



new Chair();

new String("To take up space");

}

System.out.println(



"After all Chairs have been created:\n" +

"total created = " + Chair.created +

", total finalized = " + Chair.finalized);

if(args[0].equals("before")) {

System.out.println("gc():");

System.gc();

System.out.println("runFinalization():");

System.runFinalization();

}

System.out.println("bye!");



if(args[0].equals("after"))

System.runFinalizersOnExit(true);

}

} ///:~


Горната програма създава много Chair обекти и когато по някое време боклу­ча­рят се включи тя спира да ги създава Chairs. Доколкото боклучарят може да се включи по кое да е време не може да се каже точно кога това ще стане, така че има флаг наречен gcrun за индикация кога събирането е започнало. Втори флаг f е начинът за Chair да каже на main( ) цикъла да спре да прави обакти. И два­та флага са сложени във finalize( ), който се вика при събирането на боклука.

Две други static променливи, created и finalized, пазят броя на създадените objек­ти и тези, които са финализирани от събирача на боклук. Накрая, всеки Chair има собствен (не-static) int i така че знае кой номер е. Когато Chair но­мер 47 се финализира флагът се слага true за да накара да спре процесът на съз­да­ване на Chairи.

Всичко това става в main( ), в цикъла

while(!Chair.f) {

new Chair();

new String("To take up space");

}

Бихте могли да се чудите как този цикъл въобще ще свърши, понеже няма нищо въ­тре което да мени стойността на Chair.f. Обаче finalize( ) ще го промени, на­края, като се стигне до обект 47.



Създаването на String обекти е просто създаване на повече боклук, така че да се включи боклучарят и да ги изрита, което той и ще направи когато се по­чув­ства нервен по въпроса за достатъчността на незаетата още памет.

Когато стартирате програмата давате аргумент на командния ред “преди” или “след.” “before” ще идвика System.gc( ) метода (за да стартира събирането на бо­клука) заедно със System.runFinalization( ) за да стартира финализаторите. Тези методи бяха достъпни в Java 1.0, но runFinalizersOnExit( ) методът който се вика чрез “след” аргумента го има само в Java 1.13 и по-нататък. (Забележете че може да викате този метод когато и да е през време на изпълнението на про­гра­мата и изпълнението на финализаторите не зависи от събирането на боклука при това положение).

Предишната програма показва че в Java 1.1 обещанието че финализаторите ще се стартират винаги е изпълнено, но само ако изрично ги посочите да се из­пъл­нят. Ако използвате аргумент който не е “преди” или “след” (като “никакъв”), ня­ма да има финализационен процес и ще се получи изход като този:

Created 47

Beginning to finalize after 8694 Chairs have been created

Finalizing Chair #47, Setting flag to stop Chair creation

After all Chairs have been created:

total created = 9834, total finalized = 108

bye!

Така не всички финализатори ще се стартират до свършването на програмата.4 За да се извикат с необходимост може да се извика System.gc( ) следван от System.runFinalization( ). Това ще разруши всички обекти които не са в упо­тре­ба вече в този момент. Лошото в това е че викате gc( ) преди runFinalization( ), което изглежда да противоречи на документацията на Sun коя­то твърди, че финализаторите се стартират първи, а паметта се освобождава по­сле. Обаче ако извикате runFinalization( ) първо, а после gc( ), фина­ли­за­то­ри­те няма да се изпълнят.



Една причина Java 1.1 може би да пропуска по предположение финализаторите е, че изпълнението им е скъпо. Който и от двата подхода да използвате може да за­бе­лежите по-дълги задръжки, отколкото ако се пусне боклучарят без фина­ли­за­торите.

Как работи събирачът на боклук


Ако сте работили с език където алокирането на памет за обекти на хийпа е скъ­по естествено е да предполагате че схемата в Java за алокиране на всичко (ос­вен привитиви) на хийпа е скъпа. Оказва се обаче, че събирачът на боклука мо­же да окаже знъчително облекчаващо влияние върху процеса на създаване на обек­тите. Това може да звучи малко тъпо отначало – че освобождването на па­мет може да засегне алокирането – но това е начинът на работа на някои JVMашини и означава, че създаването на обекти в нийпа в Java може да бъде срав­нимо по бързина със създаването на обекти на стека в други езици.

Наприме може да се мисли че хийпът в C++ е нещо като двор, в който всеки обект си заделя негово си местенце. Този недвижим имот може да бъде изо­ста­вен по-късно и повторно използван. В някои JVMашини хийпът на Java е доста раз­личен; той по-прилича на конвейерна лента, която се придвижва напред с вся­ко създаване на обект. Това значи че алокирането на памет е забележително бър­зо. “Хийп указателят” просто се мести напред върху девствена територия, та­ка че ефективно е същото като алокирането в стека в C++. (Разбира се, има мал­ко режийни за счетоводството, но не и търсене в паметта. (И, разбира се, са­ма­та виртуална машина не бива да се забравя при сравняването с С++ - б.пр.))

Може да забележите сега, че хийпът не е конвейерна лента и ако се третира по то­­зи начин ще се получи голямо прехвърляне на страници (страницирането е го­лям пробив в скоростта) и недостиг на страници. Трикът е, че събирачът на бо­клук влиза в ролята си и мести “указателя на хийпа” по-близо до началото на кон­вейера и по-далеч от издънването на програмата вследствие page fault и не­възможност да се излезе от него. Боклучарят реорганизира нещата и прови въз­мож­но прилагането на модела на безкрайния свободен хийп при алокирането на па­мет.

За да разберете как става това, трябва да имате по-добра представа как раз­лич­ни­те боклучарски (GC) схеми работят. Проста но бавна GC техника е броене на по­зо­ваванията. При нея всеки обект има брояч на позоваванията и всеки път ко­гато се присъедини манипулатор към обекта броячът се увеличава. Всеки път когато манипулатор излезе от обхвата или стане null броячът се намалява. Така управ­лението на броячите довежда до малки допълнителни разходи за следене ка­кво става през живота на програмата. Боклучарят преглежда целия списък на обек­тите и когато намери обект с брояч сочещ нула освобождава паметта му. Единия недостатък е, че понеже обектите може да се позовават един на друг и са­ми на себе си може да се случи броячът да не е нула, а обектът да си е за из­чист­ване. Улавянето на такова самопозоваване изисква значителна до­пъл­ни­тел­на работа от боклучаря. Броенето на позоваванията често се използва за обяс­ня­ване на събирането на боклука но не изглежда да се използва в много реа­ли­за­ции на JVM.

При по-бързи схеми събирането не е основано на броене на позоваванията. Вмес­то това се използва идеята че от всеки жив (продължаващ да бъде из­полз­ван) обект може да се проследи път до валиден манипулатор или на стека или в ста­тична памет. Веригата може да върви през няколко слоя обекти. Така ако про­следите всички манипулатори които са на стека и в статичната памет ще стиг­нете и до всички живи обекти. За всеки манипулатор трябва да се проследи пъ­тя до обекта и след това да се видят манипулаторите в обекта, които ще до­ве­дат до други обекти, и т.н., докато се обхване цялата паяжина започваща от спо­менатия манипулатор на стека или в статичната памет. Всеки срещнат по пъ­ти­щата обект сигурно е жив. Забележете, че няма проблем със самозатворени гру­пи — те просто не се броят въобще и биват почиствани целите.

При описания подход JVM използва адаптина схема за събиране на боклука и това което се прави с живите обекти зависи от конкретната текуща кон­фи­гу­ра­ция. Един от вариантите е спри-и-копирай. Тоест по причини които по-късно ще станат ясни, програмата първо се спира (тази не е фонова схема за събиране на боклука). После всеки намерен жив обект се копира в друг хийп, а старата па­мет се освобождава цялата. В добавка, понеже се копират на ново място, обек­тите се разполагат един до друг, т.е. новата памет е компактно заета (и ста­ра­та памет изцяло, т.е. бързо, се освобождава, както вече се спомена).

Разбира се, когато обект се мести на друго място всички манипулатори които го сочат трябва да се променят. Лесно се променя първоначалният ма­ни­пу­ла­тор, но има и други манипулатори които могат да сочат към обекта и те да се от­крият по време на “разходката.” Те се променят когато бъдат намерени (можем да си представим една таблица която изобразява старите адреси в но­ви­те).

Има две неща които правят тези тъй наречени “copy collectors” (копиращи боклучари, във възприетата в превода терминология - б.пр.) неефективни. Пър­вото е, че има два хийпа и обектите се преточват от единия в другия, на прак­тика използвайки два пъти повече памет. Някои JVMашини се оправят с то­ва като алокират памет на порции и работят с тях, а алокират нови само ако е не­об­ходимо.

Второто е копирането. Веднъж като се стабилизира програмата може да ге­не­ри­ра малко или никакъв боклук. Напук на това копи боклучарят ще продължи да ко­пира всичко от една място на друго, което е прахосничество. За да пре­дотвра­тят това някои JVMмашини детектират когато не се прави нов боклук и пре­включват на друга схема (това е “адаптивната” част). Тази друга схема се на­рича mark and sweep и тя е която се използваше през цялото време в ранните вер­сии на виртуалната машина на Sun. За обща употреба схемата "маркирай и из­мети" е доста бавна, но когато не се генерира много или въобще боклук тя е мно­го добра.

"Маркирай и измети" следва същата логика на проследяване на обектите както пре­ди. Обаче при намиране на жив обект в него се установява флаг, с което той се отбелязва, но още не се прави почистването. То става чак като завърши про­це­сът на маркирането. По време на метенето мъртвите обекти се освобождават. Не се прави и компактинг, обаче, така че ако боклучарят иска да уплътни па­мет­та ще го направи в отделен пас.

Името “спри и копирай” е свързано с идеята че събирането на боклука не е фо­нов процес; програмата е спряна пре време на GC. В литературата на Sun може да се намерят много споменавания на събирането на боклука като фонов про­цес с нисък приоритет, но изглежда че това е било теоретичен експеримент без реа­лизация, най-малкото не в ранните версии на JVM на Sun. Вместо това бо­клу­чарят на Sun се задействаше при недостиг на памет. В добавка, "маркирай и из­мети" изисква спиране на програмата.

Както се спомена по-рано, във JVM описвана тук паметта се заема на големи бло­кове. Ако се алокира голям обект той взема свой собствен блок. Стриктното спри-и-копирай изисква копирането на всеки жив обект на новото място преди да може да се освободи старото, което резултира в много памет. С боковете GC ти­пично може да използува мъртвите блокове за копиране в трях в процеса на ра­бота. Всеки блок има брояч на поколението за да се знае дали е жив. В нор­мал­ния случай само блоковете създадени след последния GC се сгъстяват; всич­ки останали отбелязват с брояча си ако на тях има позоваване от някъде. То­ва обслужва нормалния случай на множество късоживеещи обекти. Пе­рио­дич­но се прави пълно измитане – големите обекти още не са копирани и бло­ко­ве­те къито съдържат малки обекти са копирани и сгъстени. JVM следи ефек­тив­ност­та на GC и ако то се окаже загуба на време защото повечето обекти са дъл­го­живеещи, превключва се на маркирай-и-измети. Подобно JVM ,следи до­кол­ко е ефективно маркирай-и-измети и ако хийпът се фрагментира се превключва обрат­но на спри-и-копирай. Това е мястото където работи “адаптивната част”, така че завършваме на един дъх с фразата: “адаптивна генерационна спри-и-ко­пи­рай маркирай-и-измети.”

Има и други допълнителни възможности за ускоряване в JVM-та. Една особено важ­­на включва лоудера и Just-In-Time (JIT) компилатора. Когато трябва да се на­то­вари клас (типично: когато за пръв път ще се създава обект от този клас), .class файлът се намира и кодът се качва в паметта. В тази точка еденият под­ход е просто да се JIT всичкия код, но това има два недостатъка: заема малко по­вече време, което по продължение на програмата може да се натрупа; и уве­ли­чава дължината на изпълнимия модул (байт кодовете са значително по-ком­пакт­ни от разширения JIT код) и така може да се предизвика пейджинг, което опре­делено забавя програмата. Алтернативен подход е мързеливият, което зна­чи че кодът не се JIT компилира докато това не стане необходимо. Така кодът който ни­ко­га не се изпълни може и никога да не се JIT компилира.




Сподели с приятели:
1   ...   21   22   23   24   25   26   27   28   ...   73




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

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