Skip to content

JDTLS (Java Language Server) processes are never terminated after subagent sessions end, causing massive memory leak #15037

Closed

Description

@fashen97

Description

Details

Summary

When OpenCode spawns subagents (via the task() tool), each subagent that invokes LSP tools (e.g., lsp_diagnostics, lsp_goto_definition) may start a new JDTLS (Eclipse JDT Language Server) process. These JDTLS processes are never terminated after the subagent session completes, leading to significant memory accumulation. In a single coding session, I observed 3 orphaned JDTLS instances consuming ~2.2 GB of physical memory combined, with 6 stale temp data directories (~290 MB on disk).

Environment

  • OS: macOS (Apple Silicon)

  • OpenCode binary: ~/.opencode/bin/opencode (113 MB, dated 2025-02-25)

  • JDTLS location: ~/.local/share/opencode/bin/jdtls/ (Eclipse Equinox Launcher 1.7.100.v20251111-0406)

  • Java: /usr/bin/java (JDK 17)

  • Project type: Spring Boot 3.x + MyBatis-Plus (multi-module Maven project, ~60+ source files in service layer)

Reproduction Steps

  1. Open a Java project with OpenCode

  2. Perform a task that triggers subagent delegation — for example, ask the agent to implement a feature that involves:

    • explore subagents for codebase pattern discovery

    • task(category="quick", ...) subagents for code changes

  3. The main agent and each subagent may call LSP tools (lsp_diagnostics, lsp_goto_definition, lsp_find_references, etc.)

  4. After subagents complete and return results, observe that their JDTLS processes remain running

Observed Behavior

Orphaned JDTLS Processes

After a ~1 hour session with 3 subagent delegations, ps shows 3 JDTLS instances still running:

PID   PPID  STARTED                     COMMAND
7579 6839 Wed Feb 25 15:09:06 2026   /usr/bin/java -jar .../jdtls/plugins/org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar ...
8550 6839 Wed Feb 25 15:11:53 2026   /usr/bin/java -jar .../jdtls/plugins/org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar ...
10869 6839 Wed Feb 25 15:30:05 2026   /usr/bin/java -jar .../jdtls/plugins/org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar ...

All have PPID 6839 which is the main opencode process.

Actual Memory Usage (per footprint / Activity Monitor)

The ps aux RSS column dramatically underreports JVM memory. The real physical memory (as reported by macOS footprint command and consistent with Activity Monitor) is:

PID Physical Memory Peak Uptime
7579 751 MB 778 MB 57 min
8550 708 MB 733 MB 54 min
10869 787 MB 810 MB 36 min
Total ~2.2 GB    

The majority of memory is in VM_ALLOCATE (JVM heap) — e.g., 688 MB / 651 MB / 723 MB respectively.

​
Full footprint output for PID 7579
​
java [7579]: 64-bit    Footprint: 751 MB (16384 bytes per page)
​
Dirty     Clean Reclaimable   Regions   Category
688 MB       0 B         0 B       1140   untagged ("VM_ALLOCATE")
20 MB       0 B         0 B         1   MALLOC_NANO
14 MB       0 B         0 B         9   MALLOC_SMALL
11 MB   2432 KB         0 B         20   mapped file
5360 KB       0 B         0 B         14   MALLOC_TINY
3664 KB       0 B         0 B         8   MALLOC_MEDIUM
2512 KB       0 B         0 B       104   stack
751 MB   6592 KB         0 B       3644   TOTAL

</details>

Stale Temp Data Directories

Each JDTLS instance creates a unique data directory in $TMPDIR that is also never cleaned up:

 77M    /tmp/opencode-jdtls-data2F1raj
40M   /tmp/opencode-jdtls-data2L50Y4
40M   /tmp/opencode-jdtls-data9kaBOp
47M   /tmp/opencode-jdtls-dataMYoWmS
44M   /tmp/opencode-jdtls-dataTjKBr1
44M   /tmp/opencode-jdtls-datafK1H27

Note: 6 temp directories exist but only 3 processes are running — suggesting some processes were killed at some point but their data directories were not cleaned up. Combined disk waste: ~292 MB.

Expected Behavior

  1. Process lifecycle: When a subagent session ends (task completes or is cancelled), the JDTLS process it spawned should be terminated (SIGTERM → SIGKILL after timeout).

  2. Singleton or pool: Ideally, there should be at most one JDTLS instance per workspace shared across the main agent and all subagents, rather than spawning a new instance per subagent.

  3. Session cleanup: When the main OpenCode session ends (user quits), all child JDTLS processes should be terminated and temp directories should be removed.

  4. Temp directory cleanup: opencode-jdtls-data* directories should be cleaned up when their associated JDTLS process exits.

Impact

  • Memory: Each JDTLS instance consumes 700-800 MB of physical memory. A session with multiple subagent delegations can easily waste 2-4 GB of RAM.

  • Disk: Stale temp directories accumulate at 40-80 MB each.

  • Scalability: Java projects with large dependency trees (this project has 200+ JARs on the classpath) cause JDTLS to use even more memory for indexing. Users with constrained memory (e.g., 16 GB MacBooks) will hit performance issues quickly.

  • User confusion: Users see multiple java processes in Activity Monitor consuming hundreds of MB each, with no obvious way to identify or manage them.

Suggested Fix

  1. Short-term: Add shutdown hooks / process cleanup when subagent sessions end. Send SIGTERM to the JDTLS process associated with that session. Add a cleanup routine on OpenCode exit to kill all child JDTLS processes and remove opencode-jdtls-data* temp directories.

  2. Long-term: Share a single JDTLS instance across the main agent and all subagents within the same workspace. JDTLS supports concurrent requests via LSP protocol — there's no technical reason to spawn separate instances. This would reduce memory from N × 750 MB to 1 × 750 MB regardless of subagent count.

  3. JVM tuning (complementary): If multiple instances are unavoidable, consider setting -Xmx256m or similar JVM heap limits for JDTLS, as the default allows unbounded growth.

Workaround

Users can manually kill orphaned JDTLS processes:

# Kill all OpenCode JDTLS processespkill -f "opencode-jdtls"​# Clean up stale temp directoriesrm -rf /tmp/opencode-jdtls-data* $TMPDIR/opencode-jdtls-data*

Plugins

No response

OpenCode version

No response

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    perfIndicates a performance issue or need for optimization
    on Feb 25, 2026
  2. ivantimdev commented on Mar 22, 2026

    @ivantimdev

    +1 — reproducing on macOS (Apple Silicon)

    I’m consistently hitting this with OpenCode on a Java project.

    At the moment, I have 2 orphaned JDTLS processes showing as “Not Responding” in Activity Monitor:

    PID Memory Threads
    62377 1.79 GB 342
    72626 1.41 GB 265

    Combined, that is about 3.2 GB of physical memory from just two stuck instances.

    Both processes are unresponsive, and macOS reports them as “Not Responding.” This happens regularly during normal coding sessions. I do not need to explicitly use subagent delegation to trigger it — routine LSP tool invocations seem to be enough for these processes to accumulate over time.

    Environment:

    • macOS (Apple Silicon)
    • OpenCode latest
    • Java project (Spring Boot / Maven)

    Possible root cause

    From a quick look at the code, it seems the issue may be that Instance.state() in lsp/index.ts only runs its cleanup callback when the Instance is destroyed, not when a subagent session ends.

    As a result, each subagent can trigger getClients() → schedule(), which may spawn a new JDTLS process, and those processes can accumulate instead of being cleaned up.

    Possible fix ideas

    1. Track temp data directories on LSPServer.Handle

      • Add something like tempDirs?: string[]
      • Populate it in JDTLS spawn() with the -data path
      • Clean it up in client.shutdown() via fs.rm()
    2. Add a process.on("exit") safety net in lsp/index.ts

      • Kill all tracked child processes when the main OpenCode process exits, regardless of exit path
    3. Longer term, make LSP state truly global rather than per-Instance

      • That would allow subagents to reuse the same JDTLS client for the same workspace root
      • It would also reduce memory pressure from something like N × 1.5 GB down to 1 × 1.5 GB

    The dedup logic in getClients() (s.spawning) already seems to handle concurrent requests within a single state scope. The issue appears to be that subagents may be operating across separate state scopes.

    @thdxr Would a PR for this be welcome? I’d be happy to put together a fix if this matches the intended architecture.

    This is becoming pretty painful in practice, because it can quietly consume several gigabytes of memory during normal work until I manually clean up the stuck JDTLS processes (sometimes system crashes if you forget to do it).

  3. thdxr commented on Apr 2, 2026

    @thdxr
    Member

    Closing in favor of #20695 where we're collecting all memory-related problems. Please follow that issue for updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)perfIndicates a performance issue or need for optimization

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions