Bug Report: JavacTaskPool memory leak

muyuanjin muyuanjin at qq.com
Wed Apr 24 16:33:33 UTC 2024


System / OS / Java Runtime InformationWindows 11 23H2
openjdk 22.0.1 2024-04-16
OpenJDK Runtime Environment (build 22.0.1+8-16)
OpenJDK 64-Bit Server VM (build 22.0.1+8-16, mixed mode, sharing)

Description
com.sun.tools.javac.code.Scope.ScopeListenerList#listeners  is not cleaned up in time, which leads to the accumulation of List and WeakReference when reusing Context, leading to OOM.


Reproduce
Using JavacTaskPool to compile the same source code repeatedly, the memory usage rises steadily and cannot be recycled until OOM .


Expected Result
The heap memory size is stable and runs continuously.


Actual Result 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.List.of(List.java:1030)
        at ReproduceTest.main(ReproduceTest.java:35)


Source code for an executable test case
import com.sun.tools.javac.api.JavacTaskPool; import javax.tools.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.*; public class ReproduceTest {     private static final String source = """             import java.util.function.BiFunction;             public class LambdaContainer {                 public static BiFunction<Integer, Integer, Integer> getLambda() {                     return (x, y) -> x + y;                 }             }             """;     public static void main(String[] args) throws URISyntaxException {         JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();         JavacTaskPool javacTaskPool = new JavacTaskPool(1);         DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();         StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);         MemoryFileManager memoryFileManager = new MemoryFileManager(fileManager);         List<?> list;         do {             System.gc();             list = javacTaskPool.getTask(null, memoryFileManager, diagnostics,                     List.of("-source", "22", "-target", "22", "-encoding", "UTF-8"), null                     , List.of(new MemoryInputJavaFileObject("LambdaContainer.java", source)), task -> {                         if (task.call()) {                             try (memoryFileManager) {                                 return memoryFileManager.getOutputs();                             } catch (IOException e) {                                 throw new RuntimeException(e);                             }                         }                         return Collections.emptyList();                     });         } while (!list.isEmpty());     }     static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {         private final List<MemoryOutputJavaFileObject> outputs = new ArrayList<>();         // Simulate the cache implementation to speed up the run,         // repeated runs of the same class compilation will not cause the cache memory usage to keep growing         private final Map<String, String> binaryNameCache = new HashMap<>();         private final Map<String, Iterable<JavaFileObject>> fileListCache = new HashMap<>();         protected MemoryFileManager(JavaFileManager fileManager) {             super(fileManager);         }         @Override         public String inferBinaryName(Location location, JavaFileObject file) {             if (file instanceof BinaryJavaFileObject b) {                 String binaryName = b.getBinaryName();                 if (binaryName != null) {                     return binaryName;                 }             }             return binaryNameCache.computeIfAbsent(location.getName() + file.toString(), k -> super.inferBinaryName(location, file));         }         @Override         public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {             if (kind == JavaFileObject.Kind.CLASS) {                 var fileObject = new MemoryOutputJavaFileObject(className);                 outputs.add(fileObject);                 return fileObject;             }             return super.getJavaFileForOutput(location, className, kind, sibling);         }         @Override         public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {             String key = location.getName() + ":" + packageName + ":" + kinds + ":" + recurse;             return fileListCache.computeIfAbsent(key, k -> {                 try {                     return super.list(location, packageName, kinds, recurse);                 } catch (IOException e) {                     return Collections.emptyList();                 }             });         }         public List<MemoryOutputJavaFileObject> getOutputs() {             return new ArrayList<>(outputs);         }         @Override         public void close() throws IOException {             super.close();             outputs.clear();         }     }     interface BinaryJavaFileObject extends JavaFileObject {         String getBinaryName();     }     static class MemoryInputJavaFileObject extends SimpleJavaFileObject {         private final String content;         public MemoryInputJavaFileObject(String uri, String content) throws URISyntaxException {             super(new URI("string:///" + uri), Kind.SOURCE);             this.content = content;         }         @Override         public CharSequence getCharContent(boolean ignoreEncodingErrors) {             return content;         }     }     static class MemoryOutputJavaFileObject extends SimpleJavaFileObject implements BinaryJavaFileObject {         private final ByteArrayOutputStream stream;         private final String binaryName;         public MemoryOutputJavaFileObject(String name) {             super(URI.create("string:///" + name.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);             this.binaryName = name;             this.stream = new ByteArrayOutputStream();         }         public byte[] toByteArray() {             return stream.toByteArray();         }         public String getBinaryName() {             return binaryName;         }         @Override         public InputStream openInputStream() {             return new ByteArrayInputStream(toByteArray());         }         @Override         public ByteArrayOutputStream openOutputStream() {             return this.stream;         }     } } 

Workaround1. Through reflection, after each call to task.call(), clean up context's Types.MembersClosureCache#nilScope And the Scope.ScopeListenerList#listeners of each ClassSymbol in com.sun.tools.javac.code.Symtab#classes.
2. Use agent (attachment MemoryLeakFixAgent.java) to change the Scope.ScopeListenerList to the following implementation:    public static class ScopeListenerList {        Set<ScopeListener> listeners = Collections.newSetFromMap(new WeakHashMap<>());        void add(ScopeListener sl) {            listeners.add(sl);        }        void symbolAdded(Symbol sym, Scope scope) {            walkReferences(sym, scope, false);        }        void symbolRemoved(Symbol sym, Scope scope) {            walkReferences(sym, scope, true);        }        private void walkReferences(Symbol sym, Scope scope, boolean isRemove) {            for (ScopeListener sl : listeners) {                if (isRemove) {                    sl.symbolRemoved(sym, scope);                } else {                    sl.symbolAdded(sym, scope);                }            }        }    }
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/compiler-dev/attachments/20240424/7d8c8597/attachment.htm>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: ReproduceTest.java
Type: application/octet-stream
Size: 5759 bytes
Desc: not available
URL: <https://mail.openjdk.org/pipermail/compiler-dev/attachments/20240424/7d8c8597/ReproduceTest.java>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: MemoryLeakFixAgent.java
Type: application/octet-stream
Size: 17340 bytes
Desc: not available
URL: <https://mail.openjdk.org/pipermail/compiler-dev/attachments/20240424/7d8c8597/MemoryLeakFixAgent.java>


More information about the compiler-dev mailing list