Ciekawy problem z zasobami w Javie
Pracując z Javą zazwyczaj nie zastanawiamy się nad zasobami z jakich korzystamy. W pewnych przypadkach, ściśle określonych zarówno praktyką jak i dokumentacją różnych bibliotek, zwiększamy ilość dostępnej pamięci RAM. Zresztą wielu z was wręcz automatycznie po zainstalowaniu np. Eclipse edytuje ustawienia i podkręca ilość pamięci w celu ulżenia sobie w pracy.
Znacznie rzadziej pojawia się problem związany z ilością otwartych plików. Osobiście spotkałem się z nim tylko raz i to w systemie, który pracował na plikach wykorzystując je m.n. do składowania danych tymczasowych. W tamtym systemie nie było bazy danych jako takiej, a ta która była podłączona służyła tylko do przechowywania stanu aplikacji, ale nie danych.
W takim wypadku wystarczy odpowiednio pobawić się /etc/security/limits.conf i problem znika. Oczywiście jest to rozwiązanie w stylu „eliminację przestępczości narkotykowej osiągnęliśmy dzięki legalizacji narkotyków” i należy poprawić design. No, ale jak to kiedyś ktoś powiedział eliminacja problemów w Unixie polega na sprowadzeniu go do problemu, który nie przeszkadza.
Ja jednak dziś zaliczyłem spotkanie z kolejnym, bardzo rzadko spotykanym problemem związanym z zasobami. Zresztą problem pojawił się już kilka dni temu, ale ze względu na mylący komunikat został zakwalifikowany pod inny „paragraf”. Dziś jednak zagłębiłem się w temacie i chciałbym wam przedstawić w czym rzecz.
Myśląc o zasobach w Javie jak już pisałem myślimy zazwyczaj o RAMie, miejscu na dysku, rzadziej o ilości otwartych plików czy połączeń z bazą danych. Popatrzmy jednak na ten oto stacktrace:
Listing 1. W czym tkwi problem
java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:657)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:943)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1325)
at pl.koziolekweb.blog.threadlimit.App.main(App.java:18)
OutOfMemoryError wyrzucony przy tworzeniu nowego wątku zdaje się wskazywać na brak pamięci dla nowego wątku. Jednak gdy zwiększymy ilość pamięci to efekt będzie ten sam. W dodatku nastąpi mniej więcej po takim samym czasie. Może jakiś wątek nie jest otwierany? Czas na kod:
Listing 2. Przykładowa aplikacja
public class App {
private static final int LIMIT = 33000;
/**
* @param args
*/
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(LIMIT);
int i = 0;
try {
for (i=0; i
<p>Teoretycznie każdy z wątków po zakończeniu swojej radosnej twórczości w postaci wypisania swojej nazwy kończy działalność artystyczną. Zmieńcie limit na jakiś niski typu 3 czy 5. Uruchomcie debuger bez ustawiania breakpointów i obserwujcie ile wątków będzie uruchomionych. Okazuje się, że wątki nie kończą się. Skąd zatem błąd?</p>
<h4>Pula wątków systemu operacyjnego</h4>
<p>Komunikat jest mylący. W każdym systemie operacyjnym, jak i w produktach systemopodobnych, istnieje coś w rodzaju licznika wątków. Nie należy mylić go z licznikiem procesów. W linuxie jest to licznik ogólnosystemowy, czyli określa ilość wszystkich wątków w systemie bez względu, który proces je stworzy, można go podejrzeć za pomocą <samp>cat /proc/sys/kernel/threads-max</samp>, i modyfikować tą wartość <samp>echo N > /proc/sys/kernel/threads-max</samp>, gdzie N to ilość wątków. W Windowsach jest to 2000 wątków na proces, a wynika to z domyślnej ilości alokowanej na wątek pamięci, która wynosi 1MB. Po pomnożeniu tego przez 2000 dostajemy ~2GB, czyli maksymalną ilość pamięci możliwą do zaalokowania przez jeden wątek. Można co prawda trochę podkręcić ten limit, poprzez zmniejszenie ilości pamięci do 4kB, do około 13 tys. (granulacja pamięci to 64kB stąd taki wynik), ale nadal to nie jest to (<a href="http://blogs.msdn.com/b/oldnewthing/archive/2005/07/29/444912.aspx">źródło</a>). </p>
<p>Co ma do tego system? Przecież Java jest niezależna od platformy. Java tak. JVM nie. Maszyna wirtualna zarządza wątkami w ten sposób, że deleguje ten problem do systemu operacyjnego. W momencie wywołania <samp>Thread.start()</samp> wywoływana jest, po przejściu przez ifa i zmianę flagi, metoda natywna. Zresztą widać to na powyższym stacktrace, gdzie błąd pojawia się właśnie w metodzie natywnej.</p>
<p>W linuxie istnieje dodatkowo pewne poważne niebezpieczeństwo. Otóż po przekroczeniu ilości wątków nikt nie może tworzyć nowych wątków! Nawet <samp>root</samp>! Może zatem okazać się, że nie dość, że aplikacja się była wyjebała, to jeszcze nie można się zalogować, ani nawet wywołać prostych poleceń typu <samp>ps aux</samp> czy <samp>top</samp> w celu uzyskania PID procesu JVM i ubiciu aplikacji kill'em. </p>
<h4>Co należy zatem zrobić gdy otrzymamy taki komunikat?</h4>
- Po pierwsze spróbować uzyskać PID. Kilka prób i powinno się udać. Kill wchodzi zazwyczaj za pierwszym razem. Przy czym należy pamiętać, że w HP-UX (tru Unix) kill zachowuje się inaczej niż w linuxie.
- Po drugie zabezpieczyć sobie log i stacktrace. Pozwolą one na określenie co powinniśmy poprawić.
- Po trzecie należy zweryfikować to w jaki sposób tworzymy wątki. Niektóre implementacje, na przykład użyty tu
FixedThreadPool, szkieletu egzekutora wymagają jawnego kończenia wątku. Zatem trzeba ręcznie wątek kończyć. - Po czwarte należy zweryfikować czy aby na pewno nasza architektura i pomysł na aplikację nie powoduje nadmiernego tworzenia wątków działających w tym samym czasie. Jest to szczególnie istotne tam gdzie samodzielnie zarządzamy wątkami. Ważne jest też zamykanie wątków, które zakończyły pracę, ale wiszą na while(true).
W ostateczności możemy spróbować przekonfigurować ustawienia jądra w systemach linuxowych.
// EDIT:
Piotrek Ostrowski w komentarzu zwrócił uwagę, że w nowych Windowsach (Vista i 7) zostało zniesione ograniczenie 2GB pamięci na proces. Z samego rana sprawdziłem zatem co się będzie działo pod 32bitowym Win7 z 4GB RAM.
Wynik jest taki, że kod “wysypał się” w okolicach 5000 wątków, czyli:
- rzeczywiście Win 7 umożliwia adresowanie całej pamięci (~3,8GB) przez pojedynczy proces.
- JVM HotSpot 1.7.0 najprawdopodobniej zmienia (zmniejsza) ilość dostępnej pamięci dla wątku w momencie startu.
Jak ktoś ma Win 7 64bit to niech zrobi test i podzieli się wynikami.