Zachowanie GC przy przypisywaniu null do zmiennej referencyjnej

Próbowałem zrozumieć zachowanie GC i znalazłem coś, co mnie interesuje, czego nie jestem w stanie zrozumieć.

Proszę zobaczyć kod i wyjście:

public class GCTest {
    private static int i=0;

    @Override
    protected void finalize() throws Throwable {
        i++; //counting garbage collected objects
    }

    public static void main(String[] args) {        
        GCTest holdLastObject; //If I assign null here then no of eligible objects are 9 otherwise 10.

        for (int i = 0; i < 10; i++) {            
             holdLastObject=new GCTest();             
        }

        System.gc(); //requesting GC

        //sleeping for a while to run after GC.
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // final output 
        System.out.println("`Total no of object garbage collected=`"+i);          
    }
}

W powyższym przykładzie jeśli przypisuję {[1] } do null to otrzymuję Total no of object garbage collected=9. Jeśli nie, dostaję 10.

Czy ktoś może to wyjaśnić? Nie jestem w stanie znaleźć właściwego powodu.
Author: Mureinik, 2015-03-10

3 answers

Podejrzewam, że to z powodu określonego zadania.

Jeśli przypisujesz wartość holdLastObject przed pętlą, jest ona definitywnie przypisana dla całej metody (począwszy od momentu deklaracji) - więc nawet jeśli nie uzyskujesz do niej dostępu po pętli, GC rozumie, że mogłeś napisać kod, który ją accessesuje, więc nie sfinalizuje ostatniej instancji.

Ponieważ nie przypisujesz zmiennej wartości przed pętlą, nie jest ona definitywnie przypisana z wyjątkiem wewnątrz pętli-więc podejrzewam, że GC traktuje ją tak, jakby była zadeklarowana W pętli-wie, że żaden kod po pętli nie może odczytać ze zmiennej (ponieważ nie jest definitywnie przypisana) i wie, że może sfinalizować i zebrać ostatnią instancję.

Dla wyjaśnienia, co mam na myśli, jeśli dodasz:

System.out.println(holdLastObject);

Tuż przed linią System.gc() zobaczysz, że nie będzie kompilowana w pierwszym przypadku(bez przypisania).

Podejrzewam, że to szczegóły maszyny wirtualnej chociaż-mam nadzieję, że Jeśli GC udowodni, że żaden kod nie będzie odczytywany z lokalnej zmiennej, byłoby legalne, aby i tak pobierał ostatnią instancję (nawet jeśli w tej chwili nie jest zaimplementowana w ten sposób).

EDIT: wbrew odpowiedzi TheLostMind, wierzę, że kompilator przekazuje te informacje JVM. Używając javap -verbose GCTest znalazłem to bez przypisania:

  StackMapTable: number_of_entries = 4
    frame_type = 253 /* append */
      offset_delta = 2
      locals = [ top, int ]
    frame_type = 249 /* chop */
      offset_delta = 19
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/InterruptedException ]
    frame_type = 4 /* same */

I to z assigment:

  StackMapTable: number_of_entries = 4
    frame_type = 253 /* append */
      offset_delta = 4
      locals = [ class GCTest, int ]
    frame_type = 250 /* chop */
      offset_delta = 19
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/InterruptedException ]
    frame_type = 4 /* same */

Zwróć uwagę na różnicę w locals część pierwszego wpisu. To dziwne, że wpis class GCTest nie pojawia się gdziekolwiek bez początkowego przypisania...

 9
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-10 07:18:55

Zbadanie kodu bajtowego pomaga odkryć odpowiedź.

Kiedy przypisujesz null do zmiennej lokalnej, jak wspomniał Jon Skeet, jest to pewne przypisanie, a javac musi utworzyć zmienną lokalną w metodzie main., jak dowodzi bytecode:

// access flags 0x9
public static main([Ljava/lang/String;)V
  TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException
 L3
  LINENUMBER 12 L3
  ACONST_NULL
  ASTORE 1

W tym przypadku zmienna lokalna zachowa ostatnią przypisaną wartość i będzie dostępna do zbierania śmieci tylko wtedy, gdy wyjdzie poza zakres. Ponieważ jest zdefiniowany w main, wychodzi poza zakres tylko wtedy, gdy program jest w momencie wydruku i nie jest pobierana.

Jeśli nie przypiszesz do niego wartości, ponieważ nigdy nie jest używana poza pętlą, javac optymalizuje ją do zmiennej lokalnej w zakresie pętli for, która może być oczywiście pobrana przed zakończeniem programu.

Zbadanie kodu bajtowego dla tego scenariusza pokazuje, że brakuje całego bloku LINENUMBER 12, co dowodzi słuszności tej teorii.

Uwaga:
Z tego co wiem, to zachowanie jest nie zdefiniowane przez standard Java i mogą się różnić w zależności od implementacji javac. Obserwowałem to w następującej wersji:

mureinik@computer ~/src/untracked $ javac -version
javac 1.8.0_31
mureinik@computer ~/src/untracked $ java -version
openjdk version "1.8.0_31"
OpenJDK Runtime Environment (build 1.8.0_31-b13)
OpenJDK 64-Bit Server VM (build 25.31-b07, mixed mode)
 11
Author: Mureinik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-10 07:13:04

Nie znalazłem większych różnic w kodzie bajtowym w obu przypadkach (więc nie warto umieszczać kodu bajtowego tutaj). Tak więc moje założenie jest takie, że wynika to z optymalizacji JIT / JVM.

Wyjaśnienie:

Case -1:

public static void main(String[] args) {
  GCTest holdLastObject; //If I assign null here then no of eligible objects are 9 otherwise 10.
     for (int i = 0; i < 10; i++) {
         holdLastObject=new GCTest();
    }
    //System.out.println(holdLastObject); You can't do this here. holdLastObject might not have been initialized.
     System.gc(); //requesting GC
}

Tutaj, zauważ że nie zainicjalizowałeś holdLastObject na null. Tak więc poza pętlą nie można uzyskać do niej dostępu (pojawi się błąd czasu kompilacji). Oznacza to, że * JVM domyśla się, że pole nie jest używane w późniejszej części. Eclipse przekazuje Ci tę wiadomość. Tak więc JVM stworzy i usunie wszystko wewnątrz samej pętli . Więc 10 obiektów zniknęło.

Case -2:

 public static void main(String[] args) {
      GCTest holdLastObject=null; //If I assign null here then no of eligible objects are 9 otherwise 10.
         for (int i = 0; i < 10; i++) {
             holdLastObject=new GCTest();
        }
        //System.out.println(holdLastObject); You can't do this here. holdLastObject might not have been initialized.
         System.gc(); //requesting GC
    }

W tym przypadku, ponieważ pole jest zainicjalizowane do null, to jest tworzone poza pętlą, a zatem null reference jest wepchnięty do swojego gniazda w tabeli zmiennych lokalnych. W ten sposób JVM rozumie, że pole jest Dostępne z zewnątrz, więc nie nie niszczy ostatniej instancji zachowuje ją alive as it is still accessible / readable . Jeśli więc nie ustawisz wyraźnie wartości ostatniego odniesienia NA null, istnieje ono i jest osiągalne. Stąd 9 instancji będzie gotowe do GC.

 6
Author: TheLostMind,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-10 07:00:47