Language selector

Illegal access in Java 16 A.K.A. 'My program crashes!'

Roses are red

reflection may hit

you shall not use


As a Java developer (if you tried to at least step out of your Java 8 comfort zone) chances are you saw something like this in your logs or terminal output:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.przybyl.ddj16.stronglyEncapsulatedInternals.StronglyEncapsulatedInternals to field java.lang.String.value
WARNING: Please consider reporting this to the maintainers of org.przybyl.ddj16.stronglyEncapsulatedInternals.StronglyEncapsulatedInternals
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Since Java 9 release, a lot has been said and written about project Jigsaw, which not only modularised JDK itself (no more rt.jar!) but also brought to us the notion of the illegal access. I explain what that is in my Java. Migrating to 11 in real app talk, let me recap briefly (in case you haven’t seen it).

Not every reflection in Java world is fundamentally one of These Bad Ideas™. If you’d like to discover in runtime capabilities of an object you’ve been given, reflection might be quite handy. What’s giving us headache is so called deep reflection.

Object-oriented programming was invented for a reason. There are some pros and cons of this paradigm, but one of the key aspects of OOP is encapsulation, meaning: methods and properties of an object might have different accessibility scopes. Some methods might be public, some fields might be private, for example. The dreaded deep reflection tries to make everything visible to client code, no matter if the author of the object wanted some parts to be internal (e.g. private). As such, by many (and not only OOP purists), the deep reflection is seen as an abuse of the OOP.

In the Java world the daemon of the deep reflection is usually summoned by the setAccessible(true) call, like in the following example:

import java.lang.reflect.Field;
import java.time.Instant;
import java.util.Arrays;

public class StronglyEncapsulatedInternals {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        deepLookInto("Hello --illegal-access=deny!");

    private static void deepLookInto(String input) {
        try {
            Field valueField = String.class.getDeclaredField("value");
            byte[] actualValue = (byte[])valueField.get(input);
            System.out.println("Actual char array in string is: " + Arrays.toString(actualValue));
        } catch (NoSuchFieldException | IllegalAccessException e) {

    private static void secondsIn(Instant now) throws NoSuchFieldException, IllegalAccessException {
        var secondsField = now.getClass().getDeclaredField("seconds");

It may look innocent at the first glance, right? This is a kind of code we see a lot in the Java world, and technically it was a perfectly valid code until Java 8.

Things started to change after project Jigsaw saw the sunlight, as part of the Java 9 release (in September 2017). Then we started to see the warning quoted above, telling us (in a rephrased way, of course), that actually calling setAccessible(true) on fields and methods which aren’t accessible to us, is seen as illegal reflective access operation and as such might be A Bad Idea™.

Please note it doesn’t raise a warning about any “reflective access operation”, but “illegal reflective access operation”. Using reflection on parts which are still accessible to us (like the public part of the API) is still considered legal. In fact, should Java care if a public method is called directly or using reflection? What’s wrong in calling a public getter or accessor? Exactly…

There were many discussions and tickets about this warning. Java 9 introduced a new parameter, --illegal-access with four possible values:

  • permit (default) - which generates the warning only when the first illegal access was detected
  • warn - emitting the warning after each illegal access detection
  • debug - adding also a stack trace (pretty much like an exception) to warn
  • deny - acting like debug for the first detection, then killing the program.

Because the default was named permit, it was enough for some people to say… it’s permitted! Doesn’t matter the message was quite explicitly saying ‘… illegal …’

What about All illegal access operations will be denied in a future release? ‘Naaah, not much to worry about. I don’t think they will…’

Also, some people used to say that ‘it’s the way our library works, and it’s just a warning; ignore it and live happily ever after’. Worse, some other folks decided to follow this advice, not even worrying about --add-opens.

Trouble is, the ever after might have ended with release of Java 16 in March 2021. ;-)

In Java 16 --illegal-access=deny

You shall not use

This means that if your application summons the demons of deep reflection, either in the code you & your team wrote, or in a 3rd party dependency, your application will simply go belly up. It will stop and work no more with the defaults. Now this pesky warning can’t be simply ignored and something has to be done.

Question is, what are you going do, my fellow Java developer?

IMHO the worst thing you can do is to stay with Java versions <=15 (will your clients & users accept that?) or use the --illegal-access parameter set to permit. It’s because it doesn’t address the issue in any way, it’s like putting a lipstick on a pig or tossing @Ignore/@Disable on a falling JUnit test. Also, because this time the warning also says:

VM warning: Option --illegal-access is deprecated and will be removed in a future release

Yes, even the possibility of setting --illegal-access=deny might be taken away from us one day. I’m not in the OpenJDK/Oracle Java team, so I can’t tell you when this happens (another 3,5 years?) or if it happens at all. Thing is: the --illegal-access switch is now deprecated for removal and one day it might be removed. Gone.

Chances are this will happen for Java 17 as seen in JEP 403. Seventeen. It’s not a typo. ;-)

It’s yet another warning you may ignore on your way of producing legacy code for your future self (and future generations, in case your career plan is to go for a manager seat).

The best thing you can do, in my humble opinion, is to eliminate the problem:

  • stop using setAccessible(true) in your codebase
  • upgrade your dependencies, so they’re not making that call either
  • in case the dependency vendor says ‘it has to be this way’ (meaning: ‘we can’t change our architecture to stop violating the OOP, sorry (not sorry)'), consider another vendor.

In my reading (and you’re not obligated to agree by any means) modules in Java are not only about modularising the JDK (although that seems to be the primary goal) and annoying library maintainers. Modules in Java >=9 are also a great opportunity to enforce clean(er) architecture relying on the module-info descriptors, without any 3rd party tools.

Talking about the example code above, there are two setAccessible(true) calls. I don’t know why anyone would need to e.g. persist a String using the content of internal value. A string is a string, right? If you need bytes (who doesn’t every now and then?) please go for getBytes() in one of its variants.

The same applies to Instant: IMHO it shouldn’t be persisted by walking one private field after another private field. Instead, the code should recognise it’s working with an Instant and therefore call getEpochSecond() or getNano() (depending on the scenario) and then reconstruct the object using e.g. one ofEpochSecond(...) factory methods (also because it’s @ValueBased by Valhalla).

I understand we might not be able to go for the best option immediately, so there’s also a middle ground, which isn’t that bad:

  1. set --illegal-access=debug or (warn, if you don’t need any stacktrace) and examine the output
  2. based on the output, set --add-opens parameter(s), to legalise the access
  3. please remember that --add-opens statements can be grouped in flat text files, so you can e.g. group them like one file per one dependency

Again, please refer to my Java. Migrating to 11 in real app talk if you’d like to know how this can be done. And, as usual, please read the JEP 396.

Fly, you fools!

Language selector