String
String 클래스는 String Constant Pool에 저장되고, 불변객체로 변경되지 않는 특징을 가지고 있습니다.
이렇게 사용하면, 문자열을 Constant화 하여 다른 변수 혹은 객체들과 공유하게 되는데 이 과정에서 데이터 캐싱이 일어나고 그 만큼 성능적 이득을 취할 수 있기 때문입니다.
두번째는 데이터가 불변(immutable) 하다면 Multi-Thread 환경에서 동기화 문제가 발생하지 않기 때문에 더욱 안전한 결과를 낼 수 있기 때문입니다.
세번째는 보안(Security) 측면으로, 데이터베이스 사용자 이름, 암호는 데이터베이스 연결을 수신하기 위해 문자열로 전달되는데, 만일 번지수의 문자열 값이 변경이 가능하다면 해커가 참조 값을 변경하여 애플리케이션에 보안 문제를 일으킬 수 있습니다.
String 객체를 생성하는 방법은 2가지 입니다.
1. 리터럴를 이용하는 방식
2. new 로 새로 생성하는 방식
더 자세하게는 아래에서 다루겠습니다.
Intern이란?
위 설명과 같이 String 객체는 불변 객체이기 때문에 동일한 객체가 공유될 수 있는 특징을 가집니다.
이 특징을 활용하기 위해 동일한 문자열을 가지는 String 객체가 단 하나만 존재하도록 유지할 필요가 있습니다.
JVM 내부에는 초기에는 비어있는 문자열 객체를 관리하는 풀(pool)을 생성합니다.
이후, String 객체의 intern 메서드가 호출되면 이 풀에 해당 문자열과 같은 String 객체가 존재하는 경우 해당 객체를 반환하고, 존재하지 않는 경우 객체에 풀을 추가하고 해당 객체를 반환하게 됩니다.
또한, String 함수의 값을 호출하면 해당 객체를 직접 수정하는 것이 아니라, 함수의 결과로 해당 객체가 아닌 다른 객체를 반환합니다.
리터럴로 String을 생성하는 경우 내부에서 intern을 수행합니다.
intern 메서드의 동작 원리
public native String intern();
intern 메서드는 위와 같이 정의 되어있습니다.
새로운 String 값이 들어오면 그 값을 intern을 통해 String pool로 넣어 반환 값을 얻을 수 있습니다.
따라서 아래 예제를 통해 이해할 수 있습니다.
String a = "A";
String pple = "pple";
String apple = a+pple; // 이것은 heap 영역에 저장됩니다.
String r_apple = "Apple"; // 이것은 String constant pool에 저장됩니다.
System.out.println((a+pple) == apple); // false
System.out.println(apple == r_apple); // false
System.out.println((a+pple) == apple); // false
System.out.println((a+pple).intern() == apple); // false;
System.out.println((a+pple).intern() == r_apple); // true;
System.out.println(apple.intern() == r_apple); // true;
jdk9/jdk/src/java.base/share/native/libjava/String.c
위 github에서 확인해보면
#include "jvm.h"
#include "java_lang_String.h"
JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
JNIEXPORT jboolean JNICALL
Java_java_lang_StringUTF16_isBigEndian(JNIEnv *env, jclass cls)
{
unsigned int endianTest = 0xff000000;
if (((char*)(&endianTest))[0] != 0) {
return JNI_TRUE;
} else {
return JNI_FALSE;
}
}
jvm.h 파일을 include하고 있으며
JNICALL을 통해 JVM_InternString을 수행하고 있습니다.
따라서 jvm.h를 확인해보면
https://github.com/AdoptOpenJDK/openjdk-jdk9/blob/master/hotspot/src/share/vm/prims/jvm.cpp
jdk9/hotspot/src/share/vm/prins/jvm.cpp
// 3549
// String support ///////////////////////////////////////////////////////////////////////////
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollector oam;
if (str == NULL) return NULL;
oop string = JNIHandles::resolve_non_null(str);
oop result = StringTable::intern(string, CHECK_NULL);
return (jstring) JNIHandles::make_local(env, result);
JVM_END
oop result = StringTable::intern(string, CHECK_NULL);이 있습니다.
CHECK_NULL은 NullPointerException을 발생하는 매크로이고,
jdk9/hotspot/src/share/vm/classfile/stringTable.cpp
StringTable은 아래와 같이 동작하는 것을 알수 있습니다.
oop StringTable::intern(oop string, TRAPS)
{
if (string == NULL) return NULL;
ResourceMark rm(THREAD);
int length;
Handle h_string (THREAD, string);
jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL);
oop result = intern(h_string, chars, length, CHECK_NULL);
return result;
}
oop StringTable::intern(const char* utf8_string, TRAPS) {
if (utf8_string == NULL) return NULL;
ResourceMark rm(THREAD);
int length = UTF8::unicode_length(utf8_string);
jchar* chars = NEW_RESOURCE_ARRAY(jchar, length);
UTF8::convert_to_unicode(utf8_string, chars, length);
Handle string;
oop result = intern(string, chars, length, CHECK_NULL);
return result;
}
native는 Java 프로그램에서 다른 언어(C, C++ 등)으로 작성된 코드를 실행할 수 있는 JNI(Java Native Interface)입니다.
JNI에서 동작하는 원리는 StringTable 클래스에서 RehashableHashTable을 상속받는 클래스입니다.
해시 테이블의 균형이 맞지 않을 때, 해시 알고리즘을 변경해서 데이터가 테이블 전체에 고루 퍼지도록 할 수 있는 해시 테이블 입니다.
또한 _the_table을 정적 필드로 선언하고, the_table()함수로 접근해 StringTable객체를 싱글턴으로 사용하게 만듭니다.
String 저장되는 방식이란?
그럼 String으로 만든 값들은 도대체 어떻게 저장되는 것인가?
String.valueOf나 리터럴의 경우 플라이웨이트 패턴으로 구현되어 있습니다.
- 동일하거나 유사한 객체들 사이에, 가능한 많은 데이터를 서로 공유하여, 메모리의 사용량을 최소화하는 소프트 웨어 디자인 패턴입니다.
- 따라서 상수 풀에 있으면 쓰고, 없으면 생성한다고 볼수 있습니다.
- 이를 통해 많은 객체 생성시 메모리를 줄이고, 성능을 향상합니다.
new를 통해 String을 만든다면, 이는 힙영역에 생성됩니다.
따라서 new를 통해 객체를 만든 것은 서로 다른 hashcode값을 가지게 됩니다.
String 값은 GC의 대상인가?
Java 7 이후부터 GC의 대상이 되었습니다.
Java 7 이전에는 메모리 영역에서 PermGen 공간에 String pool이 배치되었습니다. Perm 영역은 메모리 확장도 되지 않고, GC의 대상도 아니었습니다.
하지만 Java 7이후 PermGen 공간이 없어지면서, String pool은 Heap영역으로 옮겨지게 되었고, GC의 대상이 되었습니다.
JVM 옵션으로 '-XX:StringTableSize=N'을 통해 크기를 설정할 수 있습니다.
그럼 intern은 어디에 쓸까?
equals()를 사용하면 그 값에 대해서 비교를 하는데 그럼 모든 값에서 비교를 할필요가 있습니다.
하지만 intern()은 String constant pool에서 비교하기때문에 다소 빠르다는 장점이 있습니다.
또한 개념을 알고 사용하는 것과 모르고 사용하는 차이가 있기에 결과를 제대로 분석할 수 있다는 점도 있습니다 ㅎㅎ!
참고 : https://gyoogle.dev/blog/computer-language/Java/Interend%20String%20in%20Java.html
참고 : https://blog.naver.com/adamdoha/222817943149
참고 : https://www.latera.kr/blog/2019-02-09-java-string-intern/
'기술 스텍 > Java' 카테고리의 다른 글
[Java] Reflection이란? (4) | 2024.04.13 |
---|---|
[Java] 컴포지션이란? (60) | 2024.04.12 |
[Java] Record란? (0) | 2024.04.05 |
[Java] 프로세스 vs 스레드? (0) | 2024.02.21 |
자바 버전에 따른 차이 (0) | 2024.02.19 |