Access Modifiers: Implementation and JVM Enforcement
Java provides four levels of access control: public, protected, default (package-private), and private. While most developers know the basic rules of "who can see what," few understand how these rules are actually enforced. Is it just the compiler? Or does the JVM play a role? If you bypass the compiler and modify bytecode, is a private field still private?
1. The Four Levels of Visibility
| Modifier | Same Class | Same Package | Subclass (Diff Pkg) | Non-subclass (Diff Pkg) |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ | ❌ |
| Default | ✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
Key Nuances:
- Class-Level vs. Object-Level:
privatevisibility is class-level. APersoninstance can access theprivatefields of anotherPersoninstance, provided the code is written inside thePersonclass. - Sub-packages are NOT "Same Package": In Java,
com.exampleandcom.example.serviceare treated as completely independent packages. There is no hierarchical inheritance of access rights between them. - The
protectedConstraint: In a different package, a subclass can accessprotectedmembers of its parent only via its own instance (this). It cannot access theprotectedmembers of a separate Parent instance, as this would allow one subclass to "peek" into the internal state of another subclass.
2. Under the Hood: The access_flags Bitmask
Access modifiers are encoded directly into the .class file as a bitmask called access_flags.
| Modifier | Flag Name | Mask Value |
|---|---|---|
public |
ACC_PUBLIC |
0x0001 |
private |
ACC_PRIVATE |
0x0002 |
protected |
ACC_PROTECTED |
0x0004 |
| Default | (None set) | 0x0000 |
If we run javap -v Demo.class, we can see these flags in the field/method metadata. The JVM uses these bits during the "Resolution" phase of class loading to perform its own security checks.
3. Double-Layer Protection
Access control is enforced at two stages, ensuring that Java's security model cannot be easily bypassed.
3.1 Compile-Time (The First Filter)
javac performs semantic analysis. If you try to access a private member from an external class, the compiler refuses to generate bytecode, throwing an error.
3.2 Runtime (The Infinite Verifier)
If you manually edit bytecode or use a tool to bypass the compiler, the JVM Bytecode Verifier serves as the second line of defense. When executing instructions like getfield or invokevirtual, the JVM performs its own resolution check against the access_flags. If a violation is found, it throws a java.lang.IllegalAccessError.
4. Inner Classes and the "Synthetic" Illusion
JVMs historically didn't understand the concept of "Inner Classes"—every class was a standalone .class file. This created a problem: how does an Inner class access a private field in its Outer class?
Before JDK 11: Synthetic Methods
The compiler used a "trick." It would automatically generate a package-private method in the Outer class (e.g., static int access$000(...)) that returned the value of the private field. The Inner class would then call this "Synthetic" method to fetch the data.
JDK 11+: Nest-Based Access Control (JEP 181)
JDK 11 introduced Nestmates. Classes can now declare a "NestHost" and "NestMembers" in their metadata. The JVM verifier was updated to explicitly allow private access between classes that belong to the same "Nest," eliminating the performance and security overhead of synthetic methods.
5. Reflection and the Module System
java.lang.reflect allows code to bypass access checks via setAccessible(true).
However, JDK 9+ Module System added a third layer. Even with reflection, a class cannot access members in another module unless that module explicitly "opens" the package to the caller. Without this, the JVM throws an InaccessibleObjectException.
Summary: The Access Control Chain
| Layer | Mechanism | Timing |
|---|---|---|
| Source | Access Keywords | Development |
| Compiler | Semantic Analysis | Compilation |
| Bytecode | access_flags Bitmask |
Metadata Generation |
| JVM | Bytecode Verifier | Class Loading / Linking |
| Modules | exports / opens |
Module Resolution |
| Reflection | setAccessible(true) |
Runtime Overriding |