<div dir="ltr"><div dir="ltr"><br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, May 24, 2023 at 7:03 AM David Holmes <<a href="mailto:david.holmes@oracle.com">david.holmes@oracle.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">Hi,<br>
<br>
On 23/05/2023 10:25 pm, Jean-Philippe Bempel wrote:<br>
> Hi all,<br>
> <br>
> We have just identified a Metaspace leak in a very specific case when<br>
> a class has a method using a try-with-resources construct (or similar<br>
> with try-catch) and re-transforming this class in a loop. It is<br>
> reproducible from jdk8 to jdk20.<br>
> Here the steps to reproduce:<br>
> <br>
> 1. create a java file with following content:<br>
> <br>
> public class RetransformLeak {<br>
> public static void main(String[] args) throws Exception {<br>
> new MyClass();<br>
> while (true) {<br>
> Thread.sleep(1000);<br>
> }<br>
> }<br>
> }<br>
> <br>
> class MyClass {<br>
> private static void writeFile() {<br>
<br>
I don't see this ever getting called. Is some code missing?<br>
<br>
> TWR var0 = new TWR();<br>
> try {<br>
> var0.process();<br>
> } catch (Throwable var4) {<br>
> try {<br>
> var0.close();<br>
> } catch (Throwable var3) {<br>
> var4.addSuppressed(var3);<br>
<br>
If commenting this removes the problem then it seems likely the actual <br>
Throwable being thrown as var3 is holding onto some references which <br>
keeps the old class alive. But as close() does nothing I can't see how <br>
it can throw anything. ??<br>
<br>
> }<br>
> throw var4;<br>
> }<br>
> var0.close();<br>
> // try (TWR twr = new TWR()) {<br>
> // twr.process();<br>
> // }<br>
> }<br>
> <br>
> static class TWR implements AutoCloseable {<br>
> public void process() {}<br>
> @Override<br>
> public void close() {}<br>
> }<br>
> }<br>
> <br>
> 2. compile it: javac RetransformLeak.java<br>
> 3. create a java file Agent.java with the following content that will<br>
> be our java agent performing re-transformation:<br>
> <br>
> public class Agent {<br>
> public static void premain(String arg, Instrumentation inst) {<br>
> new Thread(() -> retransformLoop(inst, arg)).start();<br>
> }<br>
> <br>
> private static void retransformLoop(Instrumentation<br>
> instrumentation, String className) {<br>
> Class<?> classToRetransform = null;<br>
> while (true) {<br>
> if (classToRetransform == null) {<br>
> for (Class<?> clazz : instrumentation.getAllLoadedClasses()) {<br>
> if (clazz.getName().equals(className)) {<br>
> System.out.println("found class: " + className);<br>
> classToRetransform = clazz;<br>
> break;<br>
> }<br>
> }<br>
> }<br>
> if (classToRetransform != null) {<br>
> try {<br>
> instrumentation.retransformClasses(classToRetransform);<br>
<br>
What ClassfileTransformer is actually in use here?<br>
<br>
> //Thread.sleep(1);<br>
> } catch (Exception e) {<br>
> throw new RuntimeException(e);<br>
> }<br>
> }<br>
> }<br>
> }<br>
> }<br>
> <br>
> 4. Compile it: javac Agent.java<br>
> 5. create a Manifest.txt file for the java agent:<br>
> <br>
> Premain-Class: Agent<br>
> Can-Retransform-Classes: true<br>
> <br>
> 6. create java agent jar: jar cfm agent.jar Manifest.txt Agent.class<br>
> 7. execute the RetransformLeak class with a max metaspace size:<br>
> java -javaagent:agent.jar=MyClass -XX:MaxMetaspaceSize=128M -cp .<br>
> RetransformLeak<br>
> <br>
> output:<br>
> found class: MyClass<br>
> Exception in thread "Thread-0" java.lang.OutOfMemoryError<br>
> at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native<br>
> Method)<br>
> at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:169)<br>
> at Agent.retransformLoop(Agent.java:22)<br>
> at Agent.lambda$premain$0(Agent.java:5)<br>
> at java.base/java.lang.Thread.run(Thread.java:1623)<br>
> <br>
> If you comment the line:<br>
> var4.addSuppressed(var3);<br>
> in MyClass#writeFile method, no OOME will be thrown and Metaspace will<br>
> remain stable.<br>
<br>
It sounds to me like this isn't a leak as such but rather the exception <br>
keeps things alive. But we need the missing details.<br>
<br></blockquote><div><br></div><div>Yes, that would be helpful.</div><br><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
Cheers,<br>
David<br>
<br>
> You can also directly use a try-with-resources construct to reproduce<br>
> the leak but I have decomposed it with try catch to be able to<br>
> pinpoint more precisely which bytecode may generate the leak.<br>
> <br>
> I can file a bug in OpenJDK jira if needed.<br>
> Thanks<br>
> Jean-Philippe Bempel<br></blockquote><div><br></div><div><div><br></div><div>My first thought was that this is not a leak but a case of class redefinition, where the bytecode is rewritten. The space for the old bytecode gets salvaged and would be re-used for metaspace allocations from the same loader, save that said re-use never happens for some reason. The output of "jcmd VM.metaspace" at the end of the program run would be helpful too.<br></div><div> </div></div><div> </div></div></div>