의문과 실험

[JAVA] int num = 1; num은 어떻게 '값'을 가지며, 비교될까

hyunsb 2023. 3. 27. 20:26

결론

 

[JAVA] int num = 1; 그래서 num은 '값'을 가지는가?

이전의 포스팅은 접근 방식 자체가 잘못되었다는 것을 깨달았다. 나의 질문이 모호했기 때문에 사람마다 답변이 달랐다고 생각한다. int a = 1; 에서 변수 a는 값 그 자체를 가리킨다는 사람과 주

hyunsb.tistory.com


의문

primitve type은 Constant Pool에 있는 특정 상수를 참조하는 것이다.

그렇기에 int a = 1, int b = 1 인 경우 같은 주소를 참조하기 때문에 a==b 가 성립된다.

라는 정보를 보고 정말 그럴까? 값을 저장하는 것이 아닌가? 라는 의문이 생겨 해당 실험을 진행하게 되었습니다.

int num = 1;

위의 코드는 정수형 변수 num1이라는 값을 저장하는 코드입니다.

코린이인 저도 의미를 바로 알 수 있는 코드입니다.

그런데 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

 

명령어는 아래 링크에 장 정리되어 있습니다.

 

List of Java bytecode instructions - Wikipedia

From Wikipedia, the free encyclopedia This is a list of the instructions that make up the Java bytecode, an abstract machine language that is ultimately executed by the Java virtual machine.[1] The Java bytecode is generated from languages running on the J

en.wikipedia.org

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 공식 문서를 참고하여 상수풀이 어떻게 구성되어 있는지 확인해봅시다.

 

Chapter 4. The class File Format

 

docs.oracle.com

상수풀은 리터럴 상수 값을 저장하는 곳입니다. String 뿐만 아니라 모든 종류의 숫자, 문자열, 식별자 이름, Class 및 Method에 대한 참조와 같은 값이 포함됩니다.

 

상수풀은 해당 항목이 나타내는 상수의 종류를 저장하는 1byte 크기의 변수 tagtag에 따라 달라지는 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의 값 1Integer타입의 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 크기의 bytesint 상수 값빅 엔디안(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] int num = 1; 그래서 num은 '값'을 가지는가?

이전의 포스팅은 접근 방식 자체가 잘못되었다는 것을 깨달았다. 나의 질문이 모호했기 때문에 사람마다 답변이 달랐다고 생각한다. int a = 1; 에서 변수 a는 값 그 자체를 가리킨다는 사람과 주

hyunsb.tistory.com