티스토리 뷰
Java의 String 객체 저장 방법
-
사실 주제 자체는 굉장히 추상적입니다. 어떤 Scope 에서 이야기 하느냐에 따라서 많이 달라질 것 같네요
-
본 글에서는 다소 고수준에서 String Literal / String Object를 비교하는 식으로 내부에서 어떻게 처리되는지를 살짝 맛볼 예정입니다.
String 클래스
-
보통 Java 에서 String 객체를 생성할 때 아래와 같이 선언합니다.
String str = "Kim";
String str1 = new String("Kim");
-
String 클래스를 확인해보면 String 클래스는 value 라는 byte(char) 배열을 통해서 문자열을 저장하고 표현합니다.
-
Java8 까지, Java9 부터 배열 타입이 다른 이유는 'Compact String' 이라는 Java9 에서 부터 추가된 기술 때문인데 아래에서 다룰 예정입니다.
-
package java.lang;
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
//~ Java8
private final char[] value;
//Java9 ~
private final byte[] value;
.... 생략
-
JVM 내부 메모리에서는 아래와 같이 저장됩니다. value는 메모리 공간에 할당된 배열을 참조합니다.
-
정확한 메모리 공간은 밑에서 보기로 합니다 ..
-
String Pool
-
위에서 살펴본 String 선언 예제에서
str
,str1
은 메모리에 적재되는 형태가 다릅니다.
//String Literal
String str = "Kim";
//String Object
String str1 = new String("Kim");
String interning
-
먼저 String Literal을 살펴봅시다.
-
Java의 String은 불변성(immutability) 이라는 특성이 있어서 JVM은 String 객체에 대한 메모리 할당을 최적화 할 수 있습니다.
-
메모리 내부에 String pool 이라는 공간을 만들어 두고 고유한 리터럴 문자열을 저장하여 이를 참조하여 사용하도록 하는 방법을 사용해서 말이죠.
-
이 과정을 String Interning 이라고 합니다.
-
-
동일한 문자열을 가지는 String 리터럴을 선언하는 예제를 확인해보겠습니다.
-
기본적으로 String 변수를 생성하고 문자열을 할당 (리터럴) 하게 되면 JVM 내부에서는 문자열 값이 String Pool 에 존재하는지 확인합니다.
-
이때 발견이 된다면 Java 컴파일러는 해당 문자열 리터럴의 주소에 대한 참조를 반환
-
찾지 못한다면 해당 문자열의 리터럴을 Pool에 저장한 후에 참조를 반환
-
String str1 = "Kim";
String str2 = "Kim";
//두 String 객체의 주소(Heap에 할당된)는 동일합니다.
assertThat(str1).isSameAs(str2);
String Object
-
이번엔 String 생성자를 이용해서 String 객체를 생성하는 경우를 살펴봅시다.
-
기본적으로
new
키워드가 등장을 하면 Java 컴파일러는 새로운 객체를 heap 공간에 할당하여 저장합니다. -
그렇기 때문에
new
로 생성된 String 객체들은 각자 Heap 공간상에 적재되게 됩니다.
String str1 = "Kim";
String str2 = new String("Kim");
//두 String 객체의 주소(Heap에 할당된)는 다릅니다.
assertThat(str1).isNotSameAs(str2);
String Literal vs String Object
- 결론적으로 Literal 선언이 재사용을 하기 때문에 new로 생성하는 것 보다는 빠릅니다.
- 다만 큰차이는 나지 않습니다 ..
- 그래서 String에서 제공하는
intern()
이라는 메소드를 통해서 new로 생성된 String 객체를 String pool에 넣을 수는 있습니다.- 다만..
intern()
이 생각보다 비싸서 오히려 부작용이 생길수 있습니다. - 자세한 내용은 아래 블로그를 참고하셔도 좋습니다.
- 다만..
String Pool Grabage Collection
-
Java 버전업이 String Pool 에도 변화가 있었습니다.
-
Java7 이전에는 String Pool 은 Perm 이라는 고정된 사이즈 영역에 할당되어 GC 대상도 아니었으며interning 되는 문자열이 많아지면 OOM이 발생할 여지도 있었습니다.
-
Java7 이후에 String Pool은 Heap 영역에 저장되어 GC 대상이 되었습니다.
-
또한 Java에서는 String Pool에 대한 여러 옵션(사이즈 등)도 제공하여 아래 글을 참고하시면 됩니다.
-
Compact String
-
Java9 부터는 문자열 형식에 따라 char byte 사이의 적절한 인코딩을 선택하여 처리한다고 합니다.
-
이를 Compact String 이라고 합니다.
-
-
이를 통해서 Heap 메모리를 절약하여 GC 성능을 좋게 만들수 있다고 합니다.
//String 클래스 생성자 일부
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
//COMPACT STRING
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
Advanced
-
한가지 흥미로운 점이 생겼습니다.
-
아래 예제를 보고 나서 아래의 그림을 보면 헷갈리기 시작합니다.
-
분명히 String 객체의 주소는 다르지만 String 객체의 value(문자열 배열)의 주소는 같습니다.
-
String str = "Kim";
String str1 = new String("Kim");
System.out.println(System.identityHashCode(str));
System.out.println(System.identityHashCode(str1));
//366712642
//1829164700
System.out.println(System.identityHashCode(str.value));
System.out.println(System.identityHashCode(str1.value));
//1347137144
//1347137144
//* 코드상으로는 확인할 수 없기 때문에 디버거로 확인해봅니다.
-
String Pool을 설명하는 자료입니다.
-
분명 "Cat" (예제의 "Kim")은 다른 객체로 생성이 되어 있지만 위 예제에서는 문자열 배열의 주소가 같습니다.
Detail
-
자세하게 확인해보겠습니다
-
str1
을 new 하는 시점의 String 클래스 생성자는 아래 예제와 같습니다.-
파라미터로 original 이라는 String 객체를 받아서 해당 객체의 값을 동일하게 참조하게 됩니다.
-
-
str
이 할당되고 난후str
의 주소를 출력하고str1
의 생성자 에서 전달받은original
의 주소를 출력해보겠습니다.-
물론 String 클래스를 수정할 수는 없으니 .. Debugger를 활용하여 확인 해봅니다.
-
-
결과를 보시면 주소가 같은 것으로 보아
str
객체가str1
의 생성자에 할당된 것을 확인할 수 있습니다.
public static void main(String[] args) {
String str = "Kim";
System.out.println(System.identityHashCode(str));
//1347137144
String str1 = new String("Kim");
}
//str1을 생성하는 String 생성자
public String(String original) {
System.out.println(System.identityHashCode(original));
//1347137144
this.value = original.value;
this.hash = original.hash;
}
Byte Code
-
더 자세히 확인하기 위해서 바이트 코드를 확인해봅시다.
Constant pool:
#1 = Methodref #9.#26 // java/lang/Object."<init>":()V
#2 = String #27 // Kim
... 생략 ...
#27 = Utf8 Kim
... 생략 ...
{
public com.jinseong.soft.Application();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/jinseong/soft/Application;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String Kim
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #2 // String Kim
9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_1
17: invokestatic #6 // Method java/lang/System.identityHashCode:(Ljava/lang/Object;)I
20: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
23: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_2
27: invokestatic #6 // Method java/lang/System.identityHashCode:(Ljava/lang/Object;)I
30: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
33: return
... 생략 ...
}
-
일단 Constant pool은 상수 풀을 의미합니다. 보게 되면 #2 에서 'Kim' 이라는 값의 String 변수가 할당이 되었습니다.
-
아래의 main 메서드의 0:, 7: 을 보게 되면 #2("Kim") 변수를 스택에 할당하는 것을 확인할 수 있습니다.
-
ldc - 상수를 스택으로 푸시하는 OP Code
-
-
즉, 컴파일 후에 "Kim" 이라는 값의 String 객체가 String Pool에 할당이 된 후 main 메서드의 상수 풀에 할당되고 아래의 과정으로
str
,str1
이 초기화 되는 것입니다.-
str 에는 미리 등록되어 있는 "Kim" 이라는 String 객체를 바로 할당
-
str1에는 new 연산을 통해서 객체를 생성할 때 파라미터로 미리 등록되어 있는 "Kim" 이라는 String 객체를 전달
-
Conclusion
-
결론적으로 String 객체를 문자열 리터럴을 감싸는 래퍼 클래스로 볼 수 있으며 String 객체는 생성 방식에 따라 재사용 될 수도 있고 Heap에 개별 객체가 생성될 수도 있습니다.
-
다만 같은 문자열 값인 경우 원본 문자열(char[] 배열) 객체는 String Pool에 존재하는 객체를 사용(참조)한다는 것입니다.
-
그림으로 한번 정리해보았습니다.
-
':String' 은 Heap에 할당된 인스턴스라는 의미입니다.
-
'Kim' 배열의 경우 명확하진 않지만 String Pool 내에 저장이 되는 것을 표현하기 위해서 String Pool 내부에 그려놓았습니다.
-
참고자료
'프로그래밍 > JAVA' 카테고리의 다른 글
[kotlin/java/spring] 토스 페이먼츠 자동결제 (billing) 연동 방법 - 1. Billing Key 발급 (5) | 2022.10.19 |
---|---|
[Java] String '+'문자열 연결 연산은 내부에서 어떻게 이루어질까? (0) | 2021.02.18 |
[Java] System 환경 변수 가져오는 방법 (0) | 2020.11.09 |
[Java] Application Uncaught Exception 기본 핸들러 설정 방법 (0) | 2020.11.07 |
[Java] String <-> ZonedDate Time 변환 방법 (0) | 2020.11.06 |
- Total
- Today
- Yesterday
- 자전거
- JavaFX Window Close
- 텐트
- windows
- intelij
- 스프링부트
- JavaFX 종료
- 일본여행
- 방통대 과제물
- springboot
- 배낭여행
- effective java
- JavaFX 테이블뷰
- 배낭 여행
- 자바
- 자전거 여행
- 이펙티브 자바
- JavaFX Table View
- JavaFX
- 이펙티브
- git
- java
- 일본 배낭여행
- 일본 여행
- 일본 자전거 여행
- Java UI
- TableView
- 이펙티브자바
- 인텔리제이
- effectivejava
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |