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,makeConcatWithConstantsand 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'sIndyInterface.bootstrap) still runs. Any otherinvokedynamicruns its bootstrap method once per call site (test/java/IndyCheck.expected, from a classtest/gen/IndyCheckGen.javawrites). A dynamic constant, method handle or method type resolves once per constant-pool entry, however many instructions name it. If that fails with aLinkageError, every later use fails the same way without running the bootstrap again. TheBootstrapMethodErrortexts are the JDK's (test/java/CondyCheck.java). - Reflection matches the JVM, short of a few gaps. Methods, fields, constructors,
invoke,newInstanceand 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 ajava.lang.reflect.Proxyover the JDK'sAnnotationInvocationHandler, 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 answerisSealedandgetPermittedSubclasses(test/java/SealedCheck.java), and field access checks access control asField.getdoes on the JVM (test/java/FieldAccessCheck.java). - Threads take turns. Each Java thread is a real thread, and
synchronized,wait/notify,sleep,interrupt, executors andCompletableFutureall work (test/java/ThreadCheck.java). The main thread ends whenmainreturns, so another thread'smain.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*andunreflect*methods,invokeExact,invoke,invokeWithArguments,asType,bindTo, the spreaders and collectors, the array element handles, the invokers, theMethodHandlescombinators fromzerototryFinallyand 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).invokeExactinsists on the exact type (test/java/ExactCheck.java). - VarHandles work for fields and arrays.
findVarHandle,findStaticVarHandle,unreflectVarHandle,arrayElementVarHandle,byteArrayViewVarHandleandbyteBufferViewVarHandlereturn handles that support the JVM's access modes and throwUnsupportedOperationExceptionfor the rest (test/java/VarHandleCheck.java,test/java/VarHandleModes.java), sojava.util.concurrent.atomicandConcurrentHashMaprun. Memory-ordering modes all act the same, which is safe while one thread runs Java at a time. The six coordinate combinators (insertCoordinatesand friends) work too (test/java/VarHandleCombinatorCheck.java), and so does abyteBufferViewVarHandleover a heap or direct buffer (test/java/ByteBufferViewCheck.java). - Memory segments, downcalls and upcalls work.
MemorySegments over Java arrays and over native memory from anArena(confined, shared, auto or global) read and write through their layouts:getandset,getAtIndex, a layout'svarHandlewith 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, orSystem.loadLibrarywithloaderLookup. It handles integers, floating point and addresses, structs and unions by value, variadic arguments,errnocapture 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.upcallStubturns a method handle into a C function pointer, soqsortcan 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 daemonThread-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) andjava.util.zipcall libc and zlib through Chez's foreign interface, on real file descriptors (test/java/FilesCheck.java,test/java/FileOutputCheck.java).struct statand 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.cprints Linux's). Elsewhere (a BSD, musl, or a glibc older than 2.33, which doesn't exportstatitself),java.nio.filestops with anUnsupportedOperationExceptionat its first use, butjava.iostreams still work.ci/linuxhas 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.getFileStoreandFileSystem.getFileStoresread the mount table,getfsstaton macOS and/proc/mountson Linux (test/java/FileStoreCheck.java).java.util.prefskeeps its nodes where the JVM does (test/java/PrefsCheck.java): under~/.javaon Linux, and in CFPreferences on macOS, in the same~/Library/Preferencesfiles, so a node HotSpot wrote reads back here and the other way round. - Stack traces are the JVM's.
getStackTrace,printStackTrace,Thread.currentThread().getStackTrace(),StackWalkerand the source locationjava.util.logginginfers 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 withCHAMPACA_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 printsNative Method. - The module system boots.
ModuleLayer.boot()holds the modules jlink resolved into the runtime image, soServiceLoadersees 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 aByteBufferheap or direct),Lookup.defineClassanddefineHiddenClass(with class data), andClass.forNamethrough a loader of your own that defines on demand all work. So doesjava.lang.reflect.Proxy: the JDK's ownProxyGeneratorgenerates 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 itsfindClass, as ABCL's does. Each is asked of the loader throughloadClass, 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 anldcof a class (aFoo.classliteral), 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.
ObjectOutputStreamandObjectInputStreamround-trip your classes and the JDK's, and the bytes are the JVM's: the same class descriptors, field order and computedserialVersionUIDs.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, blockingSocketChannels,HttpURLConnectionandInetAddresslookups 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 anInet4Address, 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=trueturns IPv6 off. UDP works too:DatagramSocketandDatagramChannel, 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).NetworkInterfacelists 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.isReachabletries 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.
ProcessBuilderandRuntime.execstart programs throughposix_spawn, with pipes, files, discarded or inherited streams, a changed environment and working directory, the JDK's PATH search, its/bin/shfallback for a script without#!, and its launch error texts.waitFor(with a timeout),destroy,destroyForcibly,onExitandstartPipelinebehave 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.Selectorruns on the JDK's ownKQueueselector on macOS and itsEPollone on Linux. Non-blockingSocketChannels andServerSocketChannels,Pipes,wakeup, interrupts and closing a selector from another thread behave as on the JVM, andHttpClient'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). Aselectthat waits gives the GIL up. - What the VM reports matches.
Thread.getAllStackTracesandenumeratelist the live threads. Another thread's stack reads as on the JVM, line numbers included, wherever it waits: insleep,wait,park, a contended monitor or blocking I/O (test/java/OtherStackCheck.java).ThreadMXBeanmeasures each thread's CPU time, and itsThreadInfo, 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'sNestHostandNestMembersattributes, and an array class has its element's access.Package.getPackageanswers 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.tmpdirandsun.java.commandare what the JVM gives on the same machine (test/java/VmCheck.java). So arenative.encoding,sun.jnu.encodingand 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 writesSystem.outas 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 throughclojure.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 bykotlincruns (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 throughgroovy.ui.GroovyMain: closures, metaclasses andExpando,methodMissing, traits,@CompileStaticclasses, JSON and XML,Eval.me, threads, all through Groovy's owninvokedynamiccall 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 andcall/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 definingScript1each run their own, andFileSystems.newFileSystem(jrt:/, {java.home: H})reads another JDK's image through its ownjrt-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 throwsUnsatisfiedLinkError, as the JVM does for an unloaded one, so a program can catch it (test/java/NativesCheck.java). sun.misc.Unsafewarns 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.accesschanges that as it does on HotSpot.