* Garbage Collection и метод finalize()

Публикувано на 15 октомври 2009 в раздел ПИК-3 Java.

В програмният език C++ съществува понятието “деструктор” – метод който “почиства” преди даден обект да бъде унищожен. Такъв метод е често използван там, защото в C++ програмистът сам контролира кога един обект да бъде изтрит (чрез “обратния” на new оператор delete).

В Java програмистът няма контрол над унищожението на обектите. Цялата тежест над това е прехвърлена върху т.нар. Garbage Collector. Въпреки това има аналогия на “почистващ метод” (деструктор) – това е метод “finalize()”. Именно той се извиква в момента, в който даден обект е изтрит от Garbage Collector.

За съжаление програмистите, които преминават от C++ към Java ще забележат бързо, че Garbage Collector не се извиква често. При по-малките програми даже е възможно да не бъде извикан въобще. Ето ви един пример:

public class GCExample{
 public static void main(String[] args){
   User[] u = new User[200];
   for (int i=0; i<u.length; i++){
     u[i] = new User();
   }
   System.out.println("Users online: "+User.count);
   System.out.println("Last id: "+User.lastid);
   // "Изтриваме" 10ти елемент
   u[10] = null;
   System.out.println("Users online: "+User.count);
   System.out.println("Last id: "+User.lastid);
 }
}

class User{
  public static int count = 0;
  public static int lastid = 0;
  public int id;
  public User(){
    this.count++;
    this.lastid++;
    this.id = lastid;
  }
  protected void finalize(){
    count--;
  }
}

Освен ако нямате завиден късмет Garbage Collector да се е включил, то резултатът ще бъде следния:

Users online: 200
Last id: 200
Users online: 200
Last id: 200

Виждате, че finalize() методът не е извикан въобще. Това е защото Garbage Collector не се е стартирал все още. Честно казано при тази програма той не би трябвало да се включи въобще. Опитайте чрез добавянето на следния код:

   while(User.count == 200){}
   System.out.println("Garbage Collection was triggered!");

Програмата почти 100% сигурно ще зацикли и никога няма да спре. Ако обаче ние собственоръчно предизвикаме Garbage Collector да се включи, то ще видим, че всичко е наред:

public class GCExample{
 public static void main(String[] args){
   User[] u = new User[200];
   for (int i=0; i<u.length; i++){
     u[i] = new User();
   }
   System.out.println("Users online: "+User.count);
   System.out.println("Last id: "+User.lastid);
   // "Изтриваме" 10ти елемент
   u[10] = null;
   System.gc();
   System.runFinalization();
   System.out.println("Users online: "+User.count);
   System.out.println("Last id: "+User.lastid);
 }
}

Тук резултатът категорично сигурно ще бъде:

Users online: 200
Last id: 200
Users online: 199
Last id: 200

Метод System.gc() извиква Garbage Collector, а System.runFinalization() го кара да почисти абсолютно всичко. Метод finalize() се е изпълнил и е намалил променливата usersonline с единица – точно това, което желаехме. Сега трябва да отговорим на въпроса “защо Garbage Collector не се включи в предишния пример” и по-скоро “кога въобще се включва Garbage Collector сам”.

Отговорът е, че Garbage Collector се включва само и единствено тогава, когато е нужно освобождаване на памет. С други думи – ако JVM изпита недостиг на памет, то тя си освобождава такава, като почиства “боклука”. Ето един пример, с който ще демонстрираме това:

public class GCExample{
 public static void main(String[] args){
   while (GCExample.continueFlag){
     new GCExample();
   }
   // "приспиваме програмата за 2 секунди
   try{
     Thread.sleep(2000);
   }
   catch(Exception e){}
   System.out.println(GCExample.ObjectsCreated);
   System.out.println(GCExample.ObjectsDeleted);
 }
}

class GCExample{
  public static boolean continueFlag = true;
  public static int ObjectsCreated = 0;
  public static int ObjectsDeleted = 0;
  public GCExample(){
    this.ObjectsCreated++;
  }
  protected void finalize(){
    continueFlag = false;
    ObjectsDeleted++;
  }
}

Едно примерно изпълнение би върнало следния резултат:

18580
1917

Виждаме, че общо са се създали 18580 обекта и чак тогава се е включил Garbage Collector. Към нито един от тези създадени обекти няма насочена променлива, но въпреки това Garbage Collector е “събрал” само 1917 от тях. Нарочно “приспахме” програмата за 2 секунди, за да сме сигурни, че той си е свършил работата и не сме го прекъснали с край на програмата ни. Нека все пак демонстрираме, че System.runFinalization() работи коректно:

public class GCExample{
 public static void main(String[] args){
   while (GCExample.continueFlag){
     new GCExample();
   }
   try{
     Thread.sleep(2000);
   }
   catch(Exception e){}

   System.gc();
   System.runFinalization();

   System.out.println(GCExample.ObjectsCreated);
   System.out.println(GCExample.ObjectsDeleted);
 }
}

Резултатът тук вече очаквано ще бъде нещо подобно на:

18688
18688

Първоначално бихме си казали, че “щом желаем да изтрием обект, то можем винаги да си извикваме сами System.gc() и System.runFinalization()”. Това обаче е лоша практика, понеже “събирането на боклука” всъщност е доста тежка процедура, която отнема доста ресурси – повече от процедурата на създаване на обект:

public class GCExample{
 public static void main(String[] args){
   long startCreateObjectsTime = System.currentTimeMillis();
   while (GCExample.continueFlag){
     new GCExample();
   }
   long endCreateObjectsTime = System.currentTimeMillis();

   long startGCTime = System.currentTimeMillis();
   System.gc();
   System.runFinalization();
   long endGCTime = System.currentTimeMillis();

   System.out.println(endCreateObjectsTime - startCreateObjectsTime);
   System.out.println(endGCTime - startGCTime);
 }
}

Примерен резултат е:

15
94

С други думи събирането на боклука е отнело в пъти повече време отколкото създаването на обектите, които биват изтрити. Затова е редно да избягваме честото ръчно пускане на Garbage Collection. Всъщност именно концепцията за Garbage Collection и липсата на възможност за контрол над изтриване на обектите е една от най-честите критики към езика Java.



Един коментар за “Garbage Collection и метод finalize()”


  1. Стефан:

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


Trackback URI | RSS за коментарите

Пусни коментар