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

Reklamy

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-8154713ExitOnOutOfMemoryError 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.

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).

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ącojvm-classloader-hierarchy

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);
	}
}