Skip to content

Instantly share code, notes, and snippets.

@parttimenerd
Last active February 21, 2025 12:35
Show Gist options
  • Save parttimenerd/d090497e84483202d39821e66f753eb0 to your computer and use it in GitHub Desktop.
Save parttimenerd/d090497e84483202d39821e66f753eb0 to your computer and use it in GitHub Desktop.
Rapid class load and unload test

This code can be used to test the class load and unload mechanism of the JDK.

It is particularly useful in reproducing a specific bug that concerns the Shenandoah GC and JFR:

Run it via java -XX:+UseShenandoahGC -XX:StartFlightRecording=filename=100us.jfr ClassUnloadTest.java > /dev/null to get

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (/home/i560383/code/jdk/src/hotspot/share/gc/shenandoah/shenandoahHeap.inline.hpp:138), pid=3490611, tid=3490641
#  Error: Shenandoah assert_forwarded failed; Object should be forwarded

Referenced from:
  interior location: 0x00000007ff8b755c
  inside Java heap
    not in collection set
  region: | 3835|R  |Y|BTE    7ff800000,    7fffdbd90,    800000000|TAMS    7ff800000|UWM    7fffdbd90|U  8047K|T     0B|G  7778K|S   269K|L     0B|CP   0

Object:
  0x000000078a0006d0 - klass 0x0000741e7e1c23c0 java.lang.Module
    not allocated after mark start
    not after update watermark
    not marked strong
    not marked weak
        in collection set
  mark: mark(is_unlocked no_hash age=0)
  region: | 3600|CS |Y|BTE    78a000000,    78a800000,    78a800000|TAMS    78a800000|UWM    78a800000|U  8192K|T  8192K|G     0B|S     0B|L  7448B|CP   0

Forwardee:
  (the object itself)

#
# JRE version: OpenJDK Runtime Environment (25.0) (fastdebug build 25-internal-adhoc.i560383.jdk)
# Java VM: OpenJDK 64-Bit Server VM (fastdebug 25-internal-adhoc.i560383.jdk, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, shenandoah gc, linux-amd64)
# Core dump will be written. Default location: Core dumps may be processed with "/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E" (or dumping to /home/i560383/code/jdk/core.3490611)
#
# JFR recording file will be written. Location: /home/i560383/code/jdk/hs_err_pid3490611.jfr
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
#
...
V  [libjvm.so+0x1c2b838]  VMError::report(outputStream*, bool)+0x2e08  (shenandoahHeap.inline.hpp:138)
V  [libjvm.so+0x1c2ed70]  VMError::report_and_die(int, char const*, char const*, __va_list_tag*, Thread*, unsigned char*, void const*, void const*, char const*, int, unsigned long)+0x4f0  (vmError.cpp:1795)
V  [libjvm.so+0xb8bd44]  report_vm_error(char const*, int, char const*, char const*, ...)+0x104  (debug.cpp:196)
V  [libjvm.so+0xb8bd7d]  (debug.cpp:149)
V  [libjvm.so+0x180b4af]  ShenandoahAsserts::print_failure(ShenandoahAsserts::SafeLevel, oop, void*, oop, char const*, char const*, char const*, int)+0x22f  (shenandoahAsserts.cpp:168)
V  [libjvm.so+0x180d7a5]  ShenandoahAsserts::assert_forwarded(void*, oop, char const*, int)+0x295  (shenandoahAsserts.cpp:332)
V  [libjvm.so+0x18a512c]  void ShenandoahHeap::conc_update_with_forwarded<narrowOop>(narrowOop*)+0xac  (shenandoahHeap.inline.hpp:138)
V  [libjvm.so+0x18b2521]  void OopOopIterateDispatch<ShenandoahConcUpdateRefsClosure>::Table::oop_oop_iterate<InstanceMirrorKlass, narrowOop>(ShenandoahConcUpdateRefsClosure*, oop, Klass*)+0x121  (shenandoahClosures.inline.hpp:246)
V  [libjvm.so+0x18a8c65]  void ShenandoahHeap::marked_object_iterate<ShenandoahObjectToOopClosure<ShenandoahConcUpdateRefsClosure> >(ShenandoahHeapRegion*, ShenandoahObjectToOopClosure<ShenandoahConcUpdateRefsClosure>*, HeapWordImpl**)+0xa45  (iterator.inline.hpp:300)
V  [libjvm.so+0x18d901c]  ShenandoahUpdateHeapRefsTask<true>::work(unsigned int)+0x30c  (shenandoahHeap.inline.hpp:630)
V  [libjvm.so+0x1c98dc0]  WorkerThread::run()+0x80  (workerThread.cpp:69)
V  [libjvm.so+0x1b4a7fa]  Thread::call_run()+0xba  (thread.cpp:231)
V  [libjvm.so+0x1646bf8]  thread_native_entry(Thread*)+0x138  (os_linux.cpp:877)
C  [libc.so.6+0xa1e2e]

with fastdebug builds. On normal release builds, you probably get something like:

[1]    3630354 IOT instruction (core dumped)  java -XX:+UseShenandoahGC -XX:StartFlightRecording=filename=100us.jf

This works for the current JDK head, JDK 23 and JDK 21. So I would assume that it affects all JDKs that include the Shenandoah GC at the moment of writing.

License

MIT

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.util.ArrayList;
import java.util.List;
public class ClassUnloadTest {
private static final String CLASS_NAME = "DynamicClass";
private static final String METHOD_NAME = "run";
private static final String SOURCE_CODE =
"public class " + CLASS_NAME + " {" +
" public void " + METHOD_NAME + "() {" +
" double sum = 0;" +
" for (int i = 0; i < 1_000_000; i++) {" +
" sum += Math.sin(i) * Math.cos(i);" + // Expensive computation
" }" +
" System.out.println(\"Computation result: \" + sum);" +
" }" +
"}";
public static void main(String[] args) throws Exception {
int iterations = 100000;
Path tempDir = Files.createTempDirectory("benchmark");
File javaFile = new File(tempDir.toFile(), CLASS_NAME + ".java");
try (FileWriter writer = new FileWriter(javaFile)) {
writer.write(SOURCE_CODE);
}
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, javaFile.getPath());
Path classFile = tempDir.resolve(CLASS_NAME + ".class");
long startTime = System.nanoTime();
// start #cores/2 threads
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < Runtime.getRuntime().availableProcessors() / 2; i++) {
Thread thread = new Thread(() -> {
try {
for (int j = 0; j < iterations; j++) {
loadAndInvoke(classFile);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
thread.start();
threads.add(thread);
}
for (Thread thread : threads) {
thread.join();
}
long duration = System.nanoTime() - startTime;
System.out.println("Executed " + iterations + " iterations in " + (duration / 1_000_000) + " ms");
// Cleanup
Files.deleteIfExists(classFile);
Files.deleteIfExists(javaFile.toPath());
Files.deleteIfExists(tempDir);
}
private static void loadAndInvoke(Path classFile) throws Exception {
URL classUrl = classFile.getParent().toUri().toURL();
try (URLClassLoader classLoader = new URLClassLoader(new URL[]{classUrl}, null)) {
Class<?> dynamicClass = classLoader.loadClass(CLASS_NAME);
Object instance = dynamicClass.getDeclaredConstructor().newInstance();
Method method = dynamicClass.getMethod(METHOD_NAME);
method.invoke(instance);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment