들어가며

String의 불변성 - StringBuilder와 StringBuffer의 차이점을 다뤘습니다. 이번에는 String + 연산에 대해서 조금 더 자세하게 다뤄보고도록 하겠습니다. 이전 글을 보고 오시는것을 추천드립니다.

 

 

[JAVA] StringBuilder, StringBuffer 의 차이점과 주의사항

들어가며이 글에서는 StringBuilder, StringBuffer 차이점뿐만 아니라 사용 이유에 대해서도 다루고있습니다. [JAVA] String의 불변성(immutability)들어가며JAVA를 처음 접할 때, String은 불변객체라고 배우고

tmd8633.tistory.com

 

 

 

이 글을 쓰는 이유

지난시간에 String 의 불변성과 StringBuilder에 대해서 알아보았습니다. 이전 글들을 작성하면서 String + 연산이 최적화 되었다는 얘기를 수 없이 보았습니다.

 

        String[] arr = {"2", "3", "4"};
        String str = "1";

        for (String s : arr) {
            str += s;
        }

이렇게 String + 연산을 하게되면 String의 불변성 때문에 "12", "123", "1234"의 메모리가 heap 영역에 저장되고 GC 대상도 증가하게 되는 것은 이제 충분히 이해가 됩니다.

그런데 이 과정을 JDK 5 부터 컴파일 시에 내부적으로 StringBuilder를 사용해서 최적화를 하였고, 완전히 최적화가 되지않아서(매번 StringBuilder객체가 생성되는 문제 등) JDK 9부터 지금까지 StringConcatFactory가 사용되어 + 연산이 완전 최적화 되었다고 했습니다. 이를 실제로 알아보기 위해 bytecode를 까봤습니다.

 

 

public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 13 L0
    ICONST_3
    ANEWARRAY java/lang/String
    DUP
    ICONST_0
    LDC "2"
    AASTORE
    DUP
    ICONST_1
    LDC "3"
    AASTORE
    DUP
    ICONST_2
    LDC "4"
    AASTORE
    ASTORE 1
   L1
    LINENUMBER 14 L1
    LDC "1"
    ASTORE 2
   L2
    LINENUMBER 16 L2
    ALOAD 1
    ASTORE 3
    ALOAD 3
    ARRAYLENGTH
    ISTORE 4
    ICONST_0
    ISTORE 5
   L3
   FRAME FULL [[Ljava/lang/String; [Ljava/lang/String; java/lang/String [Ljava/lang/String; I I] []
    ILOAD 5
    ILOAD 4
    IF_ICMPGE L4
    ALOAD 3
    ILOAD 5
    AALOAD
    ASTORE 6
   L5
    LINENUMBER 17 L5
    ALOAD 2
    ALOAD 6
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "\u0001\u0001"
    ]
    ASTORE 2
   L6
    LINENUMBER 16 L6
    IINC 5 1
    GOTO L3
   L4
    LINENUMBER 19 L4
   FRAME CHOP 3
    RETURN
   L7
    LOCALVARIABLE s Ljava/lang/String; L5 L6 6
    LOCALVARIABLE args [Ljava/lang/String; L0 L7 0
    LOCALVARIABLE arr [Ljava/lang/String; L1 L7 1
    LOCALVARIABLE str Ljava/lang/String; L2 L7 2
    MAXSTACK = 4
    MAXLOCALS = 7
}

 

StringConcatFactory.makeConcatWithConstants 메소드가 실제로 String + 연산에 사용되고 있었습니다.

 

그럼 직접 StringBuilder를 사용하지 않고 String + 연산을 해도 최적화가 되는건가? 라는 생각에 테스트를 진행했습니다.

속도테스트는 100배가량 +연산이 느렸고, 메모리도 낭비되는 것을 확인할 수 있었습니다.

 

그럼 무슨 최적화가 되었다는걸까요? 그 결과를 글로 적고싶어서 이렇게 남깁니다.

 

 

Concat

String의 concat() 메소드를 사용해보셨나요? 앞문자열과 뒷문자열을 합쳐주는 기능을 수행하는 메소드입니다.

StringConcatFactory는 그 concat 메소드처럼 여러 문자열을 하나로 합쳐주는 역할을 수행하는 것이었습니다.

 

String str1 = "1";
String str2 = str1 + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9";

 

JDK5 이전에는 이런 String + 연산을 하면 "12", "123", "1234" ... 이런 데이터가 저장이 되었지만, StringConcatFactory는 이런 연속적인 문자열 합치는 것을 한번에 수행하면서 최적화가 되는 것입니다.

 

   L0
    LINENUMBER 8 L0
    LDC "1"
    ASTORE 1
   L1
    LINENUMBER 9 L1
    ALOAD 1
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "\u000123456789"
    ]
    ASTORE 2

저는 str1 + "2" + "3' ... + "9" 까지 각각 문자열을 더했지만 이를 StringConcatFactory 에서 "23456789" 문자열로 하나로 합쳐 불필요한 메모리 낭비를 최적화 한겁니다. 그러니까 배열에서 하니씩 꺼내며 + 연산할때에는 최적화가 안되었던것이지요.

 

그러니까 String + 연산 은 절대하지말고 StringBuilder, StringBuffer나 쓰십쇼

+ Recent posts