결론
의문
primitve type은 Constant Pool에 있는 특정 상수를 참조하는 것이다.
그렇기에 int a = 1, int b = 1 인 경우 같은 주소를 참조하기 때문에 a==b 가 성립된다.
라는 정보를 보고 정말 그럴까? 값을 저장하는 것이 아닌가? 라는 의문이 생겨 해당 실험을 진행하게 되었습니다.
int num = 1;
위의 코드는 정수형 변수 num
에 1
이라는 값을 저장하는 코드입니다.
코린이인 저도 의미를 바로 알 수 있는 코드입니다.
그런데 1이라는 숫자는 어디서 왔고, num
은 어떻게 값을 가지게 되는 걸까요?
지금까지는 스택 프레임에 4바이트의 공간이 저장 → Runtime Constant Pool에 리터럴 1이 저장 → 상수 풀 1의 값을 복사하여 스택 프레임에 할당된 num 공간에 저장이라고 알고 있었는데요. 실험을 통해 확실하게 알아봅시다.(확실하지 못했읍니다..)
실험
정수형 변수는 어떻게 값이 저장되는가
int num = 1;
먼저 위의 코드가 실행되면 javac
에 의해 컴파일 되어 자바 바이트코드(인터프리터 언어)로 변환되고, 이후 클래스로더에 의해 바이트코드를 실행시점(RunTime)에 읽어들여서 메모리(Runtime Data Area)에 적절하게 배치됩니다.
그렇다면 위의 바이트코드를 javap를 사용하여 실제로 어떻게 동작되는지를 확인해봅시다.
0: iconst_1
→ 상수 1을 오퍼랜드 스택에 push
1: istore_1
→ pop하여 지역 변수 배열 1번 인덱스에 저장 (int num = 1)
2: return
istore은 오퍼랜드의 값을 pop하여 로컬 변수의 값을 설정하는 명령어입니다.
1 이라는 값은 오퍼랜드 스택에 저장(push)되었다가, 꺼내어져(pop) 변수 num에 저장이 되는 것이었습니다.
어라..? 상수풀의 참조 없이 num변수에 1이 저장이 되네요.
찾아보니 JVM에서 int
값은 4가지 범주로 나누어 명령어가 실행된다고 합니다.
명령어 | 실행 상수 값 범위 | description |
iconst | -1 ~ 5 | Push int constant |
bipush | -128 ~ 127 | Push byte |
sipush | -32768 ~ 32767 | Push short |
ldc | -2147483648 ~ 2147483647 (상수풀 참조) |
Push item from run-time constant pool |
명령어는 아래 링크에 장 정리되어 있습니다.
iconst 는 int 값을 스택에 로드한다고 합니다. (-1 ~ 5 범위의 상수 로드 가능)
bipush 와 sipush 는 각각 byte 와 short 를 스택에 정수 값으로 push 한다고 되어있는데 각각
bipush는 1byte로 표현 가능한 integer value를
sipush는 2byte로 표현 가능한 integer value를 오퍼랜드 스택에 push합니다.
Note that any referenced "value" refers to a 32-bit int as per the Java instruction set.
여기서 value란 32비트 int 라고 합니다.
즉, -32768 ~ 32767 사이의 값은 상수풀의 참조 없이 오퍼랜드 스택에 push되었다가 변수에 저장(pop)된다는 뜻인데, 한번 확인 해보겠습니다.
상수 풀 참조는 언제 할까
public static void main(String[] args) {
int a = 1;
int b = 100;
int c = 10000;
int d = 100000000;
}
이 소스코드의 바이트코드를 확인해봅시다.
0: iconst_1
→ 상수 1을 오퍼랜드 스택에 push
1: istore_1
→ pop하여 지역 변수 배열 1번 인덱스에 저장 (int a = 1)
2: bipush 100
→ 상수 100을 오퍼랜드 스택에 push
4: istore_2
→ pop하여 지역 변수 배열 2번 인덱스에 저장 (int b = 100)
5: sipush 10000
→ 상수 10000을 오퍼랜드 스택에 push
8: istore_3
→ pop하여 지역 변수 배열 3번 인덱스에 저장 (int c = 10000)
9: ldc #7
→ 상수풀에 값을 push하고 인덱스 7을 참조하여 오퍼랜드 스택에 push
11: istore 4
→ pop하여 지역 변수 배열 4번 인덱스에 저장 (int d = 1000000000)
d변수에 1000000000 이라는 값을 저장하려 하니 드디어 상수풀을 참조해서 값을 넣습니다.
그렇다면 상수풀을 참조할 때, 값을 가져올까요? 주소값을 가져올까요?
상수 풀 참조 시 값을 가져오는 것인가
Push item from run-time constant pool
Oracle 공식문서에서 ldc 명령어는 상수 풀로부터 아이템을 오퍼랜드 스택에 push하는 명령어라고 설명합니다.
조금 설명이 부족하네요.
push a constant #index from a constant pool (String, int, float, Class, java.lang.invoke.MethodType, java.lang.invoke.MethodHandle, or a dynamically-computed constant) onto the stack
위키피디아에서는 상수풀의 #index 를 참조하여 java.lang.invoke.MethodHandle 또는 동적으로 계산된 상수를 스택에 push한다고 설명되어 있네요.
뭔가 설명이 다 애매한 느낌,,
public class Test {
public static void main(String[] args) {
int num = 1000000000;
}
}
위 자바 코드의 바이트코드를 분석해봅시다.
Constant pool의 #7 번지에 1000000000 값을 참조하는 주소값이 저장되어 있습니다.
Constant pool의 #7 번지에서 값을 오퍼랜드 스택으로 push하고 이를 istore 명령어로 저장합니다.
그렇다면 정말 -32768 ~ 32767 사이의 값은 상수풀에 저장되지 않는 것일까요? 그리고 상수풀의 값은 저장되었다가 재사용될까요?
상수 풀의 값은 재사용 되는가? 범위 외의 값은 상수풀을 참조하지 않는가
public class Test {
public static void main(String[] args) {
int a = 1;
int b = 100;
int c = 10000;
int num = 1000000000;
int num2 = 1000000000;
}
}
위 자바 코드의 바이트 코드를 분석해봅시다.
예상대로라면 상수 1, 100, 10000 은 상수풀에 존재하지 않아야 하고, 상수 1000000000 은 상수풀에 한번만 입력 되어 있어야 합니다.
상수 풀에 상수 1000000000 은 한 번만 입력되어 있고, num 과 num2 는 같은 상수 풀의 번지 수를 참조 하여 값이 오퍼랜드에 push 되고 저장되는 것을 확인할 수 있습니다.
+) Integer 클래스로 레퍼런스 변수를 생성해도 위와 같이 이미 생성된 상수풀을 참조하여 값이 저장됩니다.
oracle 공식 문서를 참고하여 상수풀이 어떻게 구성되어 있는지 확인해봅시다.
상수풀은 리터럴 상수 값을 저장하는 곳입니다. String 뿐만 아니라 모든 종류의 숫자, 문자열, 식별자 이름, Class 및 Method에 대한 참조와 같은 값이 포함됩니다.
상수풀은 해당 항목이 나타내는 상수의 종류를 저장하는 1byte
크기의 변수 tag
와 tag
에 따라 달라지는 info
변수가 존재합니다. (u1 은 unsigned 8bit 를 의미합니다, u2 는 unsigned 16bit)
cp_info {
u1 tag;
u1 info[];
}
Integer 형식의 상수풀은 4바이트 상수를 나타낸다고 하네요.
The CONSTANT_Integer_info and CONSTANT_Float_info structures represent 4-byte numeric (int and float) constants:
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
정리해보면 -32768 ~ 32767
범위 이하의 정수 값은 상수풀의 참조 없이 스택 프레임(Stack Frame)에 존재하는 오퍼랜드 스택(Operand Stack)에 값이 push
되었다가 pull
되어 변수에 직접 값이 저장된다는 것.
-32768 ~ 32767
범위를 초과하는 정수 값은 먼저 상수풀에 push
되고, 해당 값을 참조하여 오퍼랜드 스택에 push
. 된 이후 pull
되어 변수에 값이 저장됩니다.
비교 실험
int a = 1;
int b = 1;
// a == b ? true
당연한 결과입니다. 오퍼랜드 스택에 의해 두 변수 모두 1의 값을 받았기 때문입니다.
int a = 1;
Integer b = Integer.valueOf(1);
// a == b ? true
Integer 클래스의 valueOf 메소드를 살펴봅시다.
Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
파라매터로 전달된 정수형 i 를 가리키는 인스턴스 Integer를 반환하는 함수입니다.
i 값이 -128 ~ 127 사이라면 IntegerCache.cache
배열에서 관리되고 있는 오브젝트를 반환하고, 범위를 초과하는 경우에는 new Integer(i)
새로운 Integer 인스턴스를 생성하여 반환합니다.
그런데 왜 true가 출력되느냐, ‘==’ 연산자는 기본적으로 primitive type에 대해서는 값을 비교하고 reference type에 대해서는 주소값을 비교하는데, 피연사중 하나가 primitive type인 경우에는 값을 비교하게 됩니다.
그렇다면 a
의 값 1
과 Integer
타입의 b
가 가리키는 오브젝트의 값인 1
이 비교되어 true.
그렇다면 -128 ~ 127
을 초과하는 정수를 Integer
타입에 저장하고 비교한다면?
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println( a==b );
// a == b ? true
Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println( c==d );
// c == d ? false
-128 ~ 127
범위의 정수는 IntegerCache
의 동일한 오브젝트의 주소를 가리키게 됩니다.
하지만 -128 ~ 127
범위 외의 정수는 새로운 Integer 객체를 생성하여 반환하기 때문에 다른 주소값을 가지게 되어 false라는 값이 도출됩니다.
객체의 고유한 해시코드를 통하여 확인해봅시다.
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(System.identityHashCode(a)); // 2003749087
System.out.println(System.identityHashCode(b)); // 2003749087
a = Integer.valueOf(128);
b = Integer.valueOf(128);
System.out.println(System.identityHashCode(a)); // 1324119927
System.out.println(System.identityHashCode(b)); // 990368553
-128 ~ 127
범위의 정수는 같은 객체의 해시코드를, 범위 외의 정수는 다른 해시코드를 가지는 것을 확인할 수 있습니다.
+) 마지막 실험
상수 풀에 저장된 상수 1000000000을 참조하여 생성된 Integer 인스턴스 변수 a, b 를 비교해봅시다.
Integer num1 = Integer.valueOf(1000000000);
Integer num2 = Integer.valueOf(1000000000);
System.out.println(num1==num2); // false
System.out.println(num1.equals(num2)); // true
0: 상수풀 #7 을 참조하여 push
2: 상수풀 #8 을 참조하여 Integer.valueOf:(I)Ljava/lang/Integer 메소드 수행
5: num1에 주소값 저장 (astore 은 레퍼런스 값을 변수에 저장하는 명령어)
6: 상수풀 #7 을 참조하여 push
8: 상수풀 #8 을 참조하여 Integer.valueOf:(I)Ljava/lang/Integer 메소드 수행
11: num2에 주소값 저장
상수풀에 존재하는 같은 메소드와 같은 상수를 참조하여 생성된 레퍼런스 변수지만
스택 프레임에서 선언된 변수가 서로 다른 힙 메모리의 주소 값을 저장하고 있기 때문에 다른 인스턴스임을 확인할 수 있습니다.
System.out.println(System.identityHashCode(num1)); // 2003749087
System.out.println(System.identityHashCode(num2)); // 1324119927
객체 고유 해시코드도 다릅니다.
결론
- +) 2023. 03. 30
강사님께서 굳이 따지고 보면 주소 값을 가진다고 하시는데
jvm 공식 문서에서는 constant pool의 Integer 값을 unsigned 32bit 크기의 메모리에 빅 엔디안 방식으로 저장하고,
오퍼랜드 스택 관련 공식 문서에서도 상수 값을 직접 저장할 때 혹은 constant pool의 Integer 값을 참조할 때 reference 가 아닌 1, 2, byte, short 혹은 value로 표현하는 것을 보니(생각해보니 4byte 짜리 2진수를 push한다는 말은 찾아볼 수 없었다),
엄밀히 따지자면 주소값을 가지지만 형식상 값을 가진다 라고 생각하는 게 맞다고 판단되었습니다.
primitive Type
정수형 변수는 ‘값’ 가지는 게 맞다.- C/C++은 primitive type이 값을 가지는 것이 맞다고 하셨습니다.
-32768 ~ 32767
범위의 정수는 상수 풀을 참조하지 않고 값을 받는다.-32768 ~ 32767
범위 외의 정수는 상수 풀을 참조한 오퍼랜드의 값을 받는다.- 상수 풀의 값은 재사용 된다.
Integer
타입의-128 ~ 127
값은 동일한 오브젝트를 가리킨다. (new Integer
제외)
+) 2023.03.30
강사님께서 primitive type은 엄밀히 따지면 C/C++ 에서는 값을 가지고, java는 주소 값을 가진다고 하셨습니다.
자바는 jvm위에서 작동하기 때문에 primitive type이 굳이 따지자면 주소 값을 가지는 게 맞는데 그냥 값을 가진다고 생각하는 게 구조상 편하다고 하십니다.
Oracle JVM공식 문서를 참고한 결과
Constant pool 의 CONSTANT_Integer_info 구조는 아래와 같습니다.
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
unsigned 8bit 크기의 tag 는 3이라는 값을 가지며 이는 CONSTANT_Integer 라는 의미의 태그입니다.
unsigned 32bit 크기의 bytes는 int 상수 값을 빅 엔디안(big endian)방식으로 저장한다고 합니다. (일단 상수풀에서 Integer는 값 자체를 저장한다는 것을 알 수 있습니다. 음수는 어떻게 저장하는겨)
강사님의 말을 바탕으로 생각했을 때,
우리가 constant pool 참조 범위 내의 값을 변수에 저장했을 시
jvm에서는 constant pool에 unsigned 32bit로 표현할 수 있는 상수값을 저장하며,
이후 오퍼랜드 스택은 constant pool의 상수값을 참조하여 int 상수 값을 가지는 bytes 를 복사하는 것이아닌 주소를 참조하는 것이라고 생각할 수 있습니다.
더 파고들면 지금 하는 공부에 방해가 될 거 같아서 조금 더 지식이 쌓이면 다시 조사해보겠습니다
+) 2023.03.31
지식인 답변
1.자바에서 정수형 상수는 주소 값을 가지지 않습니다. 오퍼랜드 스택에 값(value)으로 푸시됩니다. JVM에서는 정수형 상수를 처리하기 위해 iconst, bipush, sipush 등의 명령어를 사용합니다. 이 명령어들은 해당하는 값(value)을 오퍼랜드 스택에 직접 푸시하므로 주소 값을 참조하는 것이 아니라 값(value)을 참조합니다.
2.constant pool에서 정수형 상수는 2진수로 표현된 값(value)이 저장됩니다. 저장된 값(value)은 JVM이 실행되는 플랫폼의 엔디안 방식에 맞추어 빅 엔디안(big endian) 또는 리틀 엔디안(little endian)으로 저장됩니다.
3.constant pool의 값(value)을 참조하는 오퍼랜드 스택은 해당하는 값(value)을 복사해서 연산을 수행합니다. 즉, 주소 값을 참조해서 연산을 수행하는 것이 아니라 값(value)을 복사해서 연산을 수행합니다. 이는 자바의 원시타입(primitive type)이 값(value)을 참조하고, 객체 타입(object type)은 주소 값을 참조하는 특성 때문입니다.
혹시나 이 포스팅을 보시는 분들 중 정답을 아시는 분은 댓글로 알려주시면 감사하겠습니다.
결론
'의문과 실험' 카테고리의 다른 글
[Java] 싱글톤과 Static은 뭐가 다를까? (0) | 2023.07.03 |
---|---|
[JAVA] Vector는 Thread-Safe 한가? (0) | 2023.04.21 |
[JAVA] 자바는 Call by Reference 지원 안해. 참조변수를 넘기는 경우는 뭘까? (0) | 2023.04.04 |
[JAVA] 오라클 공식 문서에도 없는 String pool은 도대체 무엇인가? (0) | 2023.04.03 |
[JAVA] int num = 1; num이 가리키는 메모리는 '값'을 가지는가? (0) | 2023.04.02 |