|
35 | 35 | import hudson.model.Item;
|
36 | 36 | import hudson.model.TaskListener;
|
37 | 37 | import hudson.util.FormValidation;
|
| 38 | +import io.jenkins.lib.versionnumber.JavaSpecificationVersion; |
38 | 39 |
|
39 | 40 | import java.beans.Introspector;
|
40 | 41 | import java.io.Serializable;
|
@@ -206,6 +207,7 @@ private static void cleanUpClass(Class<?> clazz, Set<ClassLoader> encounteredLoa
|
206 | 207 | if (encounteredClasses.add(clazz)) {
|
207 | 208 | LOGGER.log(Level.FINER, "found {0}", clazz.getName());
|
208 | 209 | Introspector.flushFromCaches(clazz);
|
| 210 | + cleanUpClassInfoCache(clazz); |
209 | 211 | cleanUpGlobalClassSet(clazz);
|
210 | 212 | cleanUpClassHelperCache(clazz);
|
211 | 213 | cleanUpObjectStreamClassCaches(clazz);
|
@@ -266,6 +268,45 @@ private static void cleanUpGlobalClassValue(@NonNull ClassLoader loader) throws
|
266 | 268 | }
|
267 | 269 | }
|
268 | 270 |
|
| 271 | + private static void cleanUpClassInfoCache(Class<?> clazz) { |
| 272 | + JavaSpecificationVersion current = JavaSpecificationVersion.forCurrentJVM(); |
| 273 | + if (current.isNewerThan(new JavaSpecificationVersion("1.8")) |
| 274 | + && current.isOlderThan(new JavaSpecificationVersion("16"))) { |
| 275 | + try { |
| 276 | + // TODO Work around JDK-8231454. |
| 277 | + Class<?> classInfoC = Class.forName("com.sun.beans.introspect.ClassInfo"); |
| 278 | + Field cacheF = classInfoC.getDeclaredField("CACHE"); |
| 279 | + try { |
| 280 | + cacheF.setAccessible(true); |
| 281 | + } catch (RuntimeException e) { // TODO Java 9+ InaccessibleObjectException |
| 282 | + /* |
| 283 | + * Not running with "--add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED". |
| 284 | + * Until core adds this to its --add-opens configuration, and until that core |
| 285 | + * change is widely adopted, avoid unnecessary log spam and return early. |
| 286 | + */ |
| 287 | + if (LOGGER.isLoggable(Level.FINER)) { |
| 288 | + LOGGER.log(Level.FINER, "Failed to clean up " + clazz.getName() + " from ClassInfo#CACHE. A metaspace leak may have occurred.", e); |
| 289 | + } |
| 290 | + return; |
| 291 | + } |
| 292 | + Object cache = cacheF.get(null); |
| 293 | + Class<?> cacheC = Class.forName("com.sun.beans.util.Cache"); |
| 294 | + if (LOGGER.isLoggable(Level.FINER)) { |
| 295 | + LOGGER.log(Level.FINER, "Cleaning up " + clazz.getName() + " from ClassInfo#CACHE."); |
| 296 | + } |
| 297 | + Method removeM = cacheC.getMethod("remove", Object.class); |
| 298 | + removeM.invoke(cache, clazz); |
| 299 | + } catch (ReflectiveOperationException e) { |
| 300 | + /* |
| 301 | + * Should never happen, but if it does, ensure the failure is isolated to this |
| 302 | + * method and does not prevent other cleanup logic from executing. |
| 303 | + */ |
| 304 | + LOGGER.log(Level.WARNING, "Failed to clean up " + clazz.getName() + " from ClassInfo#CACHE. A metaspace leak may have occurred.", e); |
| 305 | + } |
| 306 | + } |
| 307 | + } |
| 308 | + |
| 309 | + |
269 | 310 | private static void cleanUpGlobalClassSet(@NonNull Class<?> clazz) throws Exception {
|
270 | 311 | Class<?> classInfoC = Class.forName("org.codehaus.groovy.reflection.ClassInfo"); // or just ClassInfo.class, but unclear whether this will always be there
|
271 | 312 | Field globalClassSetF = classInfoC.getDeclaredField("globalClassSet");
|
@@ -313,15 +354,18 @@ private static void cleanUpObjectStreamClassCaches(@NonNull Class<?> clazz) thro
|
313 | 354 | for (String cacheFName : new String[] {"localDescs", "reflectors"}) {
|
314 | 355 | Field cacheF = cachesC.getDeclaredField(cacheFName);
|
315 | 356 | cacheF.setAccessible(true);
|
316 |
| - ConcurrentMap<Reference<Class<?>>, ?> cache = (ConcurrentMap) cacheF.get(null); |
317 |
| - Iterator<? extends Map.Entry<Reference<Class<?>>, ?>> iterator = cache.entrySet().iterator(); |
318 |
| - while (iterator.hasNext()) { |
319 |
| - if (iterator.next().getKey().get() == clazz) { |
320 |
| - iterator.remove(); |
321 |
| - if (LOGGER.isLoggable(Level.FINER)) { |
322 |
| - LOGGER.log(Level.FINER, "cleaning up {0} from ObjectStreamClass.Caches.{1}", new Object[] {clazz.getName(), cacheFName}); |
| 357 | + Object cache = cacheF.get(null); |
| 358 | + if (cache instanceof ConcurrentMap) { |
| 359 | + // Prior to JDK-8277072 |
| 360 | + Iterator<? extends Map.Entry<Reference<Class<?>>, ?>> iterator = ((ConcurrentMap) cache).entrySet().iterator(); |
| 361 | + while (iterator.hasNext()) { |
| 362 | + if (iterator.next().getKey().get() == clazz) { |
| 363 | + iterator.remove(); |
| 364 | + if (LOGGER.isLoggable(Level.FINER)) { |
| 365 | + LOGGER.log(Level.FINER, "cleaning up {0} from ObjectStreamClass.Caches.{1}", new Object[]{clazz.getName(), cacheFName}); |
| 366 | + } |
| 367 | + break; |
323 | 368 | }
|
324 |
| - break; |
325 | 369 | }
|
326 | 370 | }
|
327 | 371 | }
|
|
0 commit comments