G1GC – OutOfMemoryError

W tym artykule opiszę sytuację jaką napotkałem podczas porównywania modułów Garbage Collector. W jednym z testów pojawiła się nieoczekiwana sytuacja, a dokładnie brak pamięci w wirtualnej maszynie Javy. Pomimo, że w pozostałych modułach GC test kończył się sukcesem.

Plan badań

Symulacja została przeprowadzona z wykorzystaniem Java wersji 8 i 11. Obie wersje zostały pobrane od podmiotu AdoptOpenJDK.

Java w wersji 8:

  • openjdk version „1.8.0_292”,
  • OpenJDK Runtime Environment (AdoptOpenJDK) (build 1.8.0_292-b10),
  • OpenJDK 64-Bit Server VM (AdoptOpenJDK) (build 25.292-b10, mixed mode).

Java w wersji 11:

  • openjdk version „11.0.11” 2021-04-20
  • OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9 (build 11.0.11+9),
  • OpenJDK 64-Bit Server VM AdoptOpenJDK-11.0.11+9 (build 11.0.11+9, mixed mode).

Do przeprowadzenia testów skorzystano z instancji Google Cloud z parametrami podanymi w tabeli poniżej. Testy zostały przeprowadzone na dwóch różnych odizolowanych instancjach o tych samych parametrach.

Typ maszynye2-standard-4
Pamięć RAM32 GB
ProcesorIntel Xeon E5 v4 (Broadwell E5), 8 procesorów wirtualnych,
bazowa częstotliwość 2.2GHz z przyspieszeniem do 2.8GHz
System operacyjnyCanonical, Ubuntu, 20.04 LTS
Dysk rozruchowy10GB
Szczegóły maszyny wirtualnej

Testy zostały wykonane przy użyciu tego samego kodu źródłowego. Zbudowane i uruchomione na odpowiedniej wersji Javy. Każdy test został uruchomiony z tą samą konfiguracją Javy:

  • -Xmx2g – ustawia maksymalny rozmiar sterty na 2GB,
  • -Xms2g – ustawia początkową wielkość sterty Java na 2GB,
  • -XX:+AlwaysPreTouch – wstępnie wyzerowanie każdej strony sterty na żądanie podczas inicjalizacji maszyny JVM
  • -XX:+UseG1GC – użycie G1GC

Konfiguracja narzędzia JHM dla wszystkich testów wygląda podobnie. Każdy test porównawczy wykorzystuje 5 iteracji rozgrzewających wirtualną maszynę Javy po 10 sekund i 15 iteracji pomiarowych po 10 sekund. Testy zostały wykonane tylko jeden raz z wykorzystaniem jednego wątku.

Algorytm

Zaprojektowano i poddano analizie test, który ma na celu porównanie zachowania modułów GC w przypadku alokowania małych jak i dużych obiektów na stercie. Algorytm zapełnia około 70% sterty obiektami o rozmiarach: 512B, 1KB, 2KB, 10KB, 100KB, 1MB, 2MB, 10MB i 100MB. Implementacja posiada dwie zawierające się w sobie pętle. W wewnętrznej pętli tworzone jest N obiektów i dodawane do lokalnej listy, aby zapełnić 70% sterty. Następnie lista jest przekazywana do innej metody, co oznacza, że obiekty już nie będą używane. Cały ten cykl powtarzany jest 4 razy, aby moduł do oczyszczania pamięci uruchomił się w trakcie testu.

@BenchmarkMode(value = {Mode.AverageTime})
@Fork(value = 1)
@Warmup(iterations = 5)
@Measurement(iterations = 15)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class AllocationBenchmark {

    @State(Scope.Benchmark)
    public static class Plan {
        @Param({"512", "1024", "2048", "10240", "102400", "1048576", "2097152", "10485760", "104857600"})
        public int size;

        public int numberOfObjects;

        @Setup(Level.Iteration)
        public void setUp() {
            final long maxHeap = Runtime.getRuntime().maxMemory();
            numberOfObjects = (int) ((maxHeap * 0.70) / size);
            System.out.println("MaxHeap:" + maxHeap + ", numberOfObjects: " + numberOfObjects);
        }
    }

    @Benchmark
    public void fillHeap(Plan plan, Blackhole blackhole) {
        for (int iter = 0; iter < 4; iter++) {
            List<byte[]> objects = new ArrayList<>(plan.numberOfObjects);
            for (int i = 0; i < plan.numberOfObjects; i++) {
                objects.add(new byte[plan.size]);
            }
            blackhole.consume(objects);
        }
    }
}

Problem

Podczas uruchamiania testu z parametrem do tworzenia tablic o rozmiarze 1MB wystąpił wyjątek java.lang.OutOfMemoryError: Java heap space, który oznacza, że zabrakło pamięci dla tworzonych obiektów. Szczegóły i listę aktywnych ramek stosu z omawianej sytuacji prezentuje poniższy zrzut ekranu.

Szczegóły uruchomienia i ślad stosu podczas wypełniania sterty obiektami o rozmiarze 1MB

Sytuacja miała miejsce w każdej wersji Java. Wspólną konfiguracją był moduł do odśmiecania pamięci z nieużywanych obiektów – G1 Garbage Collector. Aby podkreślić powagę sytuacji, to wyjątek powtórzył się nawet przy domyślnych ustawieniach w Java 11.

Rozwiązanie

Powodem zaistniałej sytuacji jest sposób działania modułu G1GC. Algorytm ma na celu wypełnić 70% pamięci obiektami rozmiaru 1MB. Warto zaznaczyć, że taka sytuacja miała miejsce tylko przy module G1. Opierając się na wiedzy z artykułu G1 Garbage Collector, przeanalizowanej literatury i działania testu możemy przeprowadzić następujące rozumowanie:

  • Jeśli rozmiar sterty jest równy 2GB, to 70% z całkowitego rozmiaru to 1433MB.
  • Jeśli celem jest zapełnienie 70% sterty obiektami o rozmiarze 1MB, to musiałoby powstać około 1433 obiektów.
  • Jeśli całkowity rozmiar pamięci wynosi 2GB, to według przeanalizowanej literatury powinno powstać 2048 regionów o wielkości 1MB.
  • Jeśli rozmiar obiektu jest 1MB, to taki obiekt jest większy od połowy rozmiaru regionu, czyli 512B. A więc obiekty tworzone w teście traktowane są jako Humongous object.
  • Tablica, aby mogła być zaalokowana na stercie potrzebuje dodatkowo 16 bajtów pamięci dla nagłówka.
  • Znając fakty, możemy wywnioskować, że rozmiar regionu jest wynikiem połączenia dwóch sąsiednich regionów, czyli obiekt znajduje się w regionie o rozmiarze 2MB.
  • Cofając się do przeanalizowanej literatury, wiemy, że ogromny region może pomieścić tylko jeden ogromny obiekt.
  • Jeśli algorytm chce zaalokować 1433 obiektów o rozmiarze 1MB + 16B nagłówka, to potrzebne jest co najmniej 1433 ogromnych regionów o wielkości 2MB.

Przeprowadzając takie rozumowanie, możemy wywnioskować, że algorytm próbował zaalokować 1433 obiektów na regionach o wielkości 2MB. Z czego udało mu się zaalokować część, ponieważ G1 dysponował tylko 1024 regionami (specjalne regiony) o wielkości 2MB, stąd brak pamięci. Z jednej strony ułatwia dostęp do takich obiektów dzięki czemu zyskujemy na wydajności, ale z drugiej strony należy mieć na uwadze, że w skrajnych przypadkach może marnować pamięć.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *