JPMS in practice (modularising a monolith, architectural impact, enterprise realities, security and maintainability claims corrected), what Java 9 changed, explaining the JVM to new developers, hidden classes, Java agents and java.lang.instrument, the Compiler API, decompilers and protecting code, sun.misc.Unsafe and its replacements (VarHandle, FFM API), MethodHandles, and real-world Java performance optimisations.
Published September 25, 2026
These questions separate people who have read about Java 9+ from people who have shipped it. Be honest about JPMS adoption:
Also know the supported replacements for internal APIs like Unsafe.
Short answer: Do it incrementally, and only where the boundaries pay off:
Automatic-Module-Name manifest entry or the JAR name.module-info.java for each module:
requires for dependencies (requires transitive for APIs you re-expose);exports only the API packages;opens packages to reflection-based frameworks (Spring, Hibernate, Jackson) that need deep access;uses/provides for ServiceLoader-based plugins.--add-opens as a temporary measure, then refactor).jdeps to analyse dependencies, and jlink to build a trimmed runtime image.module com.shopin.orders {
requires com.shopin.catalog.api; // compile- and run-time dependency
requires transitive com.shopin.common; // our API exposes types from common
requires spring.context;
exports com.shopin.orders.api; // the only public surface
opens com.shopin.orders.internal.persistence to org.hibernate.orm.core, spring.core;
provides com.shopin.common.spi.EventHandler with com.shopin.orders.internal.OrderEventHandler;
}
Short answer:
public no longer means "accessible to everyone". Only exported packages are visible, so internal packages really are internal. That enables safe refactoring of internals.provides/uses, without reflection hacks.jlink: smaller containers, and a smaller attack surface.Its impact in practice:
sun.* internals.Short answer: A module is a named, self-describing unit (module-info.class) that declares what it needs (requires) and what it offers (exports, provides).
jlink images include only the modules you need.Common trap: the source claims the JVM "loads only required modules, improving startup and memory". For classpath applications, nothing changes: classes are still loaded lazily, on first use. The real footprint win comes from jlink custom runtime images, and from CDS.
Short answer:
jlink images for leaner deployments.opens.--add-reads/--add-opens flags for test frameworks).--add-opens flags.Short answer:
module-info, and jlink.List.of, Set.of, Map.of (immutable).takeWhile, dropWhile, ofNullable and iterate-with-predicate.Optional got ifPresentOrElse, or and stream.CompletableFuture got timeouts (orTimeout, completeOnTimeout) and delayedExecutor.java.util.concurrent.Flow.ProcessHandle, a better process API.java.net.http.HttpClient).Unsafe uses).-Xlog).The impact: upgrade projects (JDK internals, removed Java EE modules in Java 11), cleaner APIs, and quicker experimentation. Practically, the six-month cadence changed how organisations plan upgrades: LTS to LTS (11 → 17 → 21 → 25).
Short answer: Walk them through java Hello:
javac compiles Hello.java into bytecode (Hello.class): instructions for a virtual CPU, the same on every OS.java starts the JVM, which loads the class (class loaders), verifies the bytecode, and links and initialises it.main on the main thread. Its stack holds the local variables, and objects go on the heap.free.main returns (and no non-daemon threads remain), the JVM runs its shutdown hooks and exits.The payoff is portability ("write once, run anywhere"), safety (verification, no pointer arithmetic), and automatic memory management.
Short answer: A class created with MethodHandles.Lookup.defineHiddenClass(bytes, ...) that:
Class.forName;It's used by framework-generated code: lambda proxy classes (LambdaMetafactory), dynamic proxies, and bytecode-generating libraries (ByteBuddy, and language runtimes). It replaced the internal Unsafe.defineAnonymousClass, which was removed in Java 17. The benefits are less Metaspace retention (fewer classloader leaks) and no need for dedicated loaders.
Common trap: "hidden classes are inaccessible to reflection". You can reflect on a hidden class if you have its Class object. It just can't be looked up by name or referenced symbolically.
java.lang.instrument for?Short answer: An agent is a JAR with a premain method (started with -javaagent:agent.jar) or an agentmain method (attached to a running JVM through the Attach API). The JVM hands it an Instrumentation instance. With java.lang.instrument, it can:
ClassFileTransformers, which rewrite bytecode as classes load (usually with ASM or ByteBuddy);getObjectSize), append JARs to class loader search paths, and so on.Uses:
Key points to cover:
-XX:+EnableDynamicAgentLoading, or load agents at startup.public final class TimingAgent {
public static void premain(String args, Instrumentation inst) {
new AgentBuilder.Default() // ByteBuddy
.type(ElementMatchers.nameStartsWith("com.shopin.service"))
.transform((b, type, loader, module, pd) -> b.visit(Advice.to(TimingAdvice.class).on(ElementMatchers.isMethod())))
.installOn(inst);
}
}
Short answer: javax.tools.JavaCompiler (from ToolProvider.getSystemJavaCompiler()) compiles Java source programmatically, with in-memory file managers, diagnostics collection and classpath control. Uses:
Key points to cover:
jlinked runtime without jdk.compiler).javax.annotation.processing) for build-time generation.Short answer: A decompiler (CFR, Fernflower/Vineflower, Procyon, JD-GUI) reconstructs Java-like source from bytecode. It's easy, because bytecode keeps names and structure. Legitimate uses:
Using it responsibly:
Protecting your code:
sun.misc.Unsafe work, what is it used for, and what replaces it?Short answer: Unsafe is an internal JDK class that exposes raw operations:
allocateMemory, putLong at addresses);final;allocateInstance);park/unpark;High-performance libraries (Netty, Cassandra, Kryo, Chronicle, LMAX Disruptor) used it for speed. It's dangerous: a wrong address can crash the JVM or silently corrupt memory, and it bypasses every Java safety guarantee.
The supported replacements:
VarHandle (Java 9): atomic operations, CAS and ordered or opaque memory access on fields and array elements.Arena, MemorySegment), and native calls, replacing JNI too.MethodHandles.Lookup.defineHiddenClass, instead of defineAnonymousClass.LockSupport, instead of park.The status: Unsafe's memory-access methods are deprecated for removal (JEP 471, Java 23), and emit runtime warnings from Java 24 (JEP 498), on the way to being removed.
MethodHandle?Short answer: A typed, directly executable reference to a method, constructor, field accessor or composed operation (java.lang.invoke, Java 7):
MethodHandles.Lookup, which checks access once, at lookup time.invokeExact (the types must match exactly) or invoke (with conversions).Why it matters:
static final), so they're close to direct calls.invokedynamic: lambdas, string concatenation, records, pattern-matching switches.private static final MethodHandle TOTAL;
static {
try {
TOTAL = MethodHandles.lookup().findVirtual(Order.class, "total", MethodType.methodType(BigDecimal.class));
} catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
}
BigDecimal t = (BigDecimal) TOTAL.invokeExact(order); // types checked at call time; cast required for invokeExact
Common trap: saying method handles are "type-checked at compile time". They're signature-polymorphic. The call site's type is checked at invocation (WrongMethodTypeException), not by javac.
Short answer: Answer with measured, specific stories (STAR, with numbers). Good examples:
JOIN FETCH/entity graphs, and added a covering index: p95 dropped from 1.2 s to 90 ms.hibernate.jdbc.batch_size, reWriteBatchedStatements).String.format and boxing in a pricing loop with JFR. Replaced them with StringBuilder/primitives, and allocation rate halved, so young GCs went down by 50%.System.gc() from a library.ConcurrentHashMap.computeIfAbsent.MaxRAMPercentage; added a CDS/AOT cache (startup from 9 s to 4 s).Q: What is jdeps used for?
A: It analyses class and JAR dependencies, both package-level and on JDK internals (--jdk-internals). It can generate module-info.java skeletons (--generate-module-info), and helps plan JDK upgrades and jlink module lists (--print-module-deps).
Q: What does --add-opens do, and why is it a smell?
A: It opens a package of a module to deep reflection by another module (or the unnamed classpath module), bypassing strong encapsulation. It's needed for some legacy libraries, but it signals dependence on internals that may break in future JDKs. Upgrade the library instead, where possible.
Q: What are module layers?
A: ModuleLayer lets an application create new sets of modules at runtime, with their own class loaders. That enables plugin systems, and even different versions of the same module in different layers.
Q: What is a multi-release JAR?
A: A JAR with Multi-Release: true, and version-specific classes under META-INF/versions/N/. A Java 9+ runtime picks the newest versions it supports, which lets libraries use new APIs while still supporting old JDKs.