Прогромистите знаят важността на инициализацията, но често забравят важността на начина на приключване. Най-после, кой има нужда да чисти променлива 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 компилира.
Сподели с приятели: |