Through Source Code Optimization: How many options do you know for String string performance optimization?

Posted by barrylee on Wed, 19 Feb 2020 19:06:40 +0100

Recommended reading:

String strings are one of the most common types in the system and occupy a large amount of memory in the system. Therefore, using strings efficiently can improve the performance of the system.

For string optimization, I summarized three scenarios to share during my work and learning process:

1. Optimized construction of very large strings

Authentication environment: jdk1.8

Decompilation tool: jad

1. Download decompile tool jad, Baidu Download

2. Validation

First execute an example 1 code:

public class test3 {
    public static void main(String[] args) {
        String str="ab"+"cd"+"ef"+"123";
    }
}

After execution, decompile with the decompile tool jad: jad-o-a-s d.java test.class

Decompiled code:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) annotate 
// Source File Name:   test.java
package example;
public class test
{
    public test()
    {
    //    0    0:aload_0         
    //    1    1:invokespecial   #1   <Method void Object()>
    //    2    4:return          
    }
    public static void main(String args[])
    {
        String str = "abcdef123";
    //    0    0:ldc1            #2   <String "abcdef123">
    //    1    2:astore_1        
    //    2    3:return          
    }
}

Case 2:

public class test1 {
    public static void main(String[] args)
    {
        String s = "abc";
        String ss = "ok" + s + "xyz" + 5;
        System.out.println(ss);
    }
}

After jad-o-a-s d.java test1.class is decompiled with the decompile tool jad:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) annotate 
// Source File Name:   test1.java

package example;

import java.io.PrintStream;

public class test1
{
    public test1()
    {
    //    0    0:aload_0         
    //    1    1:invokespecial   #1   <Method void Object()>
    //    2    4:return          
    }
    public static void main(String args[])
    {
        String s = "abc";
    //    0    0:ldc1            #2   <String "abc">
    //    1    2:astore_1        
        String ss = (new StringBuilder()).append("ok").append(s).append("xyz").append(5).toString();
    //    2    3:new             #3   <Class StringBuilder>
    //    3    6:dup             
    //    4    7:invokespecial   #4   <Method void StringBuilder()>
    //    5   10:ldc1            #5   <String "ok">
    //    6   12:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //    7   15:aload_1         
    //    8   16:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //    9   19:ldc1            #7   <String "xyz">
    //   10   21:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //   11   24:iconst_5        
    //   12   25:invokevirtual   #8   <Method StringBuilder StringBuilder.append(int)>
    //   13   28:invokevirtual   #9   <Method String StringBuilder.toString()>
    //   14   31:astore_2        
        System.out.println(ss);
    //   15   32:getstatic       #10  <Field PrintStream System.out>
    //   16   35:aload_2         
    //   17   36:invokevirtual   #11  <Method void PrintStream.println(String)>
    //   18   39:return          
    }
}

Based on the decompilation results, you can see that the interior is actually stitched by StringBuilder.

Execute the code for example 3 again:

public class test2 {
    public static void main(String[] args) {
        String s = "";
        Random rand = new Random();
        for (int i = 0; i < 10; i++) {
            s = s + rand.nextInt(1000) + " ";
        }
        System.out.println(s);
    }
}

After decompiling jad-o-a-s d.java test2.class with the decompile tool jad, it is found that its interior is also stitched by StringBuilder:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) annotate 
// Source File Name:   test2.java
package example;
import java.io.PrintStream;
import java.util.Random;
public class test2
{
    public test2()
    {
    //    0    0:aload_0         
    //    1    1:invokespecial   #1   <Method void Object()>
    //    2    4:return          
    }
    public static void main(String args[])
    {
        String s = "";
    //    0    0:ldc1            #2   <String "">
    //    1    2:astore_1        
        Random rand = new Random();
    //    2    3:new             #3   <Class Random>
    //    3    6:dup             
    //    4    7:invokespecial   #4   <Method void Random()>
    //    5   10:astore_2        
        for(int i = 0; i < 10; i++)
    //*   6   11:iconst_0        
    //*   7   12:istore_3        
    //*   8   13:iload_3         
    //*   9   14:bipush          10
    //*  10   16:icmpge          55
            s = (new StringBuilder()).append(s).append(rand.nextInt(1000)).append(" ").toString();
    //   11   19:new             #5   <Class StringBuilder>
    //   12   22:dup             
    //   13   23:invokespecial   #6   <Method void StringBuilder()>
    //   14   26:aload_1         
    //   15   27:invokevirtual   #7   <Method StringBuilder StringBuilder.append(String)>
    //   16   30:aload_2         
    //   17   31:sipush          1000
    //   18   34:invokevirtual   #8   <Method int Random.nextInt(int)>
    //   19   37:invokevirtual   #9   <Method StringBuilder StringBuilder.append(int)>
    //   20   40:ldc1            #10  <String " ">
    //   21   42:invokevirtual   #7   <Method StringBuilder StringBuilder.append(String)>
    //   22   45:invokevirtual   #11  <Method String StringBuilder.toString()>
    //   23   48:astore_1        

    //   24   49:iinc            3  1
    //*  25   52:goto            13
        System.out.println(s);
    //   26   55:getstatic       #12  <Field PrintStream System.out>
    //   27   58:aload_1         
    //   28   59:invokevirtual   #13  <Method void PrintStream.println(String)>
    //   29   62:return          
    }
}

Taken together with the case study, it is found that when a string is spliced with'+', there are several internal cases:

1.'+'splices directly into constant variables, such as'ab'+'cd'+'ef'+'123', and internal compilation splices several into a single constant string.

2.'+'spliced variable-containing strings, such as case 2:'ok' + S +'xyz'+ 5, internal compilation is actually a new StringBuilder to splice through append;

3. The case 3 cycle is essentially a'+'splicing with variable strings, so StringBuilder is also created for splicing when compiling internally.

Comparing the three cases, it is found that in the third case, a new StringBuilder object is created each time a loop is made, which increases the memory of the system and, in turn, decreases the performance of the system.

Therefore, in a single-threaded environment, StringBuilder can be used explicitly to stitch strings, avoiding a new StringBuilder object per loop; in a multi-threaded environment, thread-safe StringBuffers can be used, but with lock race, StringBuffer performance is less than StringBuilder.

In this way, the string splicing is optimized.

2. How do I save memory using String.intern?

Before answering this question, you can test a piece of code:

1. First set -XX:+PrintGCDetails-Xmx6G-Xmn3G in idea to print GC log information, as shown in the following figure:

2. Execute the following example code:

public class test4 {
    public static void main(String[] args) {
        final int MAX=10000000;
        System.out.println("No need intern: "+notIntern(MAX));
//      System.out.println("Use intern:"+intern(MAX)));
    }
    private static long notIntern(int MAX){
        long start = System.currentTimeMillis();
        for (int i = 0; i < MAX; i++) {
            int j = i % 100;
            String str = String.valueOf(j);
        }
        return System.currentTimeMillis() - start;
    }
/*
    private static long intern(int MAX){
        long start = System.currentTimeMillis();
        for (int i = 0; i < MAX; i++) {
            int j = i % 100;
            String str = String.valueOf(j).intern();
        }
        return System.currentTimeMillis() - start;
    }*/

GC logs not using intern:

No need intern: 354
[GC (System.gc()) [PSYoungGen: 377487K->760K(2752512K)] 377487K->768K(2758656K), 0.0009102 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 760K->0K(2752512K)] [ParOldGen: 8K->636K(6144K)] 768K->636K(2758656K), [Metaspace: 3278K->3278K(1056768K)], 0.0051214 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 2752512K, used 23593K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 2359296K, 1% used [0x0000000700000000,0x000000070170a548,0x0000000790000000)
  from space 393216K, 0% used [0x0000000790000000,0x0000000790000000,0x00000007a8000000)
  to   space 393216K, 0% used [0x00000007a8000000,0x00000007a8000000,0x00000007c0000000)
 ParOldGen       total 6144K, used 636K [0x0000000640000000, 0x0000000640600000, 0x0000000700000000)
  object space 6144K, 10% used [0x0000000640000000,0x000000064009f2f8,0x0000000640600000)
 Metaspace       used 3284K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 359K, capacity 388K, committed 512K, reserved 1048576K

According to the printed log analysis: without intern, the execution time is 354 MS and the memory consumption is 2422k;

Using GC logs from intern:

Use intern: 1515
[GC (System.gc()) [PSYoungGen: 613417K->1144K(2752512K)] 613417K->1152K(2758656K), 0.0012530 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1144K->0K(2752512K)] [ParOldGen: 8K->965K(6144K)] 1152K->965K(2758656K), [Metaspace: 3780K->3780K(1056768K)], 0.0079962 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2752512K, used 15729K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 2359296K, 0% used [0x0000000700000000,0x0000000700f5c400,0x0000000790000000)
  from space 393216K, 0% used [0x0000000790000000,0x0000000790000000,0x00000007a8000000)
  to   space 393216K, 0% used [0x00000007a8000000,0x00000007a8000000,0x00000007c0000000)
 ParOldGen       total 6144K, used 965K [0x0000000640000000, 0x0000000640600000, 0x0000000700000000)
  object space 6144K, 15% used [0x0000000640000000,0x00000006400f1740,0x0000000640600000)
 Metaspace       used 3786K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 420K, capacity 428K, committed 512K, reserved 1048576K

Log analysis: Without intern, the execution time is 1515 MS and the memory usage is 16694 k;

To summarize, with intern, memory is less than without intern, but it saves memory while increasing time complexity.I've tried adding another 0 to MAX=10000000, and using intern can take up to 11 seconds to execute, so it's not recommended when traversing data that is too large.

Therefore, the premise of using intern is to take into account specific usage scenarios.

Here, you can confirm that using String.intern does save memory.

Next, analyze the differences between different JDK versions of intern.

In JDK1.6, the pool of string constants is in the method area, which is a permanent generation.

In JDK1.7, the string constant pool was moved to the heap.

In JDK1.8, the pool of string constants moved into metaspace, independent of the heap.

Execute the following examples at versions 1.6, 1.7, and 1.8, respectively:

public class test5 {
    public static void main(String[] args) {

        String s1=new String("ab");
        s.intern();
        String s2="ab";
        System.out.println(s1==s2);

        String s3=new String("ab")+new String("cd");
        s3.intern();
        String s4="abcd";
        System.out.println(s4==s3);
    }
}

Version 1.6

Execution results:

fasle false

Analysis:

When the first part is executed:

1. When code compiles, the constant "ab" is created in the string constant pool first; when new is called, a String object is created in the heap, the string constant created "ab" is stored in the heap, and the String object in the heap returns a reference to s1.

2.s.intern(), where "ab" already exists in the string constant pool, no longer creates a storage copy "ab";

3.s2="ab", S2 points to "ab" in the string constant pool, and s1 points to "ab" in the heap, so they are not equal.

The diagram is as follows:

Execute Part Two:

1. Two "abcds" added by new out are stored in the heap, s3 points to "abcd" in the heap;

2. Execute s3.intern(), when "abcd" copy is stored in the string constant pool, it is found that there is no "abcd" in the constant pool, so it is stored successfully;

3.s4="abcd" points to an existing "abcd" copy in the string constant pool, while s3 points to an "abcd" in the heap. The address of the "abcd" copy is different from the "abcd" address in the heap, so it is false;

Version 1.7

false true

Execute Part One: This section is similar to jdk1.6 in that s1.intern() returns references, not copies.

Execute Part Two:

1.new String("ab")+new String("cd"), first generate "ab" and "cd" in the constant pool, then generate "ab cd" in the heap;

2. When s3.intern() is executed, the object reference to "abcd" is placed in the string constant pool, and it is found that there is no such reference in the constant pool, so it can be successfully placed.When String s4="abcd", which assigns the reference address of "abcd" in the string constant pool to s4, it is equivalent to S4 pointing to the address of "abcd" in the heap, so s3==s4 is true.

Version 1.8

false true

Referring to some blogs on the Web, in version 1.8, intern() works as follows:

If the string constant pool contains a string equivalent to the current object, the string in the constant pool is returned; if it does not exist, the string is stored in the constant pool and a reference to the string is returned.

To sum up, among the three versions, there are the following differences when using intern if no corresponding string exists in the string constant pool:

For example:

String s1=new String("ab"); s.intern();

jdk1.6: If there is no "ab" in the string constant pool, a copy of "ab" will be stored in the constant pool with an address different from the "ab" address in the heap;

jdk1.7: If there is no "ab" in the string constant pool, the object reference to "ab" will be placed in the string constant pool at the same address as the "ab" in the heap;

jdk1.8: If the string constant pool contains a string equivalent to the current object, the string in the constant pool is returned; if it does not exist, it is stored in the constant pool and a reference to the string is returned.

3. How do I use string splitting?

For simple string splitting, indexOf can be used instead of split, because the performance of split is not stable enough, for simple string splitting, indexOf can be used first.

Topics: Java MySQL less Spring