This paper mainly introduces the influence of automatic unpacking and boxing on the performance of operation from the perspective of bytecode and memory occupation.
If you want to understand bytecode, you need to understand the structure of JVM virtual machine stack and code execution process. Please refer to "deep understanding of Java virtual machine"
This paper refers to the following articles:
Java performance highlights: autoboxing / Unboxing
The function of packaging class in JAVA
Deep analysis of boxing and unboxing in Java (both shallow and deep)
Recently, in the software challenge of Huawei in 2020, the value of Integer class is often saved by List. When doing performance optimization, I thought about the performance loss of packing / unpacking, and I specially experimented.
The following code starts from 0 and continues to the maximum value that an int type can express.
long start = System.currentTimeMillis(); Long sum = 0L; // Add using wrapper classes for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("time consuming:"+(end-start)/1000.0); // Output: // 2305843005992468481 // Time: 15.175
start = System.currentTimeMillis(); long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; // Add using basic data types } System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("time consuming:"+(end-start)/1000.0); // Output: // 2305843005992468481 // Time: 1.643
The only difference between the two codes is that the sum of the former is the wrapper class long, and the sum of the latter is the basic type long
Cause analysis
In short: performance loss caused by packing and unpacking. Packing will create wrapper classes in heap space. Frequent creation will lead to a lot of heap space fragmentation
Wrapper Class: Java is an object-oriented programming language, but the eight basic data types in Java are not object-oriented. In order to use conveniently and solve this problem, a corresponding class representation is designed for each basic data type in the design of class, so the classes corresponding to the eight basic data types are collectively referred to as Wrapper Class), and the wrapper classes are all in the java.lang package.
Boxing: basic data type - > packing type (take Integer as an example)
int a = 5; // Constructor method Integer a1 = new Integer(a); // Value type to package type Integer a2 = new Integer(5); Integer a3 = new Integer("5"); // String type repackaging class // Integer.valueOf() Integer a4 = Integer.valueOf(a); Integer a5 = Integer.valueOf(5);
Note here that the initialization of the wrapper class requests space in the heap. Whether you use new Integer() or Integer.valueOf(). Because Integer.valueOf() essentially creates a new Integer object in the form of a tool class, but first queries whether the created value is in the cache pool (- 128127).
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
Unboxing: wrapper class - > basic data type (take Integer as an example)
Integer b = new Integer(5); // Suppose we already have a wrapper class // Call the xxValue() method int b1 = b.intValue(); // Convert to int type double b2 = b.doubleValue(); // Convert to double type long b3 = b.longValue(); // Convert to long type
Since version 1.5 of JDK, the syntax of automatic unpacking has been introduced, that is, the system will automatically convert the basic data types and corresponding packaging classes.
//Compiler executed integer a = integer.valueof (5) Integer a = 5; //Auto unpacking, actually executed int b = a.intValue() int b = a;
What happened to the wrapper class + = from the bytecode perspective?
Take a simple code as an example:
Integer a = 5; a+=1;
The bytecode content is (the line number of bytecode is not necessarily continuous):
0 iconst_5 1 invokestatic #16 <java/lang/Integer.valueOf> 4 astore_1 5 aload_1 6 invokevirtual #22 <java/lang/Integer.intValue> 9 iconst_1 10 iadd 11 invokestatic #16 <java/lang/Integer.valueOf> 14 astore_1
We can see that:
Integer a = 5 corresponding to 0, 1, 4 of bytecode: press constant 5 into the stack - > implicitly call integer, valueof() - > store address value of new wrapper class to variable a
a+=1 corresponds to 6-14 lines: load variable a - > unpack to int basic type (call intValue) - > press constant 1 into the stack - > pop up 1 and add the value of int type of unpacked a, press the added value back to the stack (or int) - > call Integer.valueOf(), unpack the result - > store the address value of the new packing class to variable a
Summary: we can see that to calculate the packing class, you need to unpack it first and then pack it. This process also requires requesting space from the heap. By contrast, basic types of operations are much more efficient
int a = 5; a += 1;
Its bytecode is:
0 iconst_5 1 istore_1 2 iinc 1 by 1
Basic data type operations are performed in the stack.
Understanding of wrapper class from the perspective of memory usage+=
Use the Jconsole tool that comes with JDK to check the memory usage of the previous two pieces of code.
Memory usage of Long (wrapper class)
Memory usage of basic data types
At the beginning of program execution, only about 8M memory is occupied. Using packaging class to store the operation results will lead to memory occupation of up to 80M, with a peak value of 138M, and 505 garbage collections have been carried out in the younger generation; when using basic data types, memory occupation is always around 8M, and there is no garbage collection.
Therefore, we can conclude that frequent operations on wrapper classes will take up a lot of memory space, resulting in inefficient execution.
Set of enhanced foreach loop traversal wrapper classes
We know that the operation of the wrapper class will make it inefficient, so what should we do when traversing? Which style is better?
For example, what's the difference between the following methods of addition?
// Use flow to generate a List of 0 to 40000000 List<Long> arr = LongStream.rangeClosed(0, 40000000).boxed().collect(Collectors.toList()); // ① When taking variables in for, it is the basic type, and the result is stored as the basic type long start = System.currentTimeMillis(); long sum = 0; for(long l:arr) { // The temporary variable l is created in the stack to directly convert the elements in the arr to LongValue sum += l; } System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("time consuming:"+(end-start)/1000.0); // ② When taking variables in for, it is the packing type, and the result is stored as the basic type start = System.currentTimeMillis(); long Sum = 0L; for(Long l:arr) { // Temporary variable l created in the heap Sum += l; // Call l.longValue() when adding } System.out.println(Sum); end = System.currentTimeMillis(); System.out.println("time consuming:"+(end-start)/1000.0); // ③ When taking variables in for, it is the packing type, and the result is stored as the packing class start = System.currentTimeMillis(); Long SUM = 0L; for(Long l:arr) {// Temporary variable l created in the heap SUM += l; // l and SUM call longValue(), add them and then pack them } System.out.println(SUM); end = System.currentTimeMillis(); System.out.println("time consuming:"+(end-start)/1000.0); // Output: //800000020000000 //① Time: 0.269 //800000020000000 //② Time: 0.326 //800000020000000 //③ Time: 1.089
The bytecode file will not be pasted here, so it's a little tedious to take the loop. Variable reuse also requires a local variable table.
The first method is the fastest, its local variable l is the basic type, and only needs to apply for space in the stack; its result Sum is also saved in the stack; unpacking occurs when the element is taken out.
The second method is slightly slower than the first one, and its local variable l is a packing class, which needs to apply for space in the heap; however, its result is also saved in the stack; unpacking occurs when two numbers are added.
The third method is the slowest. Its local variable l is a packing class, which needs to apply for space in the heap. Its result SUM also needs to apply for space in the heap. What's more, unpacking occurs when two numbers are added. After adding, a new packing class is created.
summary
-
The packing class will be unpacked automatically when calculating (packing class and packing class / packing class and basic type).
-
If the result is still stored in a wrapper class, boxing will happen again.
-
Frequent unpacking and packing will lead to excessive memory fragmentation, frequent garbage collection and performance impact.