🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// DOC-DERIVED — wording stable on JDK 16+ (JEP 396/403); hex id varies per run:
Exception in thread "main" java.lang.IllegalAccessError: class com.devinhyderabad.tools.Profiler cannot access class sun.nio.ch.DirectBuffer (in module java.base) because module java.base does not export sun.nio.ch to unnamed module @1a6c5a8e
at com.devinhyderabad.tools.Profiler.sample(Profiler.java:22)
at com.devinhyderabad.app.Main.main(Main.java:6)⚡ Quick Fix Works 80% of the time
Replace the internal API with its supported counterpart; if migration must wait, add a targeted --add-exports bridge for exactly the package and module involved.
# bridge, scoped tight (production-safe interim measure)
java --add-exports=java.base/sun.nio.ch=ALL-UNNAMED -jar app.jar
# in Dockerfile / systemd unit:
ENV JAVA_TOOL_OPTIONS="--add-exports=java.base/sun.nio.ch=ALL-UNNAMED"🧠 Why this Happens
Tap to expand the deep technical explanation
Since Java 9 every type belongs to a module, and cross-module access requires the exporting module to open that package to the reader. Classpath code forms the unnamed module, which may only read packages another module exports publicly. sun.nio.ch was always internal; before Java 16 illegal access merely warned. JEP 396 made strong encapsulation the default in 16 and JEP 403 removed the last override switch in 17 — so at first linkage to DirectBuffer the access check fails and the JVM throws IllegalAccessError, not a warning.
The HITEC City Parking Spot Analogy:
For years a side door in the office was unlocked with a polite sign asking staff not to use it. Renovation welded it shut — anyone who kept using the shortcut now finds a wall.
🔁 How to Reproduce Confirm this is your error
Reference any java.base internal package (sun.misc.Unsafe, sun.nio.ch.DirectBuffer) from plain classpath code on JDK 17+ and invoke it. Compile-time javac already refuses; reflective or bytecode-driven access fails at runtime with this wording. DOC-DERIVED.
🛠️ Solutions (5 Ways to Fix)
Migrate to the supported replacement API
👉 Use this as the durable fix whenever the library or your own code owns the offending call sites.
Every popular internal has an official successor: sun.misc.Unsafe field access becomes VarHandle via MethodHandles.Lookup; sun.nio.ch.DirectBuffer becomes the Foreign Function & Memory API (finalized in 22, preview earlier); CORBA-era internals have standalone artifacts. Migrating removes the dependency on undocumented behavior.
// sun.misc.Unsafe.objectFieldOffset -> VarHandle
VarHandle vh = MethodHandles.lookup()
.findVarHandle(Order.class, "total", BigDecimal.class);
vh.set(order, new BigDecimal("99.90"));Bridge with a narrowly scoped --add-exports
👉 Use this while a third-party library you cannot patch still needs the internal package.
One flag pair per package, naming the exact reader. Avoid ALL-UNNAMED for everything; enumerate the modules you truly need so future JDK hardening does not silently break other paths. Document every bridge with the issue that will retire it.
# example: a metrics lib reading buffer counters
java --add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED \
-jar app.jarUpgrade the offending library to a JDK-17-ready release
👉 Use this when the stack frame above yours belongs to a vendor jar, not your code.
Most active projects shipped strong-encapsulation-compatible builds years ago; check changelogs for JPMS notes. Upgrading deletes your --add-exports debt instead of accumulating it.
mvn -q dependency:tree -Dincludes=net.bytebuddy,org.objenesis,com.zaxxer
# then bump each to the release certified for your JDK lineDeclare Add-Exports in the executable jar manifest
👉 Use this for java -jar deployments where command-line flags tend to get lost between environments.
The manifest attribute travels with the artifact, keeping runtime behavior identical everywhere the jar runs without depending on operators remembering flags.
Manifest-Version: 1.0
Main-Class: com.devinhyderabad.app.Main
Add-Exports: java.base/sun.nio.ch java.base/sun.miscOpen every package to everything at startup
👉 DEV ONLY — never ship this; see below.
DEV ONLY. Blanket --add-opens/--add-exports for all modules silences every encapsulation error at once but destroys the boundary the platform enforces for security and stability, and hides the dozen individual migrations you actually owe. Fine for one local reproducibility test; forbidden in images and units.
# DEV ONLY - nukes encapsulation globally
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-exports java.base/sun.nio.ch=ALL-UNNAMED \
-jar app.jar📋 Version Notes
No modules; sun.* internals accessible with mere deprecation warnings.
Illegal reflective access warns by default (JEP 261 relaxed mode).
JEP 403: --illegal-access removed; JDK internals deny access outright.
Same posture; FFM API preview path for direct buffers matures here.
🛡️ How to Prevent This Next Time
Compile with --release matching production and jdeps your fat jars for internal-API usage on every dependency bump; track deprecation warnings (javac -Xlint:jdkinternals) as work items, not noise.