들어가기 앞서
String을 보다 자세히 이해하기 위해서는 JVM 구조를 알고 있는 것이 좋다. JVM에 대한 자세한 내용을 알고 싶다면 https://jaystorage.tistory.com/8 을 참고하자
String 이란
Java에서 String은 문자열을 나타내는 객체이다. C/C++에서는 문자열이 단순히 char의 배열인데 반해, Java에서의 문자열은 java.lang안에 있는 String 클래스의 객체를 의미한다.
그렇기 때문에 String 은 int 와 char 와 같은 기본형(primitive type)이 아닌 참조형(reference type) 변수로 분류된다. (String의 첫 글자가 소문자가 아닌 대문자로 시작하는 이유가 이 때문이다.)
Java의 String 은 불변(immutable)하다
Java에서는 String의 값을 변경할 수 없다.
String str1 = "aaa";
str1 = str1 + "bbb";
System.out.println(str1); // aaabbb
일반적으로 int 변수를 선언한 뒤 그 값을 바꾸면 기존 값을 변경된 값으로 덮어쓴다. 위 코드에서 보면 str1이라는 문자열도 동일한 방식으로 변경되는 것처럼 보이지만 실제로는 새로운 String 객체가 생성되는 것이다. 이러한 String 객체가 불변으로 설계된 이유는 다음과 같다.
- 캐싱
- Java에서 String은 String Constant Pool이라는 영역에 따로 저장되고 여러 객체 또는 변수에서 공유하여 사용할 수 있다.
- 계속해서 같은 String을 만들지 않고 이미 만들어진 String (캐싱된 String)을 사용하면 그만큼 성능적 이득을 취할 수 있다. - 동기화
- 불변한 객체는 멀티 스레드(multi-thread)환경에서 동기화 문제가 발생하지 않기 때문에 안전하다. - 보안
- 대부분의 프로그램에서 DB 커넥션 등 여러 상황에서 계정정보(아이디, 비밀번호)를 통해 인증을 수행한다. 그리고 이 계정정보는 주로 String으로 이루어져있다.
- String이 mutable(변화가능)하다면 해커의 공격으로 String값이 바뀔 수 있는 여지를 줄 수 있다.
String의 생성방식과 String이 저장되는 공간
Java에서 String을 생성하는 방법은 다음과 같이 리터럴을 이용한 방식과 new 연산자를 통한 방식이 있다.
String str1 = "Hello"; // 문자열 리터럴을 이용한 방식
String str2 = "Hello";
String str3 = new String("Hello"); // new 연산자를 이용한 방식
String str4 = new String("Hello");
그리고 위의 String을 서로 비교한 결과는 다음과 같다.
// 리터럴 문자열 비교
System.out.println(str1 == str2); // true
// 객체 문자열 비교
System.out.println(str3 == str4); // false
System.out.println(str3.equals(str4)); // true
// 리터럴과 객체 문자열 비교
System.out.println(str1 == str3); // false
System.out.println(str3.equals(str1)); // true
그런데 결과를 보면 조금 이상한 점이 있다. 분명 String은 primitive type이 아니기 때문에 == 을 사용해서 값을 비교하는 것이 불가능 할 것 같은데 리터럴로 선언된 str1과 str2를 비교한 결과는 true를 반환하고 있다.
이러한 결과가 나오는 이유는 String이 저장될 때 JVM의 특별한 곳에 저장되기 때문이다.
String Constant Pool
위에서 살펴봤듯이 String은 선언하는 방식에 따라 메모리에 적재되는 방식이 다르다.
위 예제의 str3이나 str4처럼 new를 통해 String을 생성하면 이 값은 JVM의 Heap 영역에 존재하게 된다. 반면 str1이나 str2처럼 리터럴을 사용해 String을 생성하면 이 값은 Heap 영역 내부에 있는 String Constant Pool이라는 영역에 저장된다.
위 코드를 그림으로 표현하면 다음과 같다.
이 그림을 보면 str1 == str2 의 결과가 왜 true인지 알 수 있다. 리터럴 값으로 할당한 두 변수 str1, str2 가 같은 메모리 주소를 가리키고 있기 때문이다.
이러한 String Constant Pool이 Heap 영역 내에서 따로 존재하는 이유는 String이 그만큼 많이 사용되기 때문이다. 프로그램 실행 중 동일한 String이 여러 번 사용될 때 String Constant Pool에 단 하나의 String 인스턴스만 생성한 뒤 공유하게 만들면 메모리 사용을 최소화 할 수 있다.
Java 버전 별 String Constant Pool
String Constant Pool이 메모리상에서 차지하는 영역은 Java의 버전에 따라 다르다.
- Java 6
String Constant Pool이 Heap 영역대신 PermGen(Permenent Generation) 영역에 존재했다. PermGen은 JVM에서 메소드 영역에 있기 때문에 GC가 일어나지 않았고, 사이즈가 32MB~96MB로 고정되어 있었다. 따라서 당연하게도 너무 다양한 String을 사용하게 된다면 OOM이 발생하고는 했다. - Java 7
이전 버전에서 가지고 있던 여러가지 이유 때문에 String Constant Pool은 Java 7부터 PermGen 영역에서 Heap 영역으로 위치를 옮겼다. Heap 사이즈에 따라 String Pool도 영향을 받기에 동적으로 메모리크기가 할당될 수 있게 되었다. PermGen 영역은 여전히 고정 메모리크기로 할당되었다. - Java 8
이때부터 고정크기로 많은 문제를 일으킨 PermGen은 Metaspace로 이름을 변경하고 고정크기에서 동적크기로 메모리 구성이 변경되었다.
'Java' 카테고리의 다른 글
String / StringBuilder / StringBuffer (0) | 2024.05.23 |
---|---|
BigDecimal (0) | 2024.05.20 |
상수(Constant)와 리터럴(Literal) (0) | 2024.03.17 |
GC(Garbage Collection, Garbage Collector) (1) | 2023.10.29 |
JVM 구조 (0) | 2023.10.22 |