📜 ⬆️ ⬇️

Uninvented story about performance, reflection and java.lang.Boolean

Once, in the icy winter season (although it was March in the yard) I needed to dig in a heap (of what is called a heap dump, and not what you thought). Having uncovered VisualVM, I opened the required file and went to the OQL console. While the court is the case, my attention was drawn to requests that are available out of the box. One of them, entitled "Too many Booleans", was especially conspicuous. In his description in English and white it says:


Check if there is a boolean on the heap (only Boolean.TRUE and Boolean.FALSE are necessary).

Feel right? Here I am penetrated.


Where can the extra "big" Boolean come from if Java has been able to wrap simple types in wrappers on its own long ago and vice versa? If the code is written correctly, all the Boolean.TRUE/Boolean.FALSE boolean to the object will use Boolean.TRUE/Boolean.FALSE , created at the first call to the java.lang.Boolean class. This is where the request comes from, which I noticed:


 select toHtml(a) + " = " + a.value from java.lang.Boolean a where objectid(a.clazz.statics.TRUE) != objectid(a) && objectid(a.clazz.statics.FALSE) != objectid(a) 

Having executed it, I was surprised to find many separate objects of the jlBoolean class. The heap didn’t say anything about their origin, so I wanted to figure out where they came from. Memory profiling showed a curious picture: new Boolean- constantly appeared, accumulated and after some time disappeared into the mouth of the GC. At certain points in time, their account could go to tens of thousands, and they occupied about 1 MB of memory.



Strictly speaking, they were not a problem, because they did not create leaks, they were quickly cleared, and what is 1 MB these days? However, the mechanism of the emergence of new objects was interesting in itself, so I began to dig.


First, let's see how to get an object of class Boolean . JDK gives us the following features:


 /*1*/ Boolean b1 = new Boolean(true); //@Deprecated   Java 9 /*2*/ Boolean b2 = new Boolean("true"); //@Deprecated   Java 9 /*3*/ Boolean b3 = true; /*4*/ Boolean b4 = Boolean.valueOf(true); /*5*/ Boolean b5 = Boolean.valueOf("true"); /*6*/ Boolean b6 = Boolean.parseBoolean("true"); 

What is the difference between them? Only the first and second methods return a new object (for the constructor). The third method in the assembly is reduced to the fourth, which, like the last two, returns Boolean.FALSE/Boolean.TRUE from the presence.


So, the reason for the appearance of many identical (by content) objects is to wrap a simple boolean in a wrapper, but not by calling Boolean.valueOf , but by Boolean.valueOf constructor. The first suspicion fell on the library developers. Well, let's try to find possible punctures. Search for the source of the dependencies connected (thanks to the developers of "Ideas"), did not reveal anything suspicious, so I had to get up the debugger in the constructor, and where the curve would output.


The very first hit confirmed the hunch: it smacked of reflection, in particular its use for processing annotations. Consider the code:


 @Transactional(readOnly = true) public class MyService { } 

During execution, reflection is used to read the @Transactional properties (in this case, readOnly ). This happens as follows (Spring Core 5.0.4.RELEASE):



Moving up the chain, we come to sun.reflect.DelegatingMethodAccessorImpl , the sources of which we can still read, but then begins the mysterious GeneratedMethodAccessor13 . And although, according to the debugger, this class is also in the sun.reflect package, its code is not available for us from the Idea, and the name itself suggests that the class was created on the fly. And it is his invoke() method that ultimately calls the Boolean(boolean value) constructor Boolean(boolean value) .


The case is complicated: now you need to somehow get the code for this method. I could not solve this problem in a hurry, so I had to go a different way: if you cannot get the code itself, then you can try to reliably uncover the way it was created. To do this, we put a simple experiment with calling a reflection method that returns a boolean :


 import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { int invocationCount = 20; Object[] booleans = new Object[invocationCount]; Method method = Main.class.getMethod("f"); for (int i = 0; i < invocationCount; i++) { booleans[i] = invoke(method); } } public static Object invoke(Method method) throws Exception { return method.invoke(null); } public static boolean f() { return false; } } 

By the way, we didn’t remove the jlBoolean from the jlBoolean constructor, right? That's only during the first 16 passes through the loop at this point the debugger does not stop! Once again: each method.invoke(null) execution returns a new object (that is, booleans[i-1] != booleans[i] ), and the constructor of this object itself is not called.


If during one of the first 16 passes we stop inside DelegatingMethodAccessorImpl.invoke() and move on, we will find that now there is a class that was absent before, namely sun.reflect.NativeMethodAccessorImpl :



Here he is:


 class NativeMethodAccessorImpl extends MethodAccessorImpl { private final Method method; private DelegatingMethodAccessorImpl parent; private int numInvocations; NativeMethodAccessorImpl(Method method) { this.method = method; } public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { // We can't inflate methods belonging to vm-anonymous classes because // that kind of class can't be referred to by name, hence can't be // found from the generated bytecode. if (++numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); parent.setDelegate(acc); } return invoke0(method, obj, args); } void setParent(DelegatingMethodAccessorImpl parent) { this.parent = parent; } private static native Object invoke0(Method m, Object obj, Object[] args); 

This is the answer to the question why we did not see the constructor call: instead, the platform-dependent method invoke0() creates an object somewhere in the depths of the VM. The same code explains why the constructor appears on the 17th pass in the call chain, and NativeMethodAccessorImpl disappears: after the number of calls to the f() method exceeds the value returned by ReflectionFactory.inflationThreshold() (for JDK 8/9/10/11 this 15), on the fly, MethodAccessorGenerator creates an intermediary for it, which in the form of a MethodAccessorImpl object MethodAccessorImpl passed to a level higher than DelegatingMethodAccessorImpl- .


Starting from the 17th pass, we observe the picture we are used to (the newly created implementation of MethodAccessorImpl ):



Thus, two places are found that return new objects: the native method NativeMethodAccessorImpl.invoke0() and code created on the fly using new MethodAccessorGenerator().generateMethod() . Let's go on the path of least resistance and for now stay on the side of Java. Since out of the box (in the case of JDK 8 with which the application is assembled) only the compiled class (from rt.jar) is available to us, and decompiling gives the unintelligible false origin with var123 instead of variable names and without any explanation, you will have to look at repositories


Familiarization with the source code MethodAccessorGenerator puts everything in its place: a bytecode is created here (yes, it is the bytecode in its original form, namely, in the form of an array of bytes). The key method for us is called emitInvoke() , in which we find the one we need :


 if (!isConstructor) { // Box return value if necessary if (isPrimitive(returnType)) { cb.opc_invokespecial(ctorIndexForPrimitiveType(returnType), typeSizeInStackSlots(returnType), 0); } else if (returnType == Void.TYPE) { cb.opc_aconst_null(); } } 

Line 663: what is called, overlooked when reading. Instead of calling valueOf() to wrap simple return values, we entered a constructor call. Obviously, this is fixable: it’s all about business that the invokespecial call invokespecial to be replaced by invokestatic , and instead of the constructor, the factory method is passed.


Alas, familiarization with source codes cherry "Nines" showed that (very suddenly) I am not the only one so clever, and I cannot win the laurels in this matter, since everything is already fixed before us :


 if (!isConstructor) { // Box return value if necessary if (isPrimitive(returnType)) { cb.opc_invokestatic(boxingMethodForPrimitiveType(returnType), typeSizeInStackSlots(returnType), 0); } else if (returnType == Void.TYPE) { cb.opc_aconst_null(); } } 

This is more obvious (JDK 9 on the left):



The problem was discovered long ago, and the corresponding problem has existed since 2004 (!) .


There is a discussion on the topic:


Start


Continuation


Let's check now if it’s better. Switching to the "nine" and repeating our experience we will see this:



After 16 hits, code was created using Boolean.valueOf() and returning Boolean.TRUE/Boolean.FALSE . True, there is still a problem with the NativeMethodAccessorImpl.invoke0() method, which stubbornly returns new objects (even in 10-ke). There is nothing to do, you need to go to the source code of the VM and see if we can do something about it.


I did not find any direct references to invoke0 , but in discussions on the topic the reflection.cpp file has surfaced and it looks like our constructor is being invoked by the invoke () method. In this method, the most important for us is the last line :


 return Reflection::box((jvalue*)result.get_value_addr(), rtype, THREAD); 

Reflection::box code :


 oop Reflection::box(jvalue* value, BasicType type, TRAPS) { if (type == T_VOID) { return NULL; } if (type == T_OBJECT || type == T_ARRAY) { // regular objects are not boxed return (oop) value->l; } oop result = java_lang_boxing_object::create(type, value, CHECK_NULL); if (result == NULL) { THROW_(vmSymbols::java_lang_IllegalArgumentException(), result); } return result; } 

The main thing is highlighted with empty lines. Now the code is java_lang_boxing_object :: create


 oop java_lang_boxing_object::create(BasicType type, jvalue* value, TRAPS) { oop box = initialize_and_allocate(type, CHECK_0); if (box == NULL) return NULL; switch (type) { case T_BOOLEAN: box->bool_field_put(value_offset, value->z); break; //.... case-case-case return box; } oop java_lang_boxing_object::initialize_and_allocate(BasicType type, TRAPS) { Klass* k = SystemDictionary::box_klass(type); if (k == NULL) return NULL; instanceKlassHandle h (THREAD, k); if (!h->is_initialized()) h->initialize(CHECK_0); return h->allocate_instance(THREAD); } 

As we see, the VM first creates a new empty object, and only then flushes the value into it and returns it to the outside. This explains the appearance of a new object without calling the constructor. Perhaps, for T_BOOLEAN , it would be possible to cache two values ​​at the VM level, but it is not clear whether the game is worth the candle.


In the dry residue


How much will we win after moving to the "nine"? Calculate:


 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"}) public class ReflectiveCallBenchmark { @Benchmark public Object invoke(Data data) throws Exception { return data.method.invoke(data); } @State(Scope.Thread) public static class Data { Method method; @Setup public void setup() throws Exception { method = getClass().getMethod("f"); } public boolean f() { return true; } } } 

JDK 8JDK 9JDK 10JDK 11
BenchmarkModeCntScoreScoreScoreScoreUnit
invokeavgtthirty9.97.07,67.7ns / op
invoke: · gc.alloc.rate.normgcprofthirty32sixteensixteensixteenB / op

All the costs of a reflexive challenge are measured here. If you need to measure the difference between wrapping a boolean using a constructor and valueOf , then you can use a simpler measurement:


 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"}) public class BooleanInstantiationBenchmark { @Benchmark public Boolean constructor(Data data) { return new Boolean(data.value); } @Benchmark public Boolean valueOf(Data data) { return Boolean.valueOf(data.value); } @State(Scope.Thread) public static class Data { @Param({"true", "false"}) boolean value; } } 

JDK 8JDK 9JDK 10JDK 11
BenchmarkModeCntScoreScoreScoreScoreUnit
valueOfavgtthirty3.73.43.63.5ns / op
constructoravgtthirty7.45.05.55.9ns / op
valueOf: · gc.alloc.rate.normgcprofthirty0000B / op
constructor: · gc.alloc.rate.normgcprofthirtysixteensixteensixteensixteenB / op

Total: -16 bytes and -2..3 ns for one reflexive method call that returns a boolean . Not bad, as for a simple change, especially considering the frequency of use of reflection in bloody Interprize, as well as the fact that the improvement also applies to other primitives. Notice that the performance of the code created using new MethodAccessorGenerator().generateMethod() , and not the creation of an object inside the VM, is measured.


As a conclusion: the improvement described is very insignificant in itself, and its effect is almost imperceptible. Although it is such trifles that are put together give an increase in the productivity of new editions of Java.


PS The value returned by the ReflectionFactory.inflationThreshold() method can be overridden using the -Dsun.reflect.inflationThreshold property passed by the argument when the VM is started. Thus, if you have already moved to the "nine", then using this flag you can reduce the threshold for creating a bytecode for a reflexive call. This may slow down the launch of the application, but it will be less "littering". The documentation explains why this mechanism was invented.


PPS Considered classes ( MethodAccessorGenerator , NativeMethodAccessorImpl , DelegatingMethodAccessorImpl , MethodAccessorImpl ) starting with the "nine" are moved to the jdk.internal.reflect package.


PP P S. Note that in the framework of the described improvement, a significant number of classes have undergone changes, not just the MethodAccessorGenerator .


PPPPS The jlBoolean device can be slightly simplified and win a couple of ns on it;)


')

Source: https://habr.com/ru/post/351988/


All Articles