-
Notifications
You must be signed in to change notification settings - Fork 195
[JENKINS-63766] Work around JDK-8231454 #543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
if (current.isNewerThan(new JavaSpecificationVersion("1.8")) | ||
&& current.isOlderThan(new JavaSpecificationVersion("16"))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in JENKINS-63766, this bug was introduced in Java 9 and was fixed in Java 16. The only version of Java in this range that we expect users to be using realistically is Java 11, but cover the whole range just to be safe.
Object cache = cacheF.get(null); | ||
Class<?> cacheC = Class.forName("com.sun.beans.util.Cache"); | ||
if (LOGGER.isLoggable(Level.FINER)) { | ||
LOGGER.log(Level.FINER, "Cleaning up " + clazz.getName() + " from ClassInfo#CACHE."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was tempted to print whether the class was already in the cache or not, as is done elsewhere in this file, but Cache#get
always adds an entry if none exists, and there seems to be no Cache#contains
method, so logging the result of Cache#get
would likely do more harm than good, so I avoided it.
LOGGER.log(Level.FINER, "Cleaning up " + clazz.getName() + " from ClassInfo#CACHE."); | ||
} | ||
Method removeM = cacheC.getMethod("remove", Object.class); | ||
removeM.invoke(cache, clazz); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cache#get
and Cache#remove
synchronize internally on a queue
object, so there is no synchronization needed here.
Upstream fix in openjdk/jdk11u-dev#1103. |
Object cache = cacheF.get(null); | ||
if (cache instanceof ConcurrentMap) { | ||
// Prior to JDK-8277072 | ||
Iterator<? extends Entry<Reference<Class<?>>, ?>> iterator = ((ConcurrentMap) cache).entrySet().iterator(); | ||
while (iterator.hasNext()) { | ||
if (iterator.next().getKey().get() == clazz) { | ||
iterator.remove(); | ||
LOGGER.log(Level.FINER, "cleaning up {0} from ObjectStreamClass.Caches.{1}", new Object[]{clazz.getName(), cacheFName}); | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a separate fix for a related bug I noticed while testing under latest OpenJDK 11.0.16 (in particular, while testing with openjdk/jdk11u-dev#1103). I got the following new error:
java.lang.ClassCastException: class java.io.ObjectStreamClass$Caches$1 cannot be cast to class java.util.concurrent.ConcurrentMap (java.io.ObjectStreamClass$Caches$1 and java.util.concurrent.ConcurrentMap are in module java.base of loader 'bootstrap')
at org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.cleanUpObjectStreamClassCaches(CpsFlowExecution.java:1448)
at org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.cleanUpLoader(CpsFlowExecution.java:1305)
at org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.cleanUpHeap(CpsFlowExecution.java:1270)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:464)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.access$400(CpsThreadGroup.java:95)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:330)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:294)
at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:67)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:139)
at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28)
at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:68)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
Looking into this further, it appears the type of this field was changed in JDK-8277072, which was backported to OpenJDK 11.0.16, which I happened to be testing. Skipping over this logic got the error to go away, and I confirmed that the class was still unloaded when running with JDK-8277072 and without entering this if
statement, so I conclude that JDK-8277072 made this if
statement unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
@@ -1298,6 +1299,7 @@ private static void cleanUpLoader(ClassLoader loader, Set<ClassLoader> encounter | |||
if (encounteredClasses.add(clazz)) { | |||
LOGGER.log(Level.FINER, "found {0}", clazz.getName()); | |||
Introspector.flushFromCaches(clazz); | |||
cleanUpClassInfoCache(clazz); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any idea if the same issue applies to sandboxed non-Pipeline Groovy scripts as well? This code duplicates https://github.com/jenkinsci/script-security-plugin/blob/35f6a0b8207ed3a32a85f27c1312da6cd738eeaa/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript.java#L205-L214.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any idea if the same issue applies to sandboxed non-Pipeline Groovy scripts as well?
No idea.
try { | ||
// TODO Work around JDK-8231454. | ||
Class<?> classInfoC = Class.forName("com.sun.beans.introspect.ClassInfo"); | ||
Field cacheF = classInfoC.getDeclaredField("CACHE"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an aside, does anyone happen to know why this is not just using ClassValue
, which is supposed to handle exactly this use case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this yesterday, too. Ask the OpenJDK people?
src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java
Outdated
Show resolved
Hide resolved
…ion.java Co-authored-by: Jesse Glick <[email protected]>
[JENKINS-63766] Work around JDK-8231454 (cherry picked from commit 0449852)
Background
See JENKINS-63766 and upstream bug JDK-8231454. Users have reported gradual exhaustion of the metaspace when running on Java 11 with Pipeline jobs that define custom classes.
Steps to reproduce
Create a new Pipeline job with
and run it under Java 11 with
-verbose:class
. After the job finishes, runjcmd ${PID} GC.run
Expected results
Note: These are the actual results when running under Java 8 or Java 17.
The
WorkflowScript
andasdf
classes get unloaded.Actual results
The
WorkflowScript
andasdf
classes never get unloaded on Java 11.Evaluation
As described in the upstream bug, a memory memory leak can occur when
java.beans.Introspector#getBeanInfo
is invoked against a class, even ifjava.beans.Introspector#flushFromCaches
is later called on that class. In our case,java.beans.Introspector#getBeanInfo
is invoked from the following stack trace:java.beans.Introspector#flushFromCaches
is later called on that class, but to no avail:Solution
JDK-8231454 solves the problem by removing the class from the
ClassInfo
cache inIntrospector#flushFromCaches
. I plan to backport JDK-8231454 tojdk11u-dev
upstream. In the meantime, this PR works around the problem for Jenkins users by doing the same thing via reflection. This, in turn, requires--add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED
, which is not in core's--add-opens
configuration right now. I plan to advise affected users to add this to their configuration manually, and I plan to follow up with a core PR (and backport) to add this to the--add-opens
configuration in core for current weeklies and LTS releases.Testing done
Java 8 and Java 17
Ran the scenario without any
--add-opens
directives. Verified in a debugger that the new code did not get executed. Verified via verbose class logging that theasdf
class was unloaded.Java 11
Ran the scenario with
--add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED
. Verified in a debugger that the new code was executed and removed theasdf
class from the cache. Verified via verbose class logging that theasdf
class was unloaded.Ran the scenario without any
--add-opens
directives and no extra logging. Verified that no log spam occurred and that theasdf
class was not unloaded.Ran the scenario without any
--add-opens
directives and a custom log recorder. Verified that aFINER
level log was created and that theasdf
class was not unloaded.Java 11 with openjdk/jdk11u-dev#1103
Ran the scenario with and without
--add-opens
. Verified that no log spam occurred and that theasdf
class was unloaded in both cases.