Skip to content

Commit f43221d

Browse files
jbachorikclaude
andcommitted
fix(extension): serialize ClassDataLoader.findClass per class name
Two threads racing findClass for the same name could both reach defineClass, which the JVM rejects with LinkageError. Guard the cache-check + defineClass block with synchronized(getClassLoadingLock(name)) so only one thread per name defines, while a lock-free fast path still handles the warm-cache case for already-loaded classes. The synchronized block is chosen over registerAsParallelCapable because it protects direct findClass callers in addition to the standard loadClass delegation path, and keeps the invariant locally visible in findClass itself. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9d9b6f2 commit f43221d

1 file changed

Lines changed: 26 additions & 13 deletions

File tree

btrace-extension/src/main/java/org/openjdk/btrace/extension/impl/ClassDataLoader.java

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,26 +73,39 @@ public ClassDataLoader(String extensionId, ClassLoader resourceLoader, ClassLoad
7373

7474
@Override
7575
protected Class<?> findClass(String name) throws ClassNotFoundException {
76-
// Check cache first
76+
// Lock-free fast path for warm cache: avoids taking the per-name class-loading lock
77+
// when the class has already been defined.
7778
Class<?> cached = loadedClasses.get(name);
7879
if (cached != null) {
7980
return cached;
8081
}
8182

82-
String resourcePath = name.replace('.', '/') + CLASSDATA_SUFFIX;
83-
byte[] classBytes = loadClassData(resourcePath);
84-
if (classBytes == null) {
85-
throw new ClassNotFoundException(name + " (no .classdata resource found)");
86-
}
83+
// Serialize the defineClass path per name so two threads racing on the same class
84+
// cannot both reach defineClass and trigger a LinkageError. getClassLoadingLock
85+
// returns this when the ClassLoader is not parallel-capable, giving us a single
86+
// lock that still works correctly under all JVM delegation paths (including any
87+
// direct findClass callers that bypass loadClass).
88+
synchronized (getClassLoadingLock(name)) {
89+
cached = loadedClasses.get(name);
90+
if (cached != null) {
91+
return cached;
92+
}
8793

88-
// Validate bytecode before defining - ensures we're loading a valid class file
89-
if (!isValidClassFile(classBytes)) {
90-
throw new ClassNotFoundException(name + " (invalid class file format)");
91-
}
94+
String resourcePath = name.replace('.', '/') + CLASSDATA_SUFFIX;
95+
byte[] classBytes = loadClassData(resourcePath);
96+
if (classBytes == null) {
97+
throw new ClassNotFoundException(name + " (no .classdata resource found)");
98+
}
9299

93-
Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
94-
Class<?> existing = loadedClasses.putIfAbsent(name, clazz);
95-
return existing != null ? existing : clazz;
100+
// Validate bytecode before defining - ensures we're loading a valid class file
101+
if (!isValidClassFile(classBytes)) {
102+
throw new ClassNotFoundException(name + " (invalid class file format)");
103+
}
104+
105+
Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
106+
loadedClasses.put(name, clazz);
107+
return clazz;
108+
}
96109
}
97110

98111
private byte[] loadClassData(String resourcePath) {

0 commit comments

Comments
 (0)