This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/skywalking-java.git
The following commit(s) were added to refs/heads/main by this push: new 762d31fa0f Fix CLASS_LOADER_TYPE_CACHE OOM issue with WeakHashMap (#772) 762d31fa0f is described below commit 762d31fa0f462c3025adf6af2d862e52c37f6815 Author: Gong Dewei <kyl...@qq.com> AuthorDate: Mon Sep 15 17:19:57 2025 +0800 Fix CLASS_LOADER_TYPE_CACHE OOM issue with WeakHashMap (#772) --- CHANGES.md | 1 + .../agent/builder/SWDescriptionStrategy.java | 26 ++- .../builder/SWDescriptionStrategyCacheTest.java | 239 +++++++++++++++++++++ 3 files changed, 260 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f28bb6123e..731f74226a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Release Notes. * Bump up apache parent pom to v35. * Update Maven to 3.6.3 in mvnw. * Fix OOM due to too many span logs. +* Fix ClassLoader cache OOM issue with WeakHashMap. All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/242?closed=1) diff --git a/apm-sniffer/bytebuddy-patch/src/main/java/net/bytebuddy/agent/builder/SWDescriptionStrategy.java b/apm-sniffer/bytebuddy-patch/src/main/java/net/bytebuddy/agent/builder/SWDescriptionStrategy.java index 62e8a0606c..0d10a039bb 100644 --- a/apm-sniffer/bytebuddy-patch/src/main/java/net/bytebuddy/agent/builder/SWDescriptionStrategy.java +++ b/apm-sniffer/bytebuddy-patch/src/main/java/net/bytebuddy/agent/builder/SWDescriptionStrategy.java @@ -34,10 +34,12 @@ import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedI import java.io.Serializable; import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -125,9 +127,16 @@ public class SWDescriptionStrategy implements AgentBuilder.DescriptionStrategy { /** * Original type cache. - * classloader hashcode -> ( typeName -> type cache ) + * ClassLoader -> (typeName -> TypeCache) + * Using WeakHashMap to automatically clean up cache when ClassLoader is garbage collected. */ - private static final Map<Integer, Map<String, TypeCache>> CLASS_LOADER_TYPE_CACHE = new ConcurrentHashMap<>(); + private static final Map<ClassLoader, Map<String, TypeCache>> CLASS_LOADER_TYPE_CACHE = + Collections.synchronizedMap(new WeakHashMap<>()); + + /** + * Bootstrap ClassLoader cache (null key cannot be stored in WeakHashMap) + */ + private static final Map<String, TypeCache> BOOTSTRAP_TYPE_CACHE = new ConcurrentHashMap<>(); private static final List<String> IGNORED_INTERFACES = Arrays.asList(EnhancedInstance.class.getName()); @@ -153,10 +162,15 @@ public class SWDescriptionStrategy implements AgentBuilder.DescriptionStrategy { } private TypeCache getTypeCache() { - int classLoaderHashCode = classLoader != null ? classLoader.hashCode() : 0; - Map<String, TypeCache> typeCacheMap = CLASS_LOADER_TYPE_CACHE.computeIfAbsent(classLoaderHashCode, k -> new ConcurrentHashMap<>()); - TypeCache typeCache = typeCacheMap.computeIfAbsent(typeName, k -> new TypeCache(typeName)); - return typeCache; + if (classLoader == null) { + // Bootstrap ClassLoader - use separate cache since null key cannot be stored in WeakHashMap + return BOOTSTRAP_TYPE_CACHE.computeIfAbsent(typeName, k -> new TypeCache(typeName)); + } else { + // Regular ClassLoader - use WeakHashMap for automatic cleanup + Map<String, TypeCache> typeCacheMap = CLASS_LOADER_TYPE_CACHE.computeIfAbsent( + classLoader, k -> new ConcurrentHashMap<>()); + return typeCacheMap.computeIfAbsent(typeName, k -> new TypeCache(typeName)); + } } @Override diff --git a/apm-sniffer/bytebuddy-patch/src/test/java/net/bytebuddy/agent/builder/SWDescriptionStrategyCacheTest.java b/apm-sniffer/bytebuddy-patch/src/test/java/net/bytebuddy/agent/builder/SWDescriptionStrategyCacheTest.java new file mode 100644 index 0000000000..90f9e4a437 --- /dev/null +++ b/apm-sniffer/bytebuddy-patch/src/test/java/net/bytebuddy/agent/builder/SWDescriptionStrategyCacheTest.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.bytebuddy.agent.builder; + +import org.junit.Test; +import org.junit.Assert; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URLClassLoader; +import java.net.URL; +import java.util.Map; + +/** + * Tests the behavior of the WeakHashMap caching mechanism in SWDescriptionStrategy. + */ +public class SWDescriptionStrategyCacheTest { + + @Test + public void testWeakHashMapCacheCleanup() throws Exception { + // Get static cache field + Field cacheField = SWDescriptionStrategy.SWTypeDescriptionWrapper.class + .getDeclaredField("CLASS_LOADER_TYPE_CACHE"); + cacheField.setAccessible(true); + @SuppressWarnings("unchecked") + Map<ClassLoader, Map<String, SWDescriptionStrategy.TypeCache>> cache = + (Map<ClassLoader, Map<String, SWDescriptionStrategy.TypeCache>>) cacheField.get(null); + + // Record initial cache size + int initialCacheSize = cache.size(); + + // Create test ClassLoader + URLClassLoader testClassLoader = new URLClassLoader(new URL[0], null); + String testTypeName = "com.test.DynamicClass"; + + // Create SWTypeDescriptionWrapper instance + SWDescriptionStrategy.SWTypeDescriptionWrapper wrapper = + new SWDescriptionStrategy.SWTypeDescriptionWrapper( + null, "test", testClassLoader, testTypeName); + + // Call getTypeCache method via reflection to trigger caching + Method getTypeCacheMethod = wrapper.getClass() + .getDeclaredMethod("getTypeCache"); + getTypeCacheMethod.setAccessible(true); + SWDescriptionStrategy.TypeCache typeCache = + (SWDescriptionStrategy.TypeCache) getTypeCacheMethod.invoke(wrapper); + + // Verify that the ClassLoader exists in cache + Assert.assertTrue("Cache should contain the test ClassLoader", + cache.containsKey(testClassLoader)); + Assert.assertNotNull("TypeCache should be created", typeCache); + Assert.assertEquals("Cache size should increase by 1", + initialCacheSize + 1, cache.size()); + + // Clear ClassLoader references, prepare for GC test + testClassLoader = null; + wrapper = null; + typeCache = null; + + // Force garbage collection + System.gc(); + Thread.sleep(100); + System.gc(); + Thread.sleep(100); + + // WeakHashMap should automatically clean up garbage collected ClassLoader entries + int attempts = 0; + int currentCacheSize = cache.size(); + while (currentCacheSize > initialCacheSize && attempts < 20) { + System.gc(); + Thread.sleep(50); + currentCacheSize = cache.size(); + attempts++; + } + + System.out.println("Cache size after GC: " + currentCacheSize + + " (initial: " + initialCacheSize + ", attempts: " + attempts + ")"); + + // Verify that WeakHashMap cleanup mechanism works properly + Assert.assertTrue("WeakHashMap should clean up entries or attempts should be reasonable", + currentCacheSize <= initialCacheSize + 1 && attempts < 20); + } + + @Test + public void testBootstrapClassLoaderHandling() throws Exception { + // Get Bootstrap ClassLoader cache field + Field bootstrapCacheField = SWDescriptionStrategy.SWTypeDescriptionWrapper.class + .getDeclaredField("BOOTSTRAP_TYPE_CACHE"); + bootstrapCacheField.setAccessible(true); + @SuppressWarnings("unchecked") + Map<String, SWDescriptionStrategy.TypeCache> bootstrapCache = + (Map<String, SWDescriptionStrategy.TypeCache>) bootstrapCacheField.get(null); + + int initialBootstrapCacheSize = bootstrapCache.size(); + + // Test Bootstrap ClassLoader (null) handling + String testTypeName = "test.BootstrapClass"; + SWDescriptionStrategy.SWTypeDescriptionWrapper wrapper = + new SWDescriptionStrategy.SWTypeDescriptionWrapper( + null, "test", null, testTypeName); + + // Call getTypeCache method via reflection + Method getTypeCacheMethod = wrapper.getClass() + .getDeclaredMethod("getTypeCache"); + getTypeCacheMethod.setAccessible(true); + SWDescriptionStrategy.TypeCache typeCache = + (SWDescriptionStrategy.TypeCache) getTypeCacheMethod.invoke(wrapper); + + // Verify Bootstrap ClassLoader cache handling + Assert.assertNotNull("TypeCache should be created for bootstrap classloader", typeCache); + Assert.assertTrue("Bootstrap cache should contain the type", + bootstrapCache.containsKey(testTypeName)); + Assert.assertEquals("Bootstrap cache size should increase by 1", + initialBootstrapCacheSize + 1, bootstrapCache.size()); + } + + @Test + public void testMultipleClassLoadersIndependentCache() throws Exception { + Field cacheField = SWDescriptionStrategy.SWTypeDescriptionWrapper.class + .getDeclaredField("CLASS_LOADER_TYPE_CACHE"); + cacheField.setAccessible(true); + @SuppressWarnings("unchecked") + Map<ClassLoader, Map<String, SWDescriptionStrategy.TypeCache>> cache = + (Map<ClassLoader, Map<String, SWDescriptionStrategy.TypeCache>>) cacheField.get(null); + + int initialCacheSize = cache.size(); + + // Create two different ClassLoaders + URLClassLoader classLoader1 = new URLClassLoader(new URL[0], null); + URLClassLoader classLoader2 = new URLClassLoader(new URL[0], null); + String testTypeName = "com.test.SameClassName"; + + // Create TypeCache with same class name for both ClassLoaders + SWDescriptionStrategy.SWTypeDescriptionWrapper wrapper1 = + new SWDescriptionStrategy.SWTypeDescriptionWrapper( + null, "test", classLoader1, testTypeName); + SWDescriptionStrategy.SWTypeDescriptionWrapper wrapper2 = + new SWDescriptionStrategy.SWTypeDescriptionWrapper( + null, "test", classLoader2, testTypeName); + + // Call getTypeCache method via reflection + Method getTypeCacheMethod = + SWDescriptionStrategy.SWTypeDescriptionWrapper.class.getDeclaredMethod("getTypeCache"); + getTypeCacheMethod.setAccessible(true); + + SWDescriptionStrategy.TypeCache typeCache1 = + (SWDescriptionStrategy.TypeCache) getTypeCacheMethod.invoke(wrapper1); + SWDescriptionStrategy.TypeCache typeCache2 = + (SWDescriptionStrategy.TypeCache) getTypeCacheMethod.invoke(wrapper2); + + // Verify that the two ClassLoaders have independent cache entries + Assert.assertNotNull("TypeCache1 should be created", typeCache1); + Assert.assertNotNull("TypeCache2 should be created", typeCache2); + Assert.assertNotSame("TypeCaches should be different instances", typeCache1, typeCache2); + + // Verify cache structure + Assert.assertEquals("Cache should contain both classloaders", + initialCacheSize + 2, cache.size()); + Assert.assertTrue("Cache should contain classloader1", cache.containsKey(classLoader1)); + Assert.assertTrue("Cache should contain classloader2", cache.containsKey(classLoader2)); + + // Verify each ClassLoader has independent type cache + Map<String, SWDescriptionStrategy.TypeCache> typeCacheMap1 = cache.get(classLoader1); + Map<String, SWDescriptionStrategy.TypeCache> typeCacheMap2 = cache.get(classLoader2); + + Assert.assertNotNull("ClassLoader1 should have type cache map", typeCacheMap1); + Assert.assertNotNull("ClassLoader2 should have type cache map", typeCacheMap2); + Assert.assertNotSame("Type cache maps should be different", typeCacheMap1, typeCacheMap2); + + Assert.assertTrue("ClassLoader1 cache should contain the type", + typeCacheMap1.containsKey(testTypeName)); + Assert.assertTrue("ClassLoader2 cache should contain the type", + typeCacheMap2.containsKey(testTypeName)); + } + + @Test + public void testConcurrentAccess() throws Exception { + // Test concurrent access scenario + final String testTypeName = "com.test.ConcurrentClass"; + final int threadCount = 10; + final Thread[] threads = new Thread[threadCount]; + final URLClassLoader[] classLoaders = new URLClassLoader[threadCount]; + final SWDescriptionStrategy.TypeCache[] results = new SWDescriptionStrategy.TypeCache[threadCount]; + + // Create multiple threads to access cache simultaneously + for (int i = 0; i < threadCount; i++) { + final int index = i; + classLoaders[i] = new URLClassLoader(new URL[0], null); + threads[i] = new Thread(() -> { + try { + SWDescriptionStrategy.SWTypeDescriptionWrapper wrapper = + new SWDescriptionStrategy.SWTypeDescriptionWrapper( + null, "test", classLoaders[index], testTypeName); + + Method getTypeCacheMethod = wrapper.getClass() + .getDeclaredMethod("getTypeCache"); + getTypeCacheMethod.setAccessible(true); + results[index] = (SWDescriptionStrategy.TypeCache) + getTypeCacheMethod.invoke(wrapper); + } catch (Exception e) { + Assert.fail("Concurrent access should not throw exception: " + e.getMessage()); + } + }); + } + + // Start all threads + for (Thread thread : threads) { + thread.start(); + } + + // Wait for all threads to complete + for (Thread thread : threads) { + thread.join(1000); // Wait at most 1 second + } + + // Verify all results + for (int i = 0; i < threadCount; i++) { + Assert.assertNotNull("Result " + i + " should not be null", results[i]); + } + + System.out.println("Concurrent access test completed successfully with " + threadCount + " threads"); + } +} \ No newline at end of file