Published on dev.to: Generating JVM memory dumps from JRE
Debugging any JVM in a Docker container
Thanks to JAVA_TOOL_OPTIONS variable it’s easy to run any JVM-based Docker image in debug mode. All we have to do is add environment variable definition „JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005” in docker run or docker-compose.yml and expose port to connect debugger
# docker run
-e "JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" \
-p 5005:5005
# docker-compose
environment:
- "JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
ports:
- 5005:5005
If it's possible, it's handy to extend application entrypoint
# Set debug options if required
if [ x"${JAVA_ENABLE_DEBUG}" != x ] && [ "${JAVA_ENABLE_DEBUG}" != "false" ]; then
java_debug_args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=${JAVA_DEBUG_SUSPEND:-n},address=${JAVA_DEBUG_PORT:-5005}"
fi
On why and how to exit JVM on OnOutOfMemoryError
For a long time all my JVM-based Docker images were configured to exit on OOM error with -XX:OnOutOfMemoryError=”kill -9 %p” (%p is the current JVM process PID placeholder). It works well with XX:+HeapDumpOnOutOfMemoryError, because JVM will dump heap first, and then execute OnOutOfMemoryError command (see the relevant code in vm/utilities/debug.cpp ). But with version 8u92 there’s now a JVM option in the JDK to make the JVM exit when an OutOfMemoryError occurs:
From the release notes:
ExitOnOutOfMemoryError
When you enable this option, the JVM exits on the first occurrence of an out-of-memory error. It can be used if you prefer restarting an instance of the JVM rather than handling out of memory errors.CrashOnOutOfMemoryError
If this option is enabled, when an out-of-memory error occurs, the JVM crashes and produces text and binary crash files.
Enhancement Request: JDK-8138745 (parameter naming is wrong though JDK-8154713, ExitOnOutOfMemoryError
instead of ExitOnOutOfMemory
)
Why exit on OOM? OutOfMemoryError may seem like any other exception, but if it escapes from Thread.run() it will cause thread to die. When thread dies it is no longer a GC root, and thus all references kept only by this thread are eligible for garbage collection. While it means that JVM has a chance recover from OOME, its not recommended that you try. It may work, but it is generally a bad idea. See this answer on SO.
JVM in Docker and PTRACE_ATTACH
Docker nowadays (since 1.10, the original pull request is here docker/docker/#17989) adds some security to running containers by wrapping them in both AppArmor (or presumably SELinux on RedHat systems) and seccomp eBPF based syscall filters (here’s a nice article about it). And ptrace is disabled in the default seccomp profile.
$ docker run alpine sh -c 'apk add -U strace && strace echo' fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz (1/1) Installing strace (4.11-r2) Executing busybox-1.24.2-r11.trigger OK: 6 MiB in 12 packages strace: ptrace(PTRACE_TRACEME, ...): Operation not permitted +++ exited with 1 +++
Why am I writing about this? Because some JDK tools depend on PTRACE_ATTACH on Linux. One of them is very useful jmap.
$ jmap -heap <pid> | |
Attaching to process ID 142, please wait... | |
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 142: Operation not permitted | |
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 142: Operation not permitted | |
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:163) | |
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach(LinuxDebuggerLocal.java:278) | |
at sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:671) | |
at sun.jvm.hotspot.HotSpotAgent.setupDebuggerLinux(HotSpotAgent.java:611) | |
at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:337) | |
at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:304) | |
at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:140) | |
at sun.jvm.hotspot.tools.Tool.start(Tool.java:185) | |
at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118) | |
at sun.jvm.hotspot.tools.HeapSummary.main(HeapSummary.java:49) | |
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) | |
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) | |
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) | |
at java.lang.reflect.Method.invoke(Method.java:498) | |
at sun.tools.jmap.JMap.runTool(JMap.java:201) | |
at sun.tools.jmap.JMap.main(JMap.java:130) | |
Caused by: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 142: Operation not permitted | |
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach0(Native Method) | |
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.access$100(LinuxDebuggerLocal.java:62) | |
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1AttachTask.doit(LinuxDebuggerLocal.java:269) | |
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:138) |
Turning seccomp off (–security-opt seccomp=unconfined) is not recommended, but we can add just this one explicit capability with –cap-add=SYS_PTRACE.
$ docker run --cap-add=SYS_PTRACE alpine sh -c 'apk add -U strace && strace echo' fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz (1/1) Installing strace (4.11-r2) Executing busybox-1.24.2-r11.trigger OK: 6 MiB in 12 packages execve("/bin/echo", ["echo"], [/* 5 vars */]) = 0 arch_prctl(ARCH_SET_FS, 0x7feaca3a8b28) = 0 set_tid_address(0x7feaca3a8b60) = 10 mprotect(0x7feaca3a5000, 4096, PROT_READ) = 0 mprotect(0x558c47ec6000, 16384, PROT_READ) = 0 getuid() = 0 write(1, "\n", 1) = 1 exit_group(0) = ? +++ exited with 0 +++
Docker Compose supports cap_add since version 1.1.0 (2015-02-25).
If you run into an issue with the jmap and jstack from OpenJDK failing with exception java.lang.RuntimeException: unknown CollectedHeap type : class sun.jvm.hotspot.gc_interface.CollectedHeap make sure you install openjdk-debuginfo package (or openjdk-8-dbg or something similiar depending on distro).
$ jmap -heap 66 | |
Attaching to process ID 66, please wait... | |
Debugger attached successfully. | |
Server compiler detected. | |
JVM version is 25.111-b14 | |
using parallel threads in the new generation. | |
using thread-local object allocation. | |
Concurrent Mark-Sweep GC | |
Heap Configuration: | |
MinHeapFreeRatio = 40 | |
MaxHeapFreeRatio = 70 | |
MaxHeapSize = 268435456 (256.0MB) | |
NewSize = 87228416 (83.1875MB) | |
MaxNewSize = 87228416 (83.1875MB) | |
OldSize = 181207040 (172.8125MB) | |
NewRatio = 2 | |
SurvivorRatio = 8 | |
MetaspaceSize = 21807104 (20.796875MB) | |
CompressedClassSpaceSize = 1073741824 (1024.0MB) | |
MaxMetaspaceSize = 17592186044415 MB | |
G1HeapRegionSize = 0 (0.0MB) | |
Heap Usage: | |
Exception in thread "main" java.lang.reflect.InvocationTargetException | |
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) | |
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) | |
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) | |
at java.lang.reflect.Method.invoke(Method.java:498) | |
at sun.tools.jmap.JMap.runTool(JMap.java:201) | |
at sun.tools.jmap.JMap.main(JMap.java:130) | |
Caused by: java.lang.RuntimeException: unknown CollectedHeap type : class sun.jvm.hotspot.gc_interface.CollectedHeap | |
at sun.jvm.hotspot.tools.HeapSummary.run(HeapSummary.java:144) | |
at sun.jvm.hotspot.tools.Tool.startInternal(Tool.java:260) | |
at sun.jvm.hotspot.tools.Tool.start(Tool.java:223) | |
at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118) | |
at sun.jvm.hotspot.tools.HeapSummary.main(HeapSummary.java:49) | |
... 6 more |
Jak ustawić parametr -XX:+HeapDumpOnOutOfMemoryError w serwerze WildFly
Należy w pliku „%JBOSS_HOME%\bin\standalone.conf.bat” ustawić parametry -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<hprof-dump-dir zamieniając linię
set „JAVA_OPTS=-Xms64M -Xmx512M -XX:MaxPermSize=256M” → set „JAVA_OPTS=-Xms64M -Xmx512M -XX:MaxPermSize=256M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=F:\memory-dumps”
Alternatywnym sposobem jest ustawienie zmiennej środowiskowej JAVA_OPTS – skrypt uruchamiający serwer standalone.bat ją uwzględnia.
Można to zrobić na działającym systemie korzystając z JConsole i operacji setVMOption mbeana com.sun.management:type=HotSpotDiagnostic lub korzystając z programu jinfo z JDK.
Sprawdzanie wersji plików JAR
Niedawno znalazłem się w sytuacji, gdy musiałem sprawdzić wersje plików JAR by zapewnić zgodność tworzonego release-a z Javą 6. Nie udało mi się znaleźć gotowego narzędzia, więc napisałem coś własnego: github.com/jarek-przygodzki/jarversion.
Program jest napisany w Scali, ale dla użytkownika nie ma to znaczenia. Po zbudowanie projektu użycia jest bardzo proste
$ jarversion jarversion 1.x Usage: jarversion [options] -j <jar1>,<jar2>... | --jars <jar1>,<jar2>... jars to include --verbose be verbose --help print usage text Print jar file version number defined as the maximum version of all classes contained within jar file $ jarversion --jars "/apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.ibm.icu_54.1.1.v201501272100.jar,/apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.sun.el_2.2.0.v201303151357.jar" /apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.ibm.icu_54.1.1.v201501272100.jar = ClassVersion(49,0) /apps/dev/eclipse-jee-mars-R/eclipse/plugins/com.sun.el_2.2.0.v201303151357.jar = ClassVersion(50,0)
Można oczywiście poprawić kilka rzeczy, w szczególności format wyjścia i prezentować informacje, że np. 45.0 to JDK 1.1 a 52.0 to J2SE 8 ale nawet w obecnej postaci aplikacja spełnia swoje zadanie
Binarne patche w dystrybucji oprogramowanie i uruchamianie bibliotek Javy w .NET
Badałem niedawno kwestię wykorzystania binarnych, przyrostowych patchy w dystrybucji oprogramowania. Głownie interesowały mnie dwie kwestie
- jak to zrobić
- na jakie oszczędności można liczyć
Podsumowując, bardzo pozytywnie zaskoczył mnie projekt xdelta a negatywnie xdeltaencoder. Na korzyść tego drugiego przemawia tylko fakt, że jest napisany w Javie. Wadą obu projektów jest licencja GPLv2 która uniemożliwia ich wykorzystanie w projektach zamkniętym kodzie. GPLv2 jest licencją wirusową – każdy program zlinkowany z biblioteką na takiej licencji to tzw. dzieło pochodne (derived work).
Pomimo rozczarowania postanowiłem przeprowadzić mały eksperyment i uruchomić tą bibliotekę w środowisku .NET korzystając z IKVM.NET co okazało się… zdumiewająco proste. Wynik jest na GitHubie w repozytorium XDeltaEncoderNet
Co ciekawe, patche wygenerowany przez JVM i .NET zwykle odrobinę się różnią, co może wynikać z różnic w implementacji algorytmów kompresji.
Lokalizacja (wersje językowe) komunikatów o błędach z wbudowanego w JRE silnika JavaScript-u w OSGi z wykorzystaniem extension:=extclasspath
Aplikacja nad którą pracuję umożliwia użytkownikom rozszerzenia funkcjonalności przez pisanie skryptów w JS (szczegóły nie są istotne). Oczywiście, gdy pisze się kod pojawiają się komunikaty o błędach a wymaganiem klienta było by były w języku polskim. Ponieważ domyślnie dostępne są dwie wersje językowe – angielska i francuska – stworzony został plik sun/org/mozilla/javascript/internal/resources/Messages_pl.properties z polskimi komunikatami który następnie został opakowany w plik jar sun.org.mozilla.javascript.nl.PL_pl.jar. W końcu plik ten został przekazany jako rozszerzenie bootclasspath aplikacji za pomocą opcji
-Xbootclasspath/a:lib\sun.org.mozilla.javascript.nl.PL_pl.jar
i rozwiązanie takie działo przed dłuższy czas (także w OSGi).
W trakcie migracji na Equinox p2 okazało się jednak, że z wielu względow -Xbootclasspath/a musi odejść – a mnie przypadło wymyślenie jak.
Pierwszym krokiem jest oczywiście ustalenie, dlaczego działało dotychczasowe rozwiązanie. Analiza wykazała, że komunikaty wczytywane są w klasie sun.org.mozilla.javascript.internal.ScriptRuntime wywołaniem
ResourceBundle.getBundle("sun.org.mozilla.javascript.internal.resources.Messages", locale)
a oryginalne lokalizacje (angielska i francuska) umieszczone są w <jre>/lib/resources.jar. Dalsza analiza wykazała, że w tym przypadku do wczytywania lokalizacji wykorzystywany jest tzw. classloader aplikacji. Żeby zrozumieć, dlaczego wyszukiwanie lokalizacji powodzi się trzeba pamiętać o trzech podstawowych faktach dotyczących classloader-ów
- są zorganizowane są w strukturę drzewiastą i powiązane relacją dziecko-rodzic
- zwykle przed wczytaniem klasy\zasobu samemu następuje próba delegacji do rodzica (parent-first delegation model – ale model child-first też jest popularny. Ważne jest to, że classloader „widzi” klasy i zasoby swoje i przodków – ale nie potomków
- główne classloadery (boot, ext, app) zorganizowane są następująco
Podczas wyszukiwania zasobu sun/org/mozilla/javascript/internal/resources/Messages_pl.properties Application Classloader deleguje do Extension ClassLoader który z kolei deleguje do Boostrap ClassLoader który znajduje zasób dzięki -Xbootclasspath – i voilà , mamy polskie komunikaty.
Dla osób dobrze znających OSGi naturalnym sposobem na rozwiązanie przyjazne dla OSGi wydają się tzw. framework extensions czyli pakunki częściowe (fragmenty) których hostem jest pakunek systemowy – system.bundle – i które dodatkowo definiują tzw. punkt rozszerzeń. Zgodnie ze specyfikacją OSGi poprawne są dwie wartości
- framework
- bootclasspath
opisane np. tutaj i w specyfikacji OSGi
Pierwszy jest dla nas bezużyteczny (pytanie dla uważnych – dlaczego). Drugi wydaje się idealny jako odpowiednik parametru -Xbootclasspath ale w praktyce okazuje się, że frameworki OSGI nie implementują go z przyczyn technicznych (patrz np. Eclipse/127724). Co nam pozostaje? Okazuje się, że Equinox OSGi obsługuje niestandardowy punkt rozszerzeń który idealnie sprawdza się w tym przypadku. Można bowiem stworzyć fragment który rozszerzy classloader aplikacji korzystając ze składni Fragment-Host: system.bundle; extension:=extclasspath. Wystarczy stworzyć fragment z następującym manifestem
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Mozilla Rhino Localization PL Bundle-SymbolicName: sun.org.mozilla.javascript.nl.PL_pl Bundle-Version: 1.0.0.qualifier Fragment-Host: system.bundle; extension:=extclasspath Bundle-RequiredExecutionEnvironment: JavaSE-1.6
który zawiera zasób sun/org/mozilla/javascript/internal/resources/Messages_pl.properties
Jak Equinox OSGi obsługuje framework extensions
czyli jak mawia Linus Torvalds Talk is cheap. Show me the code. W tym przypadku kluczowe są dwa miejsca. Pierwszym jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorageHook.loadManifest
gdzie wczytywany jest manifest pakunku
static void loadManifest(BaseData target, Dictionary<String, String> manifest) throws BundleException { try { target.setVersion(Version.parseVersion(manifest.get(Constants.BUNDLE_VERSION))); } catch (IllegalArgumentException e) { target.setVersion(new InvalidVersion(manifest.get(Constants.BUNDLE_VERSION))); } ManifestElement[] bsnHeader = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, manifest.get(Constants.BUNDLE_SYMBOLICNAME)); int bundleType = 0; if (bsnHeader != null) { target.setSymbolicName(bsnHeader[0].getValue()); String singleton = bsnHeader[0].getDirective(Constants.SINGLETON_DIRECTIVE); if (singleton == null) singleton = bsnHeader[0].getAttribute(Constants.SINGLETON_DIRECTIVE); if ("true".equals(singleton)) //$NON-NLS-1$ bundleType |= BundleData.TYPE_SINGLETON; } // check that the classpath is valid String classpath = manifest.get(Constants.BUNDLE_CLASSPATH); ManifestElement.parseHeader(Constants.BUNDLE_CLASSPATH, classpath); target.setClassPathString(classpath); target.setActivator(manifest.get(Constants.BUNDLE_ACTIVATOR)); String host = manifest.get(Constants.FRAGMENT_HOST); if (host != null) { bundleType |= BundleData.TYPE_FRAGMENT; ManifestElement[] hostElement = ManifestElement.parseHeader(Constants.FRAGMENT_HOST, host); if (Constants.getInternalSymbolicName().equals(hostElement[0].getValue()) || Constants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(hostElement[0].getValue())) { String extensionType = hostElement[0].getDirective("extension"); //$NON-NLS-1$ if (extensionType == null || extensionType.equals("framework")) //$NON-NLS-1$ bundleType |= BundleData.TYPE_FRAMEWORK_EXTENSION; else if (extensionType.equals("bootclasspath")) //$NON-NLS-1$ bundleType |= BundleData.TYPE_BOOTCLASSPATH_EXTENSION; else if (extensionType.equals("extclasspath")) //$NON-NLS-1$ bundleType |= BundleData.TYPE_EXTCLASSPATH_EXTENSION; } } else { String composite = manifest.get(COMPOSITE_HEADER); if (composite != null) { if (COMPOSITE_BUNDLE.equals(composite)) bundleType |= BundleData.TYPE_COMPOSITEBUNDLE; else bundleType |= BundleData.TYPE_SURROGATEBUNDLE; } } target.setType(bundleType); target.setExecutionEnvironment(manifest.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT)); target.setDynamicImports(manifest.get(Constants.DYNAMICIMPORT_PACKAGE)); }
drugim jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorage.processExtension
gdzie dane rozszerzenie jest obsługiwane
protected void processExtension(BaseData bundleData, byte type) throws BundleException { if ((bundleData.getType() & BundleData.TYPE_FRAMEWORK_EXTENSION) != 0) { validateExtension(bundleData); processFrameworkExtension(bundleData, type); } else if ((bundleData.getType() & BundleData.TYPE_BOOTCLASSPATH_EXTENSION) != 0) { validateExtension(bundleData); processBootExtension(bundleData, type); } else if ((bundleData.getType() & BundleData.TYPE_EXTCLASSPATH_EXTENSION) != 0) { validateExtension(bundleData); processExtExtension(bundleData, type); } }
OOM w Jetty z powodu zbyt dużej pamięci podręcznej zasobów (org.mortbay.jetty.ResourceCache$Content)
Niedawno zaczęliśmy wykorzystywać Jetty do hostowania repozytoriów P2. Szybko okazało się, że dotychczasowy rozmiar sterty który jak się wydawało w zupełności wystarczał do udostępniania statycznych zasobów (-Xmx256M) okazał się niewystarczający – podczas materializacji produktu P2 z repozytorium proces Jetty kończył się wyjątkiem java.lang.OutOfMemoryError. Doraźnym rozwiązaniem było podwojenie XMX, ale w końcu zrobiony automatycznie zrzut pamięci doczekał się analizy. Analiza to zresztą za dużo powiedziane, bo nigdy jeszcze nie widziałem tak ewidentnej przyczyny – po otwarciu zrzutu pamięci w Eclipse Memory Analyzer (MAT) raport Leak Suspects okazał się aż nadto jednoznaczny.
Co się stało? Okazuje się, że domyślne wartości w klasie org.mortbay.jetty.ResourceCache są bardzo konserwatywne
// jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java private int _maxCachedFileSize =4*1024*1024; private int _maxCachedFiles=2048; private int _maxCacheSize =32*1024*1024;
ale te w w bazowej konfiguracji domyślnego servletu już nie
<init-param> <param-name>maxCacheSize</param-name> <param-value>256000000</param-value> </init-param> <init-param> <param-name>maxCachedFileSize</param-name> <param-value>200000000</param-value> </init-param> <init-param> <param-name>maxCachedFiles</param-name> <param-value>2048</param-value> </init-param>
Spodziewałbym się tutaj chociaż szczątkowej heurystyki – przeznaczenie 256000000 bajtów na pamięć podręczną gdy całkowity rozmiar stery jest niewiele większy (lub mniejszy) nie jest zbyt rozsądną decyzją. Trzymanie w pamięci 200M plików też wydaje się nieco dziwne. Rozwiązaniem jest ustawienie parametru maxCacheSize w WEB-INF/web.xml na wartość o tyle mniejszą od XMX by wystarczyło pamięci do normalnej pracy aplikacji
<servlet> <servlet-name>default</servlet-name> <servlet-class>org.mortbay.jetty.servlet.DefaultServlet</servlet-class> <init-param> <param-name>maxCacheSize</param-name> <!-- 128M - wartość MUSI być mniejsza od XMX --> <param-value>134217728</param-value> </init-param> <init-param> <param-name>maxCachedFileSize</param-name> <!-- Nie umieszczaj w pamięci podręcznej plików większych niż 32M --> <param-value>33554432</param-value> </init-param> </servlet>
Wartość 32M jest empiryczna, przed jej dobraniem sprawdziłem rozmiar największych plików JAR w repozytorium poleceniem (Cygwin is Fun in Terminal)
$ find plugins -name "*.jar" -type f | xargs ls -lhS | head -n 10
Kto wywołuje System.gc() ?
Każdy, kto miał do czynienia z aplikacjami Javy których rozmiar sterty liczy się w gigabajtach wie, że jedną z rzeczy których należy unikać jak ognia jest tzw. odśmiecanie pełne (Full GC). W większości przypadków można to osiągnąć przez rozważny wybór algorytmu GC i staranne dobranie jego parametrów. Nie rozwiązuje to jednak przypadku, gdy cykl pełnego odśmiecania wyzwalany jest przez jawne wywołanie System.gc() co manifestuje się wpisami w logach procesu odśmiecania zaczynających się od Full GC (System) (HotSpot) lub <sys> (IBM J9).
W przypadku maszyny wirtualnej IBM (IBM J9) w celu wykrycia miejsc skąd pochodzą te wywołania wystarczy dodać odpowiednią opcję do parametrów uruchomieniowych maszyny wirtualnej
-Xtrace:print=mt,methods={java/lang/System.gc},trigger=method{java/lang/System.gc,jstacktrace}
W przypadku maszyny Oracle HotSpot sprawa jest nieco trudniejsza. Początkowo planowałem wykorzystać instrumentalizację przez BCEL ale w końcu zdecydowałem się na wykorzystanie BTrace. W tym celu napisałem prosty próbnik
import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.*; @BTrace class SystemGcCalls { @OnMethod(clazz="java.lang.System", method="gc") public void printStack() { Threads.jstack(); println(""); } }
i zainstalowałem go dla testu w działającej instalacji Eclipse (o które wiedziałem, że Full GC jest wywoływane bez potrzeby co wyłączyłem, świadom konsekwencji, za pomocą opcji -XX:+DisableExplicitGC).
$ ./btrace <pid> /Users/Jarek/Code/BTrace/SystemGcCalls.java
Wkrótce moim oczom ukazały się stosy wywołań
java.lang.System.gc(System.java) org.eclipse.ui.internal.ide.application.IDEIdleHelper$3.run(IDEIdleHelper.java:181) org.eclipse.core.internal.jobs.Worker.run(Worker.java:53)
Okazuje się, że klasa org.eclipse.ui.internal.ide.application.IDEIdleHelper
próbuje wykrywać, kiedy IDE jest bezczynne i wywołuje wtedy System.gc(). Po prostu cudownie! Analizując kod klasy IDEIdleHelper
okazuje się, że można doprowadzić do tego, że System.gc()
nie będzie wywołane przez IDE ustawiając parametr „ide.gc” w eclipse.ini
-Dide.gc=false
Dalsza analiza wskazuje na błędy dotyczące tego zachowania zgłoszone do Eclipse Bugzilla: Bug 118335 i Bug 136855. Jest to kolejny przykład na to, że zakładanie że wie się lepiej od JVM kiedy przeprowadzić odśmiecanie rzadko prowadzi do czegokolwiek dobrego.