What matches the JVM

Champaca runs the JDK's own classes, so most behavior is the JVM's by construction. Where the runtime supplies something itself (the natives, the bootstraps it compiles, the threads), it was checked against HotSpot, and the test named in each item below pins what was checked. limitations lists what's missing or different.

  • javac's bootstraps are compiled directly. The translator spots LambdaMetafactory, ObjectMethods, makeConcatWithConstants and the pattern-switch bootstraps by their class as well as their name, and generates their code itself, so a bootstrap of a program's own with one of those names (Groovy's IndyInterface.bootstrap) still runs. Any other invokedynamic runs its bootstrap method once per call site (test/java/IndyCheck.expected, from a class test/gen/IndyCheckGen.java writes). A dynamic constant, method handle or method type resolves once per constant-pool entry, however many instructions name it. If that fails with a LinkageError, every later use fails the same way without running the bootstrap again. The BootstrapMethodError texts are the JDK's (test/java/CondyCheck.java).
  • Reflection matches the JVM, short of a few gaps. Methods, fields, constructors, invoke, newInstance and field get/set work (test/java/Reflect.java). They check the receiver and each argument or value as the JVM does, with the JDK's messages: a primitive widens but never narrows, and a final field can't be set (test/java/ReflectArgCheck.java, test/java/FieldArgCheck.java). invoke, a method handle and a plain call select the method the JVM selects: a private method is its own, and a package-private one is overridden only from its run-time package (test/java/DispatchCheck.java). Annotations with their defaults, generic signatures, and member, local and anonymous classes match too (test/java/AnnotationCheck.java), and an annotation is a java.lang.reflect.Proxy over the JDK's AnnotationInvocationHandler, as on the JVM. Record components, parameter names, checked-exception lists and type annotations match (test/java/RecordCheck.java), so Jackson binds records. Sealed classes and interfaces answer isSealed and getPermittedSubclasses (test/java/SealedCheck.java), and field access checks access control as Field.get does on the JVM (test/java/FieldAccessCheck.java).
  • Threads take turns. Each Java thread is a real thread, and synchronized, wait/notify, sleep, interrupt, executors and CompletableFuture all work (test/java/ThreadCheck.java). The main thread ends when main returns, so another thread's main.join() returns and a shutdown hook sees it terminated (test/java/MainExitCheck.java). One global lock lets only one thread run Java code at a time, though, so threads give correct results and no speedup. Virtual threads run as platform threads, the JDK's fallback when continuations aren't there, which gives the JVM's results without the JVM's cheap concurrency (test/java/VirtualThreadCheck.java).
  • Method handles cover the combinator surface. The Lookup.find* and unreflect* methods, invokeExact, invoke, invokeWithArguments, asType, bindTo, the spreaders and collectors, the array element handles, the invokers, the MethodHandles combinators from zero to tryFinally and the loop builders all work with the JVM's conversions and exception texts (test/java/MethodHandleCheck.java, test/java/MethodHandleCombinatorCheck.java, test/java/LoopHandleCheck.java). Lookups check access as the JVM's do, with its messages: the class named, the member's modifiers against the lookup, findSpecial's caller, and a protected method's receiver narrowed to the lookup class (test/java/LookupAccessCheck.java). invokeExact insists on the exact type (test/java/ExactCheck.java).
  • VarHandles work for fields and arrays. findVarHandle, findStaticVarHandle, unreflectVarHandle, arrayElementVarHandle, byteArrayViewVarHandle and byteBufferViewVarHandle return handles that support the JVM's access modes and throw UnsupportedOperationException for the rest (test/java/VarHandleCheck.java, test/java/VarHandleModes.java), so java.util.concurrent.atomic and ConcurrentHashMap run. Memory-ordering modes all act the same, which is safe while one thread runs Java at a time. The six coordinate combinators (insertCoordinates and friends) work too (test/java/VarHandleCombinatorCheck.java), and so does a byteBufferViewVarHandle over a heap or direct buffer (test/java/ByteBufferViewCheck.java).
  • Memory segments, downcalls and upcalls work. MemorySegments over Java arrays and over native memory from an Arena (confined, shared, auto or global) read and write through their layouts: get and set, getAtIndex, a layout's varHandle with every access mode the JDK gives it, layout paths into structs and sequences, copy, fill, mismatch, strings. The access code is the JDK's own, so bounds, alignment, read-only, closed and wrong-thread accesses fail with the JDK's exceptions and texts (test/java/SegmentCheck.java). An access costs about 0.3 to 0.5 us, against an array element's few nanoseconds. Linker.nativeLinker() calls C functions from the default lookup (libc, libm), SymbolLookup.libraryLookup, or System.loadLibrary with loaderLookup. It handles integers, floating point and addresses, structs and unions by value, variadic arguments, errno capture and critical calls over heap arrays, with the JDK's checks and texts for the rest (test/java/ForeignCheck.java). A critical call takes 0.25 us. One that gives the global lock up so other threads run meanwhile takes 0.6 to 1.3 us, against HotSpot's 5 to 25 ns. Upcalls work too: Linker.upcallStub turns a method handle into a C function pointer, so qsort can sort with a Java comparator. Upcalls take and return structs, and they can come from several Java threads at once or from threads C started, each of which gets a daemon Thread-N, as on HotSpot. A target that throws ends the process after printing its trace, as the JDK's own wrapper does (test/java/UpcallCheck.java). An upcall costs about 1 us, against HotSpot's 60 ns.
  • Files go through libc, on macOS and Linux. java.io, java.nio.file, FileChannel (locks and memory maps included) and java.util.zip call libc and zlib through Chez's foreign interface, on real file descriptors (test/java/FilesCheck.java, test/java/FileOutputCheck.java). struct stat and its relatives are read through a table of offsets measured on each system: macOS (arm64 and x86_64) and Linux with glibc 2.33 or newer on arm64 and x86_64 (ci/linux/layout.c prints Linux's). Elsewhere (a BSD, musl, or a glibc older than 2.33, which doesn't export stat itself), java.nio.file stops with an UnsupportedOperationException at its first use, but java.io streams still work. ci/linux has a Docker image that runs the suite and the libraries on Linux (jolt linux), and CI (.github/workflows/ci.yml) runs the battery there on GitHub's x86_64 and arm64 runners, and on macOS arm64. Files.getFileStore and FileSystem.getFileStores read the mount table, getfsstat on macOS and /proc/mounts on Linux (test/java/FileStoreCheck.java). java.util.prefs keeps its nodes where the JVM does (test/java/PrefsCheck.java): under ~/.java on Linux, and in CFPreferences on macOS, in the same ~/Library/Preferences files, so a node HotSpot wrote reads back here and the other way round.
  • Stack traces are the JVM's. getStackTrace, printStackTrace, Thread.currentThread().getStackTrace(), StackWalker and the source location java.util.logging infers give the same frames as the JVM, with file names and line numbers for your own classes from a class directory (test/java/StackCheck.java). A jar's classes get line numbers with CHAMPACA_LINES=all. Without it, a jar's inlined leaf method that throws from a check shows its caller's frame (limitations). An uncaught exception prints its trace the way the JVM's default handler does. A JDK frame names its file but no line, because the startup cache keeps no inspector information for JDK code: with it, a warm start took 0.04 s longer and the cache grew by half. A native champaca wrote in Scheme has no frame, where the JVM prints Native Method.
  • The module system boots. ModuleLayer.boot() holds the modules jlink resolved into the runtime image, so ServiceLoader sees the JDK's own providers: locale data (1.234,50 €, jeudi 22 octobre), the extended charsets (Shift_JIS, windows-1252), ToolProvider. A JDK class reports its named module, and reflection into a package it doesn't export fails as on the JVM. The application class loader is the system and context class loader. Your classes report it and sit in its unnamed module, while a JDK class reports the loader its module is defined to (test/java/ModuleCheck.java). All this costs a warm start about 0.07 s and a cold one five seconds (performance).
  • Classes can be defined at run time. ClassLoader.defineClass (bytes, or a ByteBuffer heap or direct), Lookup.defineClass and defineHiddenClass (with class data), and Class.forName through a loader of your own that defines on demand all work. So does java.lang.reflect.Proxy: the JDK's own ProxyGenerator generates the proxy class, and the same native defines it (test/java/DefineCheck.java). A defined class reports its defining loader, and a hidden one gets a name of the JVM's shape, Name/0x<n>. A class a loader defines may name others only that loader has, which the loader defines from its findClass, as ABCL's does. Each is asked of the loader through loadClass, as the JVM resolves a reference through the referring class's loader, but here when the method naming it is first translated (limitations). The exception is an ldc of a class (a Foo.class literal), which resolves when it runs, as on the JVM. Reflection resolves a member's types through its declaring class's loader too (test/java/LinkCheck.java). Two loaders can each define a class of the same name, and each one's references resolve through its own loader (limitations).
  • Class definitions are checked as HotSpot checks them. A final superclass, an interface as the superclass, a class among the interfaces, an override of a final method, a class among its own supertypes, and a subclass that a sealed class or interface doesn't permit are refused with HotSpot's error and text. That holds from the class path, from a loader's bytes, from the library cache and at an instruction naming the class (test/java/ClassDefCheck.java, test/java/SealedCheck.java). The one gap is a hand-made lambda over a sealed interface, which is made where the JVM refuses it.
  • Object serialization works. ObjectOutputStream and ObjectInputStream round-trip your classes and the JDK's, and the bytes are the JVM's: the same class descriptors, field order and computed serialVersionUIDs. writeObject/readObject, writeReplace/readResolve, Externalizable, enums, records, arrays, shared references and serializable proxies work. A non-serializable superclass's no-arg constructor runs on deserialization as the specification asks. A stream resolves class names through the first class loader on the stack that isn't a built-in one, as the JVM does, or through the application loader when jolt itself is the reader (test/java/SerialCheck.java, examples/serialization.clj).
  • TCP sockets work, over IPv4 and IPv6. Socket, ServerSocket, blocking SocketChannels, HttpURLConnection and InetAddress lookups call libc the way the file natives do, with timeouts, half-close, the socket options and the JDK's exception texts (test/java/SocketCheck.java). Sockets are dual stack as on the JVM: an IPv6 socket also reaches IPv4 peers, a server on the wildcard sees an IPv4 client as an Inet4Address, and lookups return both kinds of address in the JDK's order (test/java/Ipv6Check.java, the same on a Mac and in a Linux container). -Djava.net.preferIPv4Stack=true turns IPv6 off. UDP works too: DatagramSocket and DatagramChannel, connected or not, blocking, with a timeout or under a selector, with the JVM's truncation, port-unreachable and close behavior (test/java/DatagramCheck.java). NetworkInterface lists every interface with its IPv4 and IPv6 addresses, each IPv6 one scoped to its interface, and on a Mac all twelve matched the JVM's, order and prefixes included. The check covers the loopback interface only, since the rest differ by machine, but on a Mac and in a Linux container every interface's flags, MTU, hardware address and address prefixes were compared by hand with the JVM's and matched. isReachable tries TCP port 7, as the JDK does without root. Multicast works for both families: joins, drops, source-specific joins and blocked sources, with the JDK's own refusals of the source filtering macOS doesn't do (test/java/MulticastCheck.java). It runs over the loopback interface and matches HotSpot on a Mac and in a Linux container. There the loopback carries no IPv6 multicast, so the IPv6 rows check the joins and not the delivery.
  • Child processes work. ProcessBuilder and Runtime.exec start programs through posix_spawn, with pipes, files, discarded or inherited streams, a changed environment and working directory, the JDK's PATH search, its /bin/sh fallback for a script without #!, and its launch error texts. waitFor (with a timeout), destroy, destroyForcibly, onExit and startPipeline behave as on the JVM, and so do exit codes, a signal's included (test/java/ProcessCheck.java). A child gets SIGPIPE's default disposition and none of the parent's other descriptors, as a JVM's does.
  • Selectors work, and so does java.net.http.HttpClient. Selector runs on the JDK's own KQueue selector on macOS and its EPoll one on Linux. Non-blocking SocketChannels and ServerSocketChannels, Pipes, wakeup, interrupts and closing a selector from another thread behave as on the JVM, and HttpClient's HTTP/1.1 requests, sync and async, go through its selector thread (test/java/SelectorCheck.java). HTTPS works through it too, with HTTP/2 negotiated. The tests stay offline, so that was checked by hand against a public site on 2026-10-25 (limitations). A select that waits gives the GIL up.
  • What the VM reports matches. Thread.getAllStackTraces and enumerate list the live threads. Another thread's stack reads as on the JVM, line numbers included, wherever it waits: in sleep, wait, park, a contended monitor or blocking I/O (test/java/OtherStackCheck.java). ThreadMXBean measures each thread's CPU time, and its ThreadInfo, thread dumps and deadlock detection say what each thread waits on, who holds it and what it holds (test/java/ThreadInfoCheck.java). Nests come from the class file's NestHost and NestMembers attributes, and an array class has its element's access. Package.getPackage answers for the JDK's loaded packages, and for each package of yours once one of its classes has loaded, whether or not anything asked a class for its package (test/java/PackageCheck.java). The default locale, java.io.tmpdir and sun.java.command are what the JVM gives on the same machine (test/java/VmCheck.java). So are native.encoding, sun.jnu.encoding and the console streams' encodings, which come from the locale's codeset as the JDK's launcher reads it: a Linux shell with no locale set writes System.out as ASCII, as HotSpot does (test/encoding).
  • Other JVM languages run (jvm-languages has the commands and timings). Each was compared with HotSpot's output by hand, cold and warm. Clojure, Groovy, Scala, Kawa and ABCL are rows of jolt libs (jars and modules), and the Kotlin checks were by hand. - Clojure 1.12.4 runs scripts through clojure.main (protocols, records, multimethods, spec, refs, agents, futures, pprint, eval), its REPL, with HotSpot's line numbers in errors, and AOT compilation whose classes run on either side. Loading Clojure libraries from source is much slower than on HotSpot (26 s warm against 1.5 s for five libraries), since Clojure's compiler runs as translated Java. - A Kotlin 2.2 program compiled by kotlinc runs (data and sealed classes, value classes, delegation, collections and sequences, threads, kotlin-reflect), and so does the Kotlin compiler, whose class files for a program are byte for byte HotSpot's. The compiler prints one extra stderr line where Jansi, its terminal library, can't load its JNI code and falls back. - Groovy 5.0.8 runs scripts through groovy.ui.GroovyMain: closures, metaclasses and Expando, methodMissing, traits, @CompileStatic classes, JSON and XML, Eval.me, threads, all through Groovy's own invokedynamic call sites. Groovy 4.0 can't run on JDK 25's class files, on HotSpot either. - Scala 2.13.18's compiler writes a program's class files byte for byte as on HotSpot, and its runner compiles a source file in memory and runs it. - Kawa 3.1.1 runs Scheme, which it compiles to bytecode form by form, with tail calls and call/cc. - ABCL 1.9.2 runs Common Lisp: CLOS, conditions and restarts, loop, compile, Java interop. Its boot loads each compiled Lisp file through a class loader of its own, several of them defining classes of the same name, which works because classes of one name may come from several loaders. For the same reason two of Groovy's shells defining Script1 each run their own, and FileSystems.newFileSystem(jrt:/, {java.home: H}) reads another JDK's image through its own jrt-fs.jar (checked by hand against GraalVM CE 25.0.1's).
  • No native libraries. A JDK library load (zip, net, management) is a no-op, since its natives are Scheme code (or libc calls) here. A Java library's own JNI code can't load. A JDK native champaca hasn't written yet throws UnsatisfiedLinkError, as the JVM does for an unloaded one, so a program can catch it (test/java/NativesCheck.java).
  • sun.misc.Unsafe warns as on the JVM. A library that still uses it (Guava, Kryo) gets JDK 25's deprecation warning on stderr, word for word, and -Dsun.misc.unsafe.memory.access changes that as it does on HotSpot.