Verwaiste JVM nach Session-Ende: Wrapper stirbt, Java-Kind überlebt eingefroren mit ~1 GB #80
Labels
No labels
bug
build
enhancement
headless
P1-critical
P2-high
P3-medium
P4-low
refactoring
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ai-tools/jdt-mcp-server#80
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
Nach dem Ende einer Client-Session (Claude Code) bleibt der JDT-MCP-JVM-Prozess dauerhaft am Leben, ohne noch benutzbar zu sein. Er belegt 0,4–1,1 GB RSS und wird nie beendet. Pro toter Session sammelt sich eine weitere solche Waise an.
Auf einem Entwicklungsrechner (62 GB RAM) haben sich so fünf Waisen mit zusammen ~3,8 GB angesammelt, die älteste 25 Stunden alt. Sie waren mitverantwortlich für einen globalen OOM-Kill am 2026-07-15 12:14:42, bei dem der Kernel einen anderen Prozess (20 GB) abgeschossen hat. Die Waisen waren nicht die Hauptursache dieses OOM, aber sie verkleinern den verfügbaren Speicher dauerhaft und ohne jeden Gegenwert.
Beobachtung
Alle fünf verwaisten JVMs standen im Prozess-Zustand
T(stopped) — sie waren nicht nur nutzlos, sondern eingefroren. Ein gestoppter Prozess verarbeitet SIGTERM nicht; einkill -TERMbleibt wirkungslos. Erstkill -CONTgefolgt vonkill -TERMhat sie beendet.ppid=1394ist der systemd-User-Manager, das heißt die JVMs wurden reparented — ihr ursprünglicher Parent ist weg.Parallel dazu existierten fünf Zombie-Wrapper (
[jdtls-mcp] <defunct>) mit PIDs 126035, 193139, 252060, 304980, 409473. Die PIDs korrespondieren paarweise mit den JVM-Waisen (126035↔126052, 193139↔193161, 252060↔252078, 304980↔304996, 409473↔409490) — jeder Zombie ist der tote Wrapper genau einer Waise.Ursache
Der Launcher
bin/jdtls-mcpstartet die JVM ohneexec:Das ist Absicht — der Wrapper braucht
$EXIT_CODEund$ELAPSEDfür die Workspace-Recovery-Logik (Zeilen 152–171). Die Folge ist aber eine Prozesskette aus zwei Prozessen:bash(Wrapper) →java(JVM).Beendet der Client die Session, stirbt nur der direkte Kindprozess — der bash-Wrapper. Die JVM darunter erfährt davon nichts, wird zu systemd reparented und läuft weiter. Sie erkennt weder das stdin-EOF noch den Tod ihres Parents und terminiert deshalb nie von selbst.
Offen: warum
T (stopped)und nicht einfach „läuft weiter"? Das habe ich nicht abschließend verifiziert, die Prozesse sind inzwischen beendet. Zwei plausible Mechanismen:tostopgesetzt, wird der Prozess gestoppt. Das würde zu #54 passen (34 StellenSystem.err.println).Für die Behebung ist die genaue Antwort zweitrangig: Eine JVM, die den Parent-Tod bemerkt und sich beendet, kommt gar nicht erst in diesen Zustand.
Lösungsvorschlag
Zwei Ebenen, unabhängig voneinander wirksam:
1. JVM-seitig — Parent-Death-Erkennung (die eigentliche Absicherung). Im stdio-Transport einen Watchdog: Liefert
System.inEOF (read() == -1), ist der Client weg — dann geordnet herunterfahren statt weiterzulaufen. Das ist das übliche Lifecycle-Kontrakt für stdio-MCP-Server und wirkt unabhängig davon, wie der Prozess gestartet wurde.2. Wrapper-seitig — Signal-Weiterleitung. Die Recovery-Logik verhindert ein simples
exec, aber der Wrapper kann die JVM im Hintergrund starten, ihre PID merken und Signale weiterreichen:So bleiben
$EXIT_CODE/$ELAPSEDverfügbar, und stirbt der Wrapper, nimmt er die JVM mit.Ergänzend denkbar: die JVM per
prctl(PR_SET_PDEATHSIG)-Äquivalent bzw. über systemd-Scope-Zugehörigkeit binden — beides deutlich aufwendiger als die zwei Punkte oben.Abgrenzung
Nicht zu verwechseln mit #63 (kein
-Xmxim Produkt). #63 begrenzt, wie groß eine laufende JVM werden darf; dieses Issue betrifft, wie lange eine nicht mehr benutzte JVM überhaupt existiert. Ein-Xmxwürde die Waisen kleiner machen, aber nicht beseitigen.Umgebung
~/.local/share/jdtls-mcp/bin/jdtls-mcp, Transport stdio