==(동등연산자)
와 equals
에 대해서는 이전 Wrapper Class 글에서도 잠시 다룬 적이 있습니다.
== 연산자
는 내부의 값을 비교하는 것이 아니라 객체의 레퍼런스 주소를 비교하기 때문에, Wrapper 객체의 값 비교로는 사용할 수 없고 equals
로 내부 값을 얻어 비교해야한다는 내용이었습니다.
동일성과 동등성
동일성 : 두 객체가 할당된 메모리 주소가 같으면 동일.==
를 통해 비교 가능
동등성 : 두 객체의 값이 같으면 동등.equals
를 통해 비교 가능
원시 타입은 객체가 아니기때문에 주소가 없으므로,==
연산자를 사용하였을 때 값이 같으면 동일하다고 나옵니다.
더불어 작게 언급했던 내용이, Java는 -128 ~ 127 사이의 값은 상수풀에서 관리하기 때문에, 이 범위 내의 숫자들은 ==
로 비교해도 true 가 나오게 된다는 내용입니다. (미리 정정하자면 상수풀이 아니라 IntegerCache)
Integer i1 = 128;
Integer i2 = 128;
assertTrue(i1==i2); // false
assertTrue(i1.equals(i2)); // true
Integer i3 = 127;
Integer i4 = 127;
assertTrue(i3==i4); // true
비슷한 내용으로 String 에서도 ==(동등연산자)
와 equals
에 관한 내용은 단골 주제였는데요.
아래와 같은 상황은 왜 발생하고, 어떤 이유때문에 이러한 구조가 되었는지 알아보게 되었습니다.
String str1 = "koiil";
String str2 = "koiil";
String str3 = new String("koiil");
String str4 = new String("koiil");
assertTrue(str1==str2); // true
assertTrue(str1==str3); // false
assertTrue(str3==str4); // false
assertTrue(str1.equals(str2)); // true
assertTrue(str1.equals(str3)); // true
assertTrue(str3.equals(str4)); // true
상수풀(Constant Pool)
은 Java의 heap 영역에 존재하는 공간으로, 상수들을 저장하는 용도로 사용됩니다.
상수에는 문자열 리터럴 뿐만 아니라, final로 선언된 원시타입 등도 포함될 수 있습니다.
immutable
: 상수풀은 자바 클래스 파일의 일부분으로 컴파일 시 생성되므로, 런타임 시 상수풀에 저장된 값들은 수정이 불가능해 불변성을 보장합니다.메모리 최적화
: 상수들은 자바 컴파일러에 의해 상수풀에 저장되고, 해당 상수가 필요한 코드에서는 상수풀에서 값을 참조하여 사용합니다.Java 7
까지는 상수풀의 위치가 PermGen 영역에 존재했습니다. Perm 영역은 보통 Class의 Meta 정보나 Method의 Meta 정보, Static 변수와 상수 정보들이 저장되는 공간으로 흔히 메타데이터 저장 영역이라고도 합니다.
하지만 PermGen 영역은 런타임에 변경할 수 없는 고정된 사이즈이기 때문에, intern 메서드를 과도하게 사용하면 저장할 공간이 부족해 OOM(Out Of Memory) 이 발생할 수도 있었습니다.
그래서 Java 8
부터는 PermGen 영역은 완전히 사라지고, 상수풀은 Heap 영역으로, 메타데이터는 MetaSpace라는 새로운 네이티브 영역으로 옮겨지게 되었습니다.
Heap 영역으로 변경된 이후에는 상수풀도 GC의 대상이 되었고, 메타데이터는 Metaspace 영역에서 더 큰 메모리 영역을 가질 수 있게 되었습니다.
//Java 7 HotSpot JVM
<----- Java Heap -----> <--- Native Memory --->
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Permanent | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+
<--------->
Permanent Heap
S0: Survivor 0
S1: Survivor 1
//Java 8 HotSpot JVM
<----- Java Heap -----> <--------- Native Memory --------->
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Metaspace | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+
String Constant Pool
Java에서 String을 생성하는 방법은 객체 생성과 리터럴이 있습니다.
String a = new String("a"); // String Object
String a = "a"; // String Literal
new 연산자를 통해 문자열 객체를 생성하는 경우 메모리의 Heap
영역에 할당되고,
리터럴을 이용해 생성하는 경우에는 heap
중에서도 String Constant Pool
영역에 할당됩니다.
클래스가 JVM에 로드되면 모든 리터럴이 상수풀에 위치하게 되는데, String interning
은 이러한 문자열 리터럴들을 상수풀에 저장하는 것을 의미합니다.
intern
메서드는 상수풀에 해당 문자열이 존재하면 상수풀에서 문자열을 가져오고, 존재하지 않으면 상수풀에 문자열을 추가하고 해당 문자열을 반환합니다.
자바 컴파일 시 단순히 String 리터럴을 가져오기만 하는 것이 아니라, intern 메서드를 수행하여 상수풀에 추가하게 됩니다.
String constantString = "koiil";
String newString = new String("koiil");
assertThat(constantString).isSameAs(newString); // false
String internedString = newString.intern();
assertThat(constantString).isSameAs(internedString); // true
IntegerCache
도입부에서 언급했듯이, Integer 에서 127과 128은 동일값을 == 비교했을 때의 결과가 다릅니다.
마치 상수풀에 저장된 문자열처럼 말이죠.
Integer, Long 등의 타입 또한 메모리 사용을 최적화하기 위해 같은 값을 갖는 객체나 상수를 중복해서 생성하지 않고, 기존 값을 참조하는 방법을 사용하고 있습니다.
int 리터럴을 Integer로 직접 대입하는 것은 auto-boxing 의 예입니다.
리터럴 값이 객체로 변환되는 코드는 컴파일러에 의해 수행되고, 컴파일 시간동안, 컴파일러는 Integer a = 127;
을 Integer a = Integer.valueOf(127);
로 변경합니다.
그리고 Integer.valueOf()
메서드를 살펴보면 IntegerCache
의 존재를 알 수 있습니다.
// Integer.java
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; //캐시 내 값이면 캐시 반환
return new Integer(i); // 외에는 새 객체를 생성
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
...
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
...
}
처음 Integer 이 호출되는 순간, IntegerCahe 는 -128 ~ 127 사이의 값을 미리 생성해 캐시 배열에 저장해둡니다.
그리고 이후 범위 내의 값을 호출할 시, 캐시된 값을 돌려줍니다.
이러한 캐싱은 Integer 만 있는 것이 아니라 ByteCache
, ShortCache
, LongCache
, CharacterCache
도 각각 존재합니다.
Flyweight 패턴
이러한 디자인 패턴을 플라이웨이트(Flyweight) 패턴이라고 합니다.
플라이웨이트 패턴(Flyweight Pattern)은 객체 지향 디자인 패턴 중 하나로, 많은 수의 유사한 객체를 생성할 때 발생하는 메모리 사용량을 최소화하고 성능 저하 문제를 해결하기 위한 패턴입니다.
플라이웨이트 패턴은 객체를 공유 객체(Shared Object)와 비공유 객체(Unshared Object)로 분류하는데, 비공유 객체를 공유 객체로 대체해 객체 생성 횟수를 줄이고 메모리 사용량을 최적화합니다.
이 패턴은 객체의 내부 상태와 외부 상태를 분리해서, 내부 상태를 공유 객체로 관리하고, 외부 상태를 비공유 객체로 관리합니다.
즉, 객체의 공통적인 내부 상태를 공유해서 메모리 사용량을 줄이고, 고유한 외부 상태를 비공유 객체로 처리해서 다양한 변화에 대응할 수 있도록 합니다.
Ref.
Guide to Java String Pool
[Java] Integer.valueOf(127) == Integer.valueOf(127) 는 참일까요?
JDK 8에서 Perm 영역은 왜 삭제됐을까
인터닝(Interning)이란 무엇인가?
동등비교와 정렬