JPMS Split Package — JVM Fails Fast, Not Silently
- JPMS enforces strong encapsulation at the JVM level: module-info.java declares dependencies and exposed packages.
- The module path resolves dependencies eagerly and fails on split packages — no silent surprises.
- Use
exportsfor direct access andopensfor reflective access; never over-export.
- JPMS is a deployment unit above JARs, enforcing strong encapsulation at JVM level
- module-info.java declares dependencies (requires), exposed packages (exports), and services
- --add-opens grants reflective access at runtime, --add-exports grants compile-time access
- Split packages (same package in multiple modules) cause boot layer failure — leads to noisy startup errors
- Performance: module path classloading is slightly faster than classpath due to explicit dependencies
- Production pitfall: reflection-based frameworks (Spring, Hibernate) need --add-opens for deep introspection — missing these silently breaks DI at runtime
JPMS Quick Debug Commands
Module not found at startup
java --list-modules | grep module_namejar --describe-module --file=module.jarReflective access failure at runtime
java --add-reads java.base=ALL-UNNAMED --add-exports java.base/sun.security.x509=ALL-UNNAMED <main-class> 2>&1 | grep 'Unable to make'jlink --add-exports java.base/sun.security.x509=ALL-UNNAMED --output myimageSplit package error on boot
jdeps --module-path libs --check <target-module> | grep 'split'jar -tf both-libs.jar | grep -E 'org/slf4j/impl/.+class$' | sort -uProduction Incident
Production Debug GuideCommon symptoms and actions to diagnose JPMS-related startup errors
If you've ever cracked open a production JVM heap dump and found a third-party library reaching deep into sun.misc.Unsafe, or spent a day debugging a ClassNotFoundException that only appeared when a JAR was repackaged, you already know the pain that JPMS was built to eliminate. The Java Platform Module System, shipped in Java 9 as part of Project Jigsaw after nearly a decade of design work, is the most structurally significant change to the Java platform since generics. It isn't a minor API addition — it's a new unit of deployment that sits above the JAR and below the application.
Before JPMS, the JVM had exactly one accessibility boundary at runtime: public. If a class was public, anyone on the classpath could use it, full stop. That meant internal JDK APIs like sun.reflect and com.sun.* were fair game for library authors who needed performance shortcuts, and there was no tooling that could enforce the architectural boundaries your team drew on whiteboards. The result was a fragile, monolithic JDK and codebases where 'refactoring an internal package' was a multi-sprint project because you never knew who was secretly depending on it.
By the end of this article you'll understand how the module system enforces strong encapsulation at the JVM level, how to author a correct module-info.java for a real multi-module project, how the module path differs from the classpath at the classloader level, where the system genuinely breaks down (split packages, reflective frameworks, legacy migration), and exactly what to say when an interviewer asks you to contrast --add-opens with --add-exports. This is the practical, internals-first guide that the official Javadoc never was.
What is JPMS? — The Core Idea
JPMS introduces a new level of structuring above packages: the module. A module is a named, self-describing collection of code and data that explicitly declares: - What it requires (dependencies on other modules) - What it exports (packages accessible to other modules) - What it provides (service implementations for service interfaces) - What it consumes (uses) of services
This is declared in a module-info.java file placed at the root of the source tree. When compiled, it becomes module-info.class inside the JAR. The JVM uses this metadata to enforce strong encapsulation: a module cannot access another module's internal packages unless they are explicitly exported.
// Example: module-info.java for a payment module in TheCodeForge module io.thecodeforge.payment { requires java.base; // always implicitly required, but good to be explicit requires io.thecodeforge.account; requires jakarta.persistence; exports io.thecodeforge.payment.api; // public API package exports io.thecodeforge.payment.dto; // DTOs shared over external APIs // Keep implementation packages sealed // Not exported: io.thecodeforge.payment.internal provides io.thecodeforge.common.PaymentGateway with io.thecodeforge.payment.StripeGateway; uses io.thecodeforge.common.PaymentAuditor; }
opens io.thecodeforge.payment.dto to com.fasterxml.jackson.databind;opens X (or opens X to framework.module)exports Yexports Y and opens Y (or opens Y to ...)Module Path vs Classpath — How the JVM Loads Modules
When the JVM starts with the module path, it uses a new built-in module layer that merges all discovered module descriptors and resolves dependencies eagerly before any code runs. This is fundamentally different from the classpath approach, where class loading is lazy and the closest match wins.
- Module path (
--module-pathor-p): JVM scans all JARs and directories formodule-info.class, builds a module graph, checks for split packages, and fails fast on conflicts. - Classpath: Classic classloader (URLClassLoader) with linear search — last loaded can override earlier identical packages silently, leading to Heisenbugs.
JPMS also introduces the concept of unnamed module: any code on the classpath becomes part of the unnamed module, which can read all named modules but not the other way around (unless --add-reads is used). This is the bridge during migration.
# Run with module path java --module-path mods:libs --module io.thecodeforge.app # Run with classpath (unnamed module) java -cp mods:libs io.thecodeforge.app.Main # Mixed: app on module path, drivers on classpath java --module-path mods --add-modules io.thecodeforge.app -cp drivers/* io.thecodeforge.app.Main
- Module path: eager resolution, split package detection, strong encapsulation.
- Classpath: lazy loading, last-loaded-wins, no access control.
- Unnamed module: a fallback that can read all named modules but its code cannot be read by named modules.
Services — Loose Coupling with provides/uses
JPMS supports service loading via ServiceLoader that works across module boundaries. A module declares that it provides an implementation of a service interface, and another declares that it uses that service. The ServiceLoader discovers all implementations from all modules at runtime without compile-time dependency on the implementation class.
This is a departure from the traditional reflection-based discovery (like META-INF/services). The module system enforces that a module cannot provide a service if it doesn't also read the module that exports the service interface.
// Service interface in module io.thecodeforge.common package io.thecodeforge.common; public interface PaymentGateway { PaymentResult process(PaymentRequest request); } // Service implementation in module io.thecodeforge.payment package io.thecodeforge.payment; public class StripeGateway implements PaymentGateway { // ... } // Client code using ServiceLoader ServiceLoader<PaymentGateway> loaders = ServiceLoader.load(PaymentGateway.class); for (PaymentGateway gateway : loaders) { // found implementation from all modules that provide it }
--add-opens. Prefer ServiceLoader for extensibility.provides ... with ....provides in module-info.provides declaration — the uses was there but the provider was silently missing.provides with the uses directive.Migration Strategy — Gradual Adoption of JPMS
Migrating a large codebase to JPMS in one go is risky. Oracle recommends a phased approach: 1. Classpath only — add module-info.java but put all JARs on classpath (the module descriptor is ignored). This lets you compile with module path but run with classpath. 2. Run with module path but keep all JARs that are not modularised on the classpath (unnamed module). Use --add-reads and --add-exports to bridge. 3. Make all JARs modularised — either by adding module-info.java to owned JARs or using automatic modules (JARs without module-info become automatic modules, reading all named modules). 4. Enforce strong encapsulation by removing unnecessary --add-exports and --add-reads.
Automatic modules are a temporary relief: a JAR on the module path without module-info becomes an automatic module that exports all its packages and reads all other modules. This can hide split-package issues.
// No module-info.class in the JAR -> automatic module // The module name is derived from the JAR filename (e.g., guava-31.1-jre.jar -> guava) // Automatic modules have no control over what they export or read. // They can read all named modules and all packages are exported to all. // This is a migration bridge, not a final state.
jackson-core-2.12.jar and jackson-core-2.13.jar), the module system sees them as the same module and may pick one arbitrarily, causing ClassNotFoundException.jdeps --generate-module-info to create skeleton module-info from old JARs.jdeps --check catches most issues.Common Pitfalls and How to Fix Them
Beyond split packages, the most common production issues with JPMS include: - Transitive dependency on an internal JDK API — libraries that used sun.misc.BASE64Decoder now fail. Fix: use java.util.Base64 or --add-exports. - ClassLoader assumptions — some frameworks assume a single classloader or that all classes are from the classpath. JPMS uses multiple classloaders per module. If you see ClassNotFoundException from a framework that tries to load classes by reflection, add --add-opens and --add-reads. - Module name conflicts — two libraries use the same module name (e.g., com.google.common vs com.guava). The JVM throws an exception. Rename one with a custom module-info or use shaded JARs. - --add-opens security risk — granting ALL-UNNAMED reflective access opens up all packages to all code. In production, restrict to specific modules.
// Prefer opens to specific consumer modules rather than ALL-UNNAMED module io.thecodeforge.payment { exports io.thecodeforge.payment.api; // Only Jackson can reflect on our DTOs opens io.thecodeforge.payment.dto to com.fasterxml.jackson.databind; // Only Hibernate can reflect on our entities opens io.thecodeforge.payment.model to org.hibernate.orm; }
--add-exports flags.--illegal-access=deny (Java 9–16) or --add-opens tracking (Java 17+).--add-opens to specific modules, not ALL-UNNAMED.--illegal-access=deny in test.opens and exports in module-info.java--add-opens targeting that module--add-reads if needed| Directive | Compile-time access | Runtime direct access | Runtime reflective access | Typical use |
|---|---|---|---|---|
| exports package | Yes (if module is required) | Yes | Yes (reflection on public members) | Sharing public API types |
| opens package | No | No | Yes (all members including private) | Framework reflection (Jackson, Hibernate) |
| requires module | Yes | Depends on exports/opens | Depends on exports/opens | Declaring a dependency |
| requires transitive | Yes (propagates to downstream modules) | Propagates accordingly | Propagates accordingly | Common dependencies |
🎯 Key Takeaways
- JPMS enforces strong encapsulation at the JVM level: module-info.java declares dependencies and exposed packages.
- The module path resolves dependencies eagerly and fails on split packages — no silent surprises.
- Use
exportsfor direct access andopensfor reflective access; never over-export. - ServiceLoader works with
provides/usesdirectives in modular mode, not with legacy META-INF/services files. - Migrate in phases: classpath → mixed → full modular; automatic modules are a bridge, not a destination.
- Always restrict
--add-opensto specific consumer modules — avoidALL-UNNAMEDin production.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain the difference between --add-exports and --add-opens in JPMS.SeniorReveal
- QWhat is a split package and why does the JVM reject it on the module path?Mid-levelReveal
- QHow does ServiceLoader work in a modular environment compared to pre-JPMS?SeniorReveal
- QWhat is an automatic module and when should you use it?Mid-levelReveal
- QHow would you resolve a ClassCastException where two instances of the same class are from different classloaders?SeniorReveal
Frequently Asked Questions
Can I use JPMS with Java 8 or older?
No. JPMS was introduced in Java 9 and is not backwards compatible with older Java versions. If you need to support Java 8, you cannot use module-info.java. However, you can still design your code with modules in mind (e.g., strict package visibility, no reliance on internal APIs) and add module-info.java later when you migrate to Java 9+.
What happens if I put a modular JAR on the classpath?
The JAR's module-info.class is ignored entirely. The JAR behaves like a regular classpath JAR: all public classes are accessible, no strong encapsulation, no split package detection. This is part of the migration strategy — you can compile with modules but run with classpath initially.
How do I handle a library that uses internal JDK APIs?
First check if a newer version of the library exists that uses public APIs. If not, you need to grant runtime access via --add-exports and possibly --add-opens. For example, --add-exports java.base/sun.security.x509=ALL-UNNAMED. This is a workaround, not a permanent solution.
Does JPMS affect performance?
The module path classloading is slightly faster because dependencies are resolved eagerly and the class loader has less searching to do (due to explicit package to module mapping). In most applications the difference is negligible. The real performance benefit comes from the ability to use jlink to create custom runtime images that include only needed modules, reducing memory footprint and startup time.
Can I export a package to only specific modules?
Yes. Use exports package to moduleA, moduleB; or opens package to moduleC; in module-info.java. This restricts the package's visibility to only the named modules. It's a good practice to limit exposure.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.