Skip to content

[Bug] Manifold leaks JavaParser via self-referential ThreadLocal #742

@Ampflower

Description

@Ampflower

Describe the bug

On reused javac daemons, such as Gradle, Manifold will exhaust the JVM heap after about ~30-50 uses on large codebases or large dependencies with 4 GiB of RAM allocated.

To Reproduce

Steps to reproduce the behavior:

  1. A repo with either a rather large code base, or large dependencies may be required.
    • The repo needs to be using a reusable compiler daemon, normally involving Gradle.
  2. With the repo, you'll want to run a clean compileJava or equivalent several times.
  3. Find that the JVM has exhausted memory after several tries.

Note: this is easier to reproduce if you hard limit the daemon to very little RAM that is still sufficient to compile the application in the first place.

Expected behavior

The daemon to be infinitely reusable without leaking.

Screenshots

Image

I will note, the 'Retained Heap' is deceptive as it is not accounting for the class loader leaked via this way.

Desktop (please complete the following information):

  • OS Type & Version: Arch Linux, Linux 6.17.1-1-cachyos-bore-lto
  • Java/JDK version: Azul 21.0.4
  • Gradle: 8.14
  • IDE version (IntelliJ IDEA or Android Studio): N/A, but: IntelliJ IDEA 2022.2.2
    • This is not required to reproduce the bug.
  • Manifold version: 2025.1.27
  • Manifold IntelliJ plugin version: N/A, but: 2022.2.44
    • This is not required to reproduce the bug.

Additional context

Requires a reusable daemon. The daemon in question must remain enabled and, in the case of Gradle, must not use --no-daemon.

Note: messing with environment variables, including injecting _JAVA_OPTIONS or JAVA_TOOL_OPTIONS in the case of Gradle will spawn a new daemon as it'll deem the old one incompatible. This is also true for messing with JVM flags, or explicitly declaring --no-daemon with an environment-incompatible wrapper.

You may wish to...

  • Use env "_JAVA_OPTIONS=-XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError" <your IDE>
  • Pass it directly via org.gradle.jvmargs=-XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError in gradle.properties, or using the -D argument.

Stack trace

N/A, as relevant exceptions are just thrown indeterminately and not at the actual memleak; but I can share where it is leaking:

### Tree split @ Entry#referent
- at java.lang.ThreadLocal @ 0x727229e00
- at java.lang.ThreadLocal$ThreadLocalMap$Entry#referent
  - note: internally a `WeakReference`
### Tree split @ Entry#value
- at java.lang.ThreadLocal @ 0x727229e00
- at manifold.internal.host.JavacManifoldHost#_javaParser
- at manifold.internal.javac.JavaParser#_host
- at java.lang.ThreadLocal$ThreadLocalMap$Entry#value
  - note: strongly retained
### Root of the stack
- at java.lang.ThreadLocal$ThreadLocalMap$Entry[128]#101
- at java.lang.ThreadLocal$ThreadLocalMap#table
- at java.lang.Thread#threadLocals
- at Thread[/127.0.0.1:57540 to /127.0.01:38471 workers]

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions