All Java programmers explicitly or implicitly use reflection to invoke methods. Even if you didn’t do it yourself, the libraries or frameworks that you use will probably do it for you. Let's see how this call is arranged inside and how fast it is. We will look in OpenJDK 8 with the latest updates.
Start learning is actually with the method Method.invoke . Three things are done there:
MethodAccessor
is created and remembered if it is not already there (if this method has not yet been called through reflection).MethodAccessor.invoke
.Access verification consists of two parts. A quick check establishes that both the method and the class containing it have public
modifiers. If this is not the case, then it is verified that the calling class has access to this method. To find out the calling class, use the private method Reflection.getCallerClass()
. Some people, by the way, like to use it in their code. In Java 9, a public Stack-Walking API will appear and it will be extremely reasonable to switch to it.
It is known that access checks can be canceled by calling method.setAccessible(true)
in advance. This setter sets the override
flag to ignore checks. Even if you know that your method is public, setting setAccessible(true)
will save some time on checks.
Let's see how many different scenarios eat up the time. Let's write a simple class with public and non-public methods:
public static class Person { private String name; Person(String name) { this.name = name; } public String getName1() { return name; } protected String getName2() { return name; } }
We write a JMH test, parameterized with two flags: accessible
and nonpublic
. This will be our preparation:
Method method; Person p; @Setup public void setup() throws Exception { method = Person.class.getDeclaredMethod(nonpublic ? "getName2" : "getName1"); method.setAccessible(accessible); p = new Person(String.valueOf(Math.random())); }
And the benchmark itself:
@Benchmark public String reflect() throws Exception { return (String) method.invoke(p); }
I see the following results (3 forks, 5x500ms warm-up, 10x500ms measurement):
(accessible) | (nonpublic) | Time |
---|---|---|
true | true | 5,062 ± 0,056 ns / op |
true | false | 5,042 ± 0,032 ns / op |
false | true | 6.078 ± 0.039 ns / op |
false | false | 5,835 ± 0,028 ns / op |
Indeed, if setAccessible(true)
executed, the result is the fastest. At the same time, it makes no difference whether or not the public method. If setAccessible(false)
, then both tests are slower and the non-public method is slightly slower than the public one. But I expected the difference to be stronger. It mainly helps here that Reflection.getCallerClass()
is an intrinsic of the JIT compiler, which in most cases is replaced by just a compile-time constant : if the JIT compiler inline calls Method.invoke, it knows what method it inlines, and It means and knows what it should return to getCallerClass()
. Further verification is essentially reduced to comparing the package of the called and calling classes. If the package were different, another class hierarchy would be checked.
What happens next? Next you need to create a MethodAccessor
object. By the way, despite the fact that Person.class.getMethod("getName")
will always return a new instance of the Method
object used inside MethodAccessor
will be reused through the root field, which, of course, is nice. However, getMethod
itself getMethod
much slower than a call, so if you plan to call a method several times, it is wise to store the Method
object.
Creating MethodAccessor
's ReflectionFactory . Here we see two scenarios that are controlled by the global settings of the JVM:
-Dsun.reflect.noInflation=true
option is -Dsun.reflect.noInflation=true
(disabled by default), an auxiliary class is generated immediately, which will launch the target method.DelegatingMethodAccessorImpl
wrapper is created, inside which NativeMethodAccessorImpl
is placed. In turn, he considers how many times this method was called. If the number of calls has exceeded the threshold specified by -Dsun.reflect.inflationThreshold
(default 15), then the accessor is inflated: an auxiliary class is generated, as in the first scenario. If the threshold is not reached, the call goes honestly through JNI. Although the implementation on the C ++ side is trivial , the overhead for JNI is quite high.Let's see what will happen to our test if you enable -Dsun.reflect.noInflation=true
and if you use only JNI (for this we set a large threshold -Dsun.reflect.inflationThreshold=100000000
):
(accessible) | (nonpublic) | Default | noInflation | JNI-only |
---|---|---|---|---|
true | true | 5,062 ± 0,056 | 4,935 ± 0,375 | 195,960 ± 1,873 |
true | false | 5.042 ± 0.032 | 4,914 ± 0,329 | 194,722 ± 1,151 |
false | true | 6.078 ± 0.039 | 5,638 ± 0,050 | 196,196 ± 0,910 |
false | false | 5,835 ± 0,028 | 5.520 ± 0.042 | 194.626 ± 0.918 |
Hereinafter, all results are in nanoseconds per operation. As expected, JNI is significantly slower, so it is unnecessary to include such a mode. Curiously, the noInflation mode was slightly faster. This is due to the fact that there is no DelegatingMethodAccessorImpl
, which removes the need for one indirect addressing. By default, the call goes through Method → DelegatingMethodAccessorImpl → GeneratedMethodAccessorXYZ
, and with this option the chain is reduced to Method → GeneratedMethodAccessorXYZ
. Calling Method → DelegatingMethodAccessorImpl
monomorphic and easy to virtualize, but indirect addressing still remains.
By the way, about devirtualization. It is worth noting that our benchmark is bad, because it does not reflect the situation in a real program. In the benchmark, we call only one method through reflection, which means we have only one generated accessor, which is also easily devirtualized and even inline. In a real application this does not happen: there are many accessors. To emulate this situation, let's optionally poison the type profile in the setup
method:
if(polymorph) { Method method2 = Person.class.getMethod("toString"); Method method3 = Person.class.getMethod("hashCode"); for(int i=0; i<3000; i++) { method2.invoke(p); method3.invoke(p); } }
Please note that we did not change the code, the performance of which we measure. We just made a few thousand seemingly useless calls before this. However, these useless calls will spoil the picture a bit: JIT sees that there are a lot of options and cannot substitute the only possible one, making an honest virtual call now. The results will be as follows (poly is a variant with the transformation of a method call into a polymorphic, does not affect JNI)
(acc) | (nonpub) | Default | Default / poly | noInflation | noInflation / poly | JNI-only |
---|---|---|---|---|---|---|
true | true | 5,062 ± 0,056 | 6,848 ± 0,031 | 4,935 ± 0,375 | 6,509 ± 0,032 | 195,960 ± 1,873 |
true | false | 5.042 ± 0.032 | 6.847 ± 0.035 | 4,914 ± 0,329 | 6,490 ± 0,037 | 194,722 ± 1,151 |
false | true | 6.078 ± 0.039 | 7.855 ± 0.040 | 5,638 ± 0,050 | 7.661 ± 0.049 | 196,196 ± 0,910 |
false | false | 5,835 ± 0,028 | 7.568 ± 0.046 | 5.520 ± 0.042 | 7.111 ± 0.058 | 194.626 ± 0.918 |
As you can see, the virtual call adds about 1.5-1.8 ns on my hardware - even more than access checks. It is important to remember that the behavior of a virtual machine in a microbenchmark may differ significantly from that in a real application, and, if possible, recreate conditions close to reality. Here, of course, is still far from reality: at a minimum, all the necessary objects in the processor L1 cache and garbage collection do not occur, because there is no garbage.
Some might think cool, they say that with -Dsun.reflect.noInflation=true
everything becomes faster. Let only 0.3 ns, but still. Yes, plus the first 15 calls will accelerate. Yes, and the working set has slightly decreased, we save the processor cache - solid pluses! Add an option in production and we'll live! So do not. In the benchmark, we tested one scenario, and in nature there are others. For example, some code may call many different methods once. With this option, the accessor will be generated immediately on the first call. And how much does it cost? How long is the accessor generated?
To evaluate this, you can use reflection to clear the private Method.methodAccessor
field (by clearing Method.root
), forcing it to initialize the accessor again. Recording the field through reflection is well optimized, so the test will not slow down much. We get these results. Top line - previously obtained results (polymorph, accessible), for comparison:
(test) | Default | noInflation | Jni |
---|---|---|---|
invoke | 6,848 ± 0,031 | 6,509 ± 0,032 | 195,960 ± 1,873 |
reset + invoke | 227.133 ± 9.159 | 100195,746 ± 2060,810 | 236,900 ± 2,042 |
As you can see, if the accessor is reset, then by default the performance becomes slightly worse than in the version with JNI. But if we completely refuse from JNI, then we get 100 microseconds to launch the method. Generation and loading of a class in runtime compared to a single method call (even through JNI) is, of course, monstrously slow. Therefore, the default behavior "to try 15 times through JNI and only then generate a class" seems extremely reasonable.
In general, remember that there is no magic option that will speed up any application. If she was, she would be enabled by default. What is the point of hiding it from people? Perhaps there is an option that will speed up your application specifically, but do not take on faith any advice like "cut -XX: + MakeJavaFaster, and everything will fly."
What do these generated accessors look like? The bytecode is generated in the MethodAccessorGenerator class using the rather trivial low-level ClassFileAssembler API, which is somewhat similar to the abbreviated ASM library. Classes are given the names of the form sun.reflect.GeneratedMethodAccessorXYZ
, where XYZ
is a global synchronized counter, you can see them in the graph trace and debugger.
The generated class exists only in memory, but we can easily dump it to disk by adding a line to the ClassDefiner.defineClass method
try { Files.write(Paths.get(name+".class"), bytes); } catch(Exception ex) {}
After that, you can look at the class in the decompiler. For our getName1()
method, the following code was generated (FernFlower decompiler and manual variable renaming):
public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public GeneratedMethodAccessor1() {} public Object invoke(Object target, Object[] args) throws InvocationTargetException { if(target == null) { throw new NullPointerException(); } else { Person person; try { person = (Person)target; if(args != null && args.length != 0) { throw new IllegalArgumentException(); } } catch (NullPointerException | ClassCastException ex) { throw new IllegalArgumentException(ex.toString()); } try { return person.getName1(); } catch (Throwable ex) { throw new InvocationTargetException(ex); } } } }
Notice how many extra things you have to do. We need to check that we were given a non-empty object of the desired type and passed an empty argument list or null
instead of the argument list (not everyone knows, but when called through the reflection method without arguments, we can pass null
instead of an empty array). In this case, you must carefully observe the contract: if null
passed instead of an object, then throw a NullPointerException
. If you passed an object of another class, then IllegalArgumentException
. If an exception occurred while executing person.getName1()
, then InvocationTargetException
. And this method has no arguments. And if they are? For example, let's call this method (for a change, now static and returning void
):
class Test { public static void test(String s, int x) {} }
Now the code is much larger:
public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public GeneratedMethodAccessor1() {} public Object invoke(Object target, Object[] args) throws InvocationTargetException { String s; int x; try { if(args.length != 2) { throw new IllegalArgumentException(); } s = (String)args[0]; Object arg = args[1]; if(arg instanceof Byte) { x = ((Byte)arg).byteValue(); } else if(arg instanceof Character) { x = ((Character)arg).charValue(); } else if(arg instanceof Short) { x = ((Short)arg).shortValue(); } else { if(!(arg instanceof Integer)) { throw new IllegalArgumentException(); } x = ((Integer)arg).intValue(); } } catch (NullPointerException | ClassCastException ex) { throw new IllegalArgumentException(ex.toString()); } try { Test.test(s, x); return null; } catch (Throwable ex) { throw new InvocationTargetException(ex); } } }
Notice that instead of int
we have the right to pass Byte
, Short
, Character
or Integer
, and all of this must be transformed. This is where the transformation goes. Such a block will be added for each primitive argument where an expanding transformation is possible. Now it’s also clear why catch is catching a NullPointerException
: it can occur during anboxing and then we must also IllegalArgumentException
. But due to the fact that the method is static, we do not care at all that in the parameter target
. Well, the return null
line appeared, because our method returns void
. All this magic is neatly painted in MethodAccessorGenerator.emitInvoke .
This is how the method call works. The constructors call is similarly arranged. Also, this code is partially reused to deserialize objects. When an accessor already exists, from the point of view of the JVM, it is not very different from the code that you would write yourself manually, so reflection starts working very quickly.
In conclusion, starting with Java 7, the java.lang.invoke API has appeared, which also allows you to call methods dynamically, but it works quite differently.
Source: https://habr.com/ru/post/318418/
All Articles