Running Chronicle on Java 17 and Later: The Required JVM Arguments, Explained

August 11th, 2026

Chronicle's libraries deliver microsecond latencies across all percentiles, not just on average. Achieving that on the JVM means working closer to the machine than ordinary Java applications ever need to: off-heap memory, memory-mapped files, deterministic deallocation, and a runtime protection layer for our enterprise products. From Java 17 onward, the platform locks down the internal APIs that make this possible, so a small, well-defined set of JVM arguments must be supplied at launch. 

This article explains what changed in the JVM, why Chronicle needs these arguments, what each one does, and exactly how to apply them. It is a companion to the Nexus setup guide; if your build resolves Chronicle artefacts but your application fails on startup, this is the document you want. 

The short version 

On Java 17 and later, start your application with these arguments: 

--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED 
--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED 
--add-exports=java.base/jdk.internal.util=ALL-UNNAMED 
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED 
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED 
--add-opens=java.base/java.io=ALL-UNNAMED 
--add-opens=java.base/java.lang=ALL-UNNAMED 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED 
--add-opens=java.base/java.util=ALL-UNNAMED 
--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED 

Skip to How to apply them for the practical mechanics. The sections in between explain why each one is there, so you can reason about them rather than copy them on trust. 

What changed in the JVM 

For most of Java's history, application code could reach into the platform's internal classes. It was discouraged, but it worked. High-performance libraries relied on it to do things the public API does not expose: allocate and free off-heap memory, address raw memory directly, and control object lifecycles precisely.

Two changes closed that door: 

The Java Platform Module System (Java 9, JEP 261) divided the JDK into modules and drew a boundary around each one. Packages that a module does not explicitly export or open are encapsulated. 

Strong encapsulation by default (Java 16 JEP 396, made permanent in Java 17 JEP 403) turned that boundary from a warning into a hard rule. The --illegal-access escape hatch that bridged the gap on Java 9 to 15 was removed. 

From Java 17, reaching into an encapsulated package is denied unless you grant access explicitly. That is what --add-exports and --add-opens do. They are not a workaround or a hack; they are the supported mechanism the JDK provides for exactly this situation, and Chronicle uses the smallest set that the libraries need. 

--add-exports versus --add-opens 

The two arguments grant different kinds of access, which is why both appear in the list: 

--add-exports MODULE/package=TARGET grants compile-time and link-time access to the public types in an otherwise non-exported package. It is the equivalent of the module having exported that package to you. 

--add-opens MODULE/package=TARGET grants deep reflective access. It allows reflection ( setAccessible(true) ) to read and write any member, including private ones, of every type in the package at runtime. 

In every case here the target is ALL-UNNAMED , meaning the unnamed module that ordinary application classpath code runs in. If you deploy Chronicle on the module path instead of the classpath, the targets change accordingly; contact Chronicle Software if that applies to you. 

Why Chronicle needs them 

Each argument exists to support a specific capability. Grouped by purpose: 

Off-heap memory and direct memory access 

--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED 
--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED 

Chronicle Bytes and Chronicle Core work directly with native memory rather than the Java heap. This avoids garbage-collection pauses on the hot path, which is the single largest source of latency jitter in a typical Java service. The low-level memory primitives that make this possible live in sun.misc.Unsafe and the JDK's internal misc package, both of which are encapsulated by default. 

Deterministic deallocation 

--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED

Off-heap memory is not managed by the garbage collector, so it must be released deterministically and promptly. Chronicle uses the JDK's internal reference and cleaner machinery to tie native memory lifecycles to their owning objects without waiting for a GC cycle. That machinery is in jdk.internal.ref . 

Memory-mapped files 

--add-exports=java.base/sun.nio.ch=ALL-UNNAMED 
--add-opens=java.base/java.io=ALL-UNNAMED 

Chronicle Queue persists messages by memory-mapping files, giving the application shared, page cache-backed access to data with no serialisation overhead on read. Doing this efficiently requires reaching into the NIO channel internals ( sun.nio.ch ) and the file-descriptor plumbing in java.io

Reflective access for serialisation and wire formats 

--add-opens=java.base/java.lang=ALL-UNNAMED 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED 
--add-opens=java.base/java.util=ALL-UNNAMED 
--add-exports=java.base/jdk.internal.util=ALL-UNNAMED 

Chronicle Wire reads and writes objects directly to and from memory in a range of formats. To do so without allocation it accesses object fields reflectively, including members of core java.lang and java.util types. These packages must be opened for that reflection to be permitted. 

Runtime code generation 

--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 
--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED 

To eliminate reflection and interface-dispatch overhead at steady state, Chronicle generates specialised classes at runtime and compiles them in-process. This uses the JDK compiler internals in the jdk.compiler module. 

The enterprise protection layer (JGuard) 

--add-opens=java.base/java.lang=ALL-UNNAMED 

Chronicle's enterprise products are protected by JGuard, which unpacks and defines the protected classes at startup. Defining packages programmatically calls into java.lang.ClassLoader , so java.base/java.lang must be opened. This is the argument whose absence produces the most common and most visible failure, described next. 

What happens if they are missing

Without these arguments, the application fails fast at startup. The enterprise protection layer is usually the first thing to run, so the typical symptom is: 

java.lang.reflect.InaccessibleObjectException: Unable to make protected 
java.lang.Package java.lang.ClassLoader.definePackage(...) accessible: 
module java.base does not "opens java.lang" to unnamed module 

You may also see InaccessibleObjectException referencing other packages, or, on non-enterprise libraries, errors originating from off-heap memory or wire operations. All have the same root cause and the same fix: supply the full set of arguments. A partial set will get you past the first failure only to fail at the next one, so apply them as a block. 

These are launch-time requirements only. They have no effect on compilation, and they do not change the behaviour of your application beyond permitting the access Chronicle needs. 

How to apply them 

Pass the arguments wherever the JVM is launched. The right place depends on how you run your service. 

A run script or command line 

List the arguments before -jar or your main class: 

java \ 
--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED \ 
--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED \ 
--add-exports=java.base/jdk.internal.util=ALL-UNNAMED \ 
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \ 
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ 
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED \ 
--add-opens=java.base/java.io=ALL-UNNAMED \ 
--add-opens=java.base/java.lang=ALL-UNNAMED \ 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \ 
--add-opens=java.base/java.util=ALL-UNNAMED \ 
--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED \ 
-jar your-application.jar 

A JAR manifest 

For an executable JAR, the Add-Opens and Add-Exports manifest headers let you bundle the arguments with the artefact so callers do not have to remember them. List the packages space separated (the java.base/ form, without =ALL-UNNAMED ): 

Add-Opens: java.base/java.lang java.base/java.lang.reflect java.base/java.util java.base/java 
Add-Exports: java.base/jdk.internal.misc java.base/jdk.internal.ref java.base/jdk.internal.ut

A container image 

Set them in the entrypoint or via JAVA_TOOL_OPTIONS , which the JVM picks up automatically: 

ENV JAVA_TOOL_OPTIONS="--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED \ --add-exports=java.base/jdk.internal.ref=ALL-UNNAMED \ 
--add-exports=java.base/jdk.internal.util=ALL-UNNAMED \ 
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \ 
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ 
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED \ 
--add-opens=java.base/java.io=ALL-UNNAMED \ 
--add-opens=java.base/java.lang=ALL-UNNAMED \ 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \ 
--add-opens=java.base/java.util=ALL-UNNAMED \ 
--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED" 

Maven tests 

To run tests against Chronicle, add the arguments to the Surefire (and Failsafe) argLine so they apply to the forked test JVM: 

<plugin> 
<groupId>org.apache.maven.plugins</groupId> 
<artifactId>maven-surefire-plugin</artifactId> 
<configuration> 
<argLine> 
--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED 
--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED 
--add-exports=java.base/jdk.internal.util=ALL-UNNAMED 
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED 
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED 
--add-opens=java.base/java.io=ALL-UNNAMED 
--add-opens=java.base/java.lang=ALL-UNNAMED 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED 
--add-opens=java.base/java.util=ALL-UNNAMED 
--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED 
</argLine> 
</configuration> 
</plugin> 

Your IDE 

Add the same arguments to the run configuration's VM options when launching a main method or tests directly from the IDE. 

Version notes

Java 17 and later: all of the above arguments are required. 

Java 11 to 16: the JVM permits this access with a warning rather than an error. The same arguments are recommended to suppress the warnings and to keep your configuration consistent with Java 17. 

Java 8: no arguments are required; the module system does not exist. 

A note on security 

These arguments widen access only for the modules you name, and only to the unnamed module your own application runs in. They do not disable security manager checks, weaken TLS, or expose anything to code outside your process. They are the same arguments Chronicle's own build and test infrastructure uses, and they are appropriate for production deployment. If your organisation operates a hardened JVM policy and you need to narrow the set further for a specific product, contact Chronicle Software and we will confirm the minimum required for your configuration. 

Questions about your deployment? Contact your Chronicle Software representative or [email protected].