본문 바로가기

자바 String 클래스에 대해서

이번 주제에서 다룰 핵심내용을 정리해보면 아래와 같다.

  1. String은 내부적으로 char[]배열을 사용해서 문자열을 저장한다.
  2. new 연산자로 생성하면 String constant pool에 저장하지 않고 에 그대로 저장
  3. String은 불변클래스이므로 매번 새로운 인스턴스를 생성하는것이다.

 

String클래스에 대한 간단한 소개

  • String클래스는 불변객체이다. String클래스 내부에는 final로 선언된 char [] val 이 들어있다.
  • String클래스를 통해 문자열을 저장하면 내부에서는 char [] 배열을 이용해서 문자열 정보를 저장하는것이다.
  • 만약 ,"ABC"라는 문자열을 String클래스로 저장을 한다면 value라는 char 배열에 한문자씩 들어가는것이다.

 

String클래스 문자열 생성하는 방법

String클래스를 위에서 간단하게 알아봤으니 이제 String을 이용해서 문자열을 생성하는 방법을 알아보자. 문자열의경우 new로 생성하는 방법 말고도 ""(리터럴) 로 생성하는 방법도 있다.

 

  • 리터럴로 생성하면 constant pool에 올라가서 같은 문자열이면 같은 주소를 참조한다.
  • new키워드로 생성하면 constant pool에 올라가지 않고 매번 힙에 생성된다.

 

new키워드로 문자열을 생성하면 아래 그림과 같이 작업이 수행되면 메모리를 많이 사용한다. 한번 생성한 문자열을 수정한다는것은 결국 새로운 메모리 공간에 기존에 저장된 문자열과 새로운 문자열을 합쳐서 완전 새로운 문자열을 만들어내는 방식이다.

리터럴로 생성을 하면 문자열을 생성할때 내부적으로 intern을 호출하여 상수풀에 등록된 문자열의 레퍼런스를 확인한다고 한다.(intern메서드는 String이 상수풀에 등록된 경우 해당 문자열의 주소값을 반환하는 역할을 한다.)

String a = "ABC"; // 상수풀에 ABC라는 값이 저장
String b = "ABC"; // 상수풀에 ABC라는 값이 있으므로 같은 레퍼런스를 가진다.

이때, 주의해야할 부분이 있다. String의 메서드중에서 .substring()은 interning과정이 없기때문에 동등비교를 하더라도 같은 값이 나오지 않는다. 리터럴로 문자를 생성했다고 하더라도 문자열의 경우 equals를 이용해서 비교를 하자.

 

 

왜 String클래스를 이렇게 설계했을까?

불변하게 만든이유를 스레드 세이프 때문일것이다. 리터럴로 생성하면 constant pool에서 같은 문자열을 공유하고 있다. 하지만 이를 수정하게 허용한다면? 동시성 문제를 피하기 힘들다...! 이를 위해서 수정을하는게 아니라 완전히 다른 문자열을 만드는 방법을 채택한것이다.

 

 

Constant Pool

위에서 계속 등장한 constant pool은 자마8 이전에는 perm gen영역에 있었다. 하지만 out of memory문제를 줄이기 위해서 java8 부터는 perm 영역을 meta space로 이동시켰다. meta space는 힙의 native memory영역인데, jvm구동중에서 운영체제에 의해서 메모리공간이 자동으로 스케쥴링되므로 out of memory로부터 자율로워진다.

String의 +연산

 

String클래스에 대해서 간단히 확인했는데, 여기까지만 알아도 String에서 + 연산을 이용해서 문자열을 붙여나가면 안된다는것을 알 수 있다. (매번 새로운 String인스턴스를 새롭게 생성한다)

 

Java에서는 이런 문제를 인지하고 컴파일시키면 자동으로 StringBuilder로 변경시켜주고 있다. 하지만, + 연산을 수행할때마다 StringBuilder를 새롭게 생성하기 때문에 개발자가 코드로 직접 StringBuilder를 한번만 생성하고 append로 문자열을 붙여나가는게 더 이득이다.

 

jdk 5.0이상을 사용한다면 + 연산을 jvm이 자동으로 최적화 해주긴 한다.

String s = "";       
s+= "concat";

컴파일시 아래와 같은 코드로 변한다.

StringBuilder builder = new StringBuilder();
builder.append("");
builder.append("concat");
String s = builder.toString();

하지만, for문 안에서 String을 합칠경우에는 매번 StringBuilder를 생성하기때문에 장점이 없어 보이지만, loop문 밖에서는 나름 효율이 있어 보인다.


String str2 = "String" + str0 + "000";        
for (int i=0;i<10;i++){  
   str2 = str2 + "1111";  
   str2 += "1111";        
}

String str2 = (new StringBuilder("String")).append(str0).append("000").toString();  
for(int i = 0; i < 10; i++) {  
   str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString();  
   str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString();  
}

 

 

현재 pc에 zulu open jdk 11버전이 설치되어있다. 완전 최신버전은 아니지만 비교적 최신버전인 java 11에서도 비슷한 방식으로 최적화를 할까?

 

 

아래와 같이 코드를 작성하고 디컴파일을 하면 StringBuilder가 아닌 처음보는 함수가 등장한다.바이트코드를 변경하지 않고도 동적으로 Concatation전략을 변경할수 있도록 java9부터 최적화를 시켰다. 자세한 글은 아래 링크에 설명이 되어있다.

 

즉 내부에서 StringBuilder를 통해서 concat을 하도록 최적화하긴 하지만 동적으로 String concat전략을 변경할 수 있도록 한것이다.

 


String a = "Hello";
String b= "World!";
String c = a + b;
String var2 = "Hello";
String var3 = "World!";
var2.makeConcatWithConstants<invokedynamic>(var2, var3);

 

 

StringBuilder, StringBuffer

StringBuilder,StringBuffer모두 같은 기능을한다. String의 문제점을 보완해주는 역할을 하는데 왜 2개를 구분한것일까? String의 경우 불변객체이므로 스레드세이프하다. 하지만 문자열을 수정하게되면 멀티스레드 환경에서 동시성 문제를 피할수 없다. 이를위해 StringBuffer는 내부에서 synchronized를 통해서 스레드세이프를 보장한다. 반면, StringBuilder는 스레드세이프 하지 않지만 속도가 더 빠른 장점이 있다.

스레드 세이프가 확실하지 않다면 StringBuilder가 이득이지만, 웹 애플리케이션 환경이 대부분 멀티스레드 환경이므로 StringBuffer를 사용하는것이 좋을것 같다.

 

 

String,StringBuilder,StringBuffer 성능비교

 

 

위에서 이론을 통해서 String보다는 StrinfBuffer가 좋다는것을 알았고, StringBuffer보다는 StringBuilder가 좋다는것을 확인했는데, 이를 성능관점에서 비교해보자.

long t = System.currentTimeMillis();
String s = "";
for (int i = 0 ; i < 40000; i++){
   s += "test string";
}
System.out.println("String : "+(System.currentTimeMillis() - t));

long t0 = System.currentTimeMillis();
StringBuffer buf = new StringBuffer();
for (int i = 0 ; i < 40000; i++){
    buf.append("test string");
}
System.out.println("Buffers : "+(System.currentTimeMillis() - t0));

t0 = System.currentTimeMillis();
StringBuilder building = new StringBuilder();
for (int i = 0 ; i < 40000; i++){
    building.append("test string");
}
System.out.println("Builder : "+(System.currentTimeMillis() - t0));

총 4만번을 돌렸는데 string의경우 967ms가 나머지는 2ms인것을 확인할 수 있다.

 

 

 

 

2억번 돌리면 300ms차이가 난다. 성능 차이가 많이 나는편은 아니기때문에 스레드세이프한 StringBuffer를 쓰는게 좋을것 같다.

t0 = System.currentTimeMillis();
StringBuilder building = new StringBuilder();
for (int i = 0 ; i < 40000; i++){
    building.append("test string");
}
System.out.println("Builder : "+(System.currentTimeMillis() - t0)); // 993
long t0 = System.currentTimeMillis();
StringBuffer buf = new StringBuffer();
for (long i = 0 ; i < 20000000; i++){
    buf.append("test string");
}
System.out.println("Buffers : "+(System.currentTimeMillis() - t0)); //1239

'Java' 카테고리의 다른 글

자바 객체 생성되는 과정에 대해서  (0) 2021.09.02
Java 배열에 대해서  (0) 2021.08.27
Thread Dump로 DeadLock 확인하기  (0) 2021.06.12
자바 - enum  (0) 2020.09.01
Executor 인터페이스  (0) 2020.08.22

댓글