Advanced 5 min · March 05, 2026

Java Reflection — setAccessible Fails on JDK 9+

setAccessible fails on JDK 9+ internals with InaccessibleObjectException.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • Reflection exposes class metadata the JVM already holds in Metaspace
  • Field, Method, Constructor objects are cached views into bytecode
  • setAccessible(true) bypasses Java access controls — use with caution
  • Method.invoke() is ~15x slower than direct call; MethodHandle near-zero overhead after JIT warmup
  • Java 9+ modules can block reflective access with InaccessibleObjectException
  • Biggest mistake: calling getDeclaredMethod() in a loop — cache once at startup

How the JVM Actually Loads Classes — The Foundation of Reflection

Before you can understand Reflection, you need to understand what happens when the JVM loads a class. Every time the classloader brings a .class file into memory, the JVM creates a single, unique java.lang.Class object for it. This Class object is the crown jewel — it's a live, runtime representation of everything the compiler knew about that class: its name, fields, methods, constructors, annotations, superclass, and interfaces.

This Class object lives in the Method Area (or Metaspace in Java 8+) and is shared across all instances of that class. When you call someObject.getClass() or write MyClass.class, you're grabbing a reference to that same singleton object.

Reflection is simply the API built around this Class object. The java.lang.reflect package gives you Field, Method, Constructor, and Modifier — all of which are views into data the JVM already holds. You're not doing anything supernatural. You're asking the JVM to tell you what it already knows.

The key insight: Reflection doesn't generate new information. It exposes existing metadata that the compiler baked into the bytecode. That's why it works — and also why it can break when that metadata is stripped by obfuscators or ahead-of-time compilers.

Invoking Methods and Mutating Private Fields at Runtime

Inspecting a class is just the beginning. The real power — and the real danger — of Reflection is the ability to invoke methods and read or write fields that were never meant to be touched from outside the class.

To call a method reflectively, you get a Method object and call invoke() on it, passing the target instance and arguments. For private methods, you must call setAccessible(true) first. This bypasses Java's access control checks — a door the JVM keeps locked by default, but which you can unlock with this single call.

Field mutation works the same way. You get a Field, call setAccessible(true), and then call field.set(instance, newValue). You've just written to a private field without a setter. Hibernate uses this exact mechanism to populate entity fields from database results without requiring public setters.

The danger is real: setAccessible(true) doesn't just bypass encapsulation for your code — it can silently corrupt object invariants if you write an invalid value. A private field named connectionPoolSize might have guards in its setter that enforce a minimum value. When you bypass the setter via reflection, those guards don't run. You own the consequences.

In Java 9+, the module system adds an extra layer. Even setAccessible(true) can throw an InaccessibleObjectException if the target module doesn't open its package. This is intentional — it closes a loophole that existed for decades.

Dynamic Object Creation and Generic Type Erasure at Runtime

One of Reflection's most powerful — and most misunderstood — use cases is instantiating classes at runtime without knowing them at compile time. This is exactly how Spring creates your @Service beans, how Jackson deserializes JSON into POJOs, and how plugin systems load user-defined classes from external JARs.

You get the Constructor object from the Class, call setAccessible(true) if needed, and call newInstance() on the Constructor. Note: Class.newInstance() was deprecated in Java 9 because it silently propagated checked exceptions. Always use Constructor.newInstance() instead — it's explicit about exceptions.

Now for the tricky part: generics. Java uses type erasure, meaning generic type parameters like List<String> are compiled down to just List at the bytecode level. At runtime, Reflection can't tell you the difference between a List<String> and a List<Integer> — they're both raw List to the JVM.

However, there's a workaround. If a generic type appears as a field declaration, method parameter, or superclass, that information IS preserved in the bytecode as a signature attribute. You can recover it via getGenericType() on a Field, which returns a ParameterizedType. This is how Jackson knows which generic type to deserialize into.

Understanding this distinction — runtime erasure vs. signature-level retention — separates developers who truly understand Reflection from those who just use it.

Reflection Performance, Caching, and When to Avoid It

Reflection is not free. Before Java 21, invoking a method reflectively was roughly 10–50x slower than a direct call, primarily because the JVM can't apply standard JIT optimizations like inlining across a reflective call boundary. The JVM also performs security checks on every reflective access unless you've cached the accessible Member object.

The good news: most of that cost is in the lookup, not the invocation. Class.forName(), getDeclaredMethod(), and setAccessible() are the expensive operations — the actual invoke() call is much cheaper, especially after the JVM's inflation mechanism kicks in. After ~15 native invocations, the JVM generates bytecode stubs (inflation) for the reflective call, dramatically improving performance. You can control this with the sun.reflect.inflationThreshold system property, though touching internal properties in production is risky.

The production rule is simple: never look up. Always cache. Get your Method, Field, and Constructor objects once — ideally at startup — and reuse them. Store them in static fields or a ConcurrentHashMap keyed by class name. This is exactly what Spring does: it resolves all reflection targets at application startup, then uses the cached Method objects for every subsequent bean operation.

Java 7+ MethodHandles (java.lang.invoke) offer a better alternative for hot paths. A MethodHandle behaves like a typed function pointer — the JVM CAN inline across it, making it nearly as fast as a direct call after JIT warmup. For any reflection-heavy code on a critical path, migrating from Method.invoke() to MethodHandle is the right production-grade move.

Security and Module System Implications of Reflection

Reflection is a double-edged sword. It gives you flexibility — but it also opens holes that attackers and misconfigurations can exploit.

The most dangerous pattern is using setAccessible(true) on objects you receive from external or untrusted sources. If your code calls setAccessible(true) on a Field or Method obtained from user-provided class names, an attacker can read and write private fields, invoke private constructors, and break encapsulation.

Java's SecurityManager (deprecated in Java 17 and removed in Java 18) was one line of defense. The modern defense is the Java module system (JPMS) introduced in Java 9. Modules can explicitly export or open packages. By default, java.base does not open its packages to unnamed modules, so reflective access to JDK internals is blocked.

When you need to grant reflective access, you add --add-opens flags at JVM startup: --add-opens java.base/java.lang=ALL-UNNAMED. But this is a global permission — it opens the package to ALL code, not just yours. A more secure approach is to move your code into a named module that explicitly opens its packages only to specific modules.

For applications that don't control the JVM flags (e.g., cloud environments, shared hosting), reflective access to internal APIs is simply impossible. Framework authors must design for this: use standard APIs, avoid private field access, and provide setter-based injection. Spring's reflection-based injection, for example, works without setAccessible on public methods and constructors, but for private fields it requires module openness.

Reflection vs MethodHandle vs Direct Call
AspectMethod.invoke() (Reflection)MethodHandle (java.lang.invoke)
JIT InlineableNo — opaque to JIT optimizerYes — JIT can inline across it
Speed (hot path)~15x slower than direct call~1.5x slower, often near-zero overhead
Type SafetyRuntime only — no compile-time checkChecked at creation, fast at invoke
Exception HandlingWraps in InvocationTargetExceptionThrows original exception directly
Module System (Java 9+)Blocked by module encapsulationSame restrictions apply, cleaner API
Lookup CostHigh — cache getDeclaredMethod()High — cache MethodHandle once
ReadabilityFamiliar, widely understoodLess familiar, more verbose setup
Best Use CaseFramework/tooling bootstrappingPerformance-critical hot paths

Key Takeaways

  • The Class object in Metaspace is the single source of truth — Reflection doesn't generate new data, it exposes what the compiler already baked into the bytecode.
  • getDeclaredFields() sees private members in the current class only; getFields() sees public members across the inheritance chain — mixing them up silently loses fields in serializers and mappers.
  • Never look up in a loop — getDeclaredMethod() and setAccessible() are expensive. Cache your Method, Field, and Constructor objects at startup. That single change can deliver 10–20x throughput improvement.
  • For hot-path reflective invocation, switch to MethodHandle — the JVM can inline across it post-JIT warmup, making it nearly as fast as a direct method call, unlike Method.invoke() which stays opaque to the optimizer.
  • Java 9+ modules block reflective access to encapsulated packages. Always test reflection code on the minimum Java version you support and consider using public APIs instead of private field access.

Common Mistakes to Avoid

  • Calling getDeclaredMethod() or getDeclaredField() inside a loop or per-request path
    Symptom: Your service degrades under load with surprisingly high CPU in profiler output. Each lookup triggers security checks, class hierarchy traversal, and string comparisons.
    Fix: Resolve all Method and Field objects once at startup and store them in static final fields or a pre-warmed cache map.
  • Catching InvocationTargetException and logging its message as null
    Symptom: When a reflectively-invoked method throws a checked or unchecked exception, it's wrapped in InvocationTargetException. Calling getMessage() or printStackTrace() on the wrapper gives misleading or empty output.
    Fix: Always call ite.getCause() to get the real exception before logging or rethrowing. Make this a team code-review rule for any code using Method.invoke().
  • Using getDeclaredFields() on a class but forgetting to walk the superclass chain
    Symptom: A custom serializer silently drops fields from parent classes, producing incomplete JSON or database rows.
    Fix: Write a utility method that loops up the class hierarchy via getSuperclass() until getSuperclass() returns null, collecting fields at each level. Use a Set to avoid duplicates.

Interview Questions on This Topic

  • QWhat is the difference between getMethod() and getDeclaredMethod() in the Java Reflection API, and what does setAccessible(true) actually do under the hood?Mid-levelReveal
    getMethod() returns a Method object for a public method, including inherited ones. getDeclaredMethod() returns any method declared directly in the class (including private, protected, public) but does not search superclasses. Under the hood, setAccessible(true) modifies the AccessibleObject's accessible flag, which suppresses the access control checks during invoke(). In Java 9+, it also checks module boundaries and throws InaccessibleObjectException if the target class is not open to the caller's module.
  • QHow does Java's type erasure affect what you can and can't discover about generic types using Reflection at runtime — and what technique can you use to work around it?SeniorReveal
    Java erases generic type parameters at runtime, so a List<String> and List<Integer> are both just List when examined via getClass(). However, if the generic type appears in a field declaration (e.g., List<String> stages) or method signature, the bytecode preserves the full parameterized type in the 'Signature' attribute. You can recover it via Field.getGenericType() which returns a ParameterizedType. The TypeToken pattern (using an anonymous subclass of a parameterized type) allows recovery from class-level generics because the subclass retains the type argument in its superclass reference.
  • QYou have a performance-critical service that uses Reflection to invoke methods on dynamically loaded plugins. Under load, the service is slower than expected. Walk me through how you'd diagnose and fix the issue.SeniorReveal
    First, profile the application using async-profiler or JFR to identify hotspots. If reflection-related methods are high, check whether Method/Field lookups happen per-request. The fix is to cache the Method objects in a static map after initial lookup. If the invocation itself is hot, consider switching from Method.invoke() to MethodHandle, which the JIT can inline (near-zero overhead). Also verify that setAccessible is called only once per Method object. Finally, look for alternative APIs that avoid reflection altogether for the hot path.

Frequently Asked Questions

Is Java Reflection slow and should I avoid it in production?

Reflection has real overhead, but the cost is almost entirely in the lookup phase — getDeclaredMethod(), setAccessible(), and Class.forName(). If you cache the resulting Method or Field objects at startup and reuse them, the per-invocation cost is modest. For genuinely hot paths, use MethodHandle instead. Frameworks like Spring and Hibernate use reflection heavily in production — they just cache aggressively.

Can Java Reflection access private fields and methods?

Yes. Calling setAccessible(true) on a Field or Method object bypasses Java's access control checks and lets you read or write private members. In Java 9+, this can throw InaccessibleObjectException if the target class is in a module that doesn't explicitly open its package — you'll need to add --add-opens JVM flags or restructure your code.

What is the difference between Class.forName() and .class syntax in Java?

MyClass.class is a compile-time constant — the class must be known at compile time and it doesn't trigger static initializer blocks. Class.forName("com.example.MyClass") is a runtime lookup that resolves the class by name string, triggers static initializers, and uses the calling class's classloader. Use .class for known types and forName() for plugin systems or config-driven class loading.

How do I handle InvocationTargetException properly?

Always reach for ite.getCause() to get the actual exception thrown by the invoked method. InvocationTargetException itself is a wrapper — its getMessage() often returns null. In catch blocks, check for InvocationTargetException first, then unwrap before logging or rethrowing.

Can I use Reflection to create instances without a no-arg constructor?

Yes. Fetch the Constructor object that matches the parameters you want using getDeclaredConstructor(Class... parameterTypes), then call constructor.newInstance(args...). If the constructor is private, call setAccessible(true) first.

🔥

That's Advanced Java. Mark it forged?

5 min read · try the examples if you haven't

Previous
Generics in Java
2 / 28 · Advanced Java
Next
Annotations in Java