본문 바로가기
Development/Java

자바 Integer 캐싱에 대해 (Java IntegerCache, Autoboxing)

by Nahwasa 2024. 2. 27.

 

  간단한 자바 문제를 만들어달라는 요청을 받았다. 장난을 섞어서 이전에 알고리즘 문제를 풀 때 이슈가 있었던 상황을 문제로 만들었다. (이하의 코드 결과는 자바 실행 시의 옵션이나 구현에 따라 달라질 수 있다. 아무런 옵션을 넣지 않고, 이하의 코드만 실행한 경우를 가정한다.)

Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);

 

 

  어찌보면 당연해보이는 문제지만, 은근 주변에 시켜보면 답이 많이 다르게 나올꺼다.

1. 객체끼리의 주소값 비교이므로 false, false 라는 답변 -> 훌륭하다.

2. 에이 당연히 true, true지 -> 좀 아쉽다.

3. true, false네 -> 이미 이하의 내용을 알고 있는 경우이거나, 변태다.

 

 

  일반적으로 '1. false, false' 정도로 답변하면 좋은 답변이다. 원시타입(예를들어 int형의 100)이 Wrapper 클래스에 할당될 경우, 이 값은 Integer.valueOf(100); 처럼 컴파일러가 코드를 바꾸어 autoboxing 시킨다 (이 내용에 대해 잘 모른다면 '오라클 문서 - Autoboxing and Unboxing' 이걸 참고해보자.). 따라서 Integer로 변경된 값끼리 비교하게 되고, 이는 객체끼리의 비교이므로 equals를 써야 실제 값 비교이며, 객체에 대한 '=='은 주소값 비교이다. 따라서 false, false라고 답변했다면 충분한 답변이다.

 

 

  근데 사실 답은 '3. true, false'이다 (물론 Integer가 아니라 int 였으면 이건 또 둘 다 true 맞다.). 그 이유는 String이 캐싱되는 것 처럼 Integer도 캐싱 (정확힌 String과 달리 그냥 자바 프로그램이 실행될 때, 무조건 -128~127은 미리 만들어둔다.) 되기 때문이다. 별다른 옵션을 주지 않는다면 -128~127이 캐싱된다 . 즉, a랑 b를 127로 바꾸면 a==b는 true지만, a랑 b를 128로 바꾸면 a==b는 false라는 얘기이다.

 

 

  내 경우에도 위의 내용 정도까지만 알고 있었는데, 위 문제를 풀어본 후배의 질문에 궁금해진 부분들이 있어서 좀 더 알아보게 되었고, 해당 내용을 공유하려고 작성하게 되었다.

 


 

  그럼 우선 이 부분을 동작시키는 Integer.java 파일 내의 IntegerCache 클래스를 한번 봐보자. (이하 코드는 corretto-17 자바 버전의 코드이다.)

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer[] cache;
        static Integer[] archivedCache;

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    h = Math.max(parseInt(integerCacheHighPropValue), 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            // Load IntegerCache.archivedCache from archive, if possible
            CDS.initializeFromArchive(IntegerCache.class);
            int size = (high - low) + 1;

            // Use the archived cache if it exists and is large enough
            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;
            }
            cache = archivedCache;
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

 

 

  코드를 통해 알 수 있는 점은 다음과 같다.

 

1. 캐싱되는 범위는 조절할 수 있다.

String integerCacheHighPropValue =
    VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
    try {
        h = Math.max(parseInt(integerCacheHighPropValue), 127);
        // Maximum array size is Integer.MAX_VALUE
        h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
    } catch( NumberFormatException nfe) {
        // If the property cannot be parsed into an int, ignore it.
    }
}
high = h;

 

  코드로 보아 자바 실행 시 '-Djava.lang.Integer.IntegerCache.high=<size>' 처럼 옵션을 넣어 최대치를 조정할 수 있음을 알 수 있다. 추가로 IntegerCache 클래스의 주석을 통해 '-XX:AutoBoxCacheMax=<size>' 옵션으로도 동일하게 최대치를 조정할 수 있음을 알 수 있다.

 

 

2. 최소값은 지정할 수 없으며, 최대값은 127 이하로 설정 불가하다.

static final int low = -128;

 

  최소값은 -128로 고정이다. 최소값이 -128로 고정인 이유는 openjdk의 이슈사항 코맨트에서 그 이유를 유추해볼 수 있었다.

아직까진 최소값을 조절 가능하게 할 필요를 못느꼈다고 한다.

 

   따라서 Integer 캐싱의 범위는 아무런 설정도 안하면 [-128, 127] 이고, 따로 옵션을 주면 [-128, max(127, <size>)] 이다.  이하 코드에 따라 최대값은 127 이하로는 내릴 수 없다.

h = Math.max(parseInt(integerCacheHighPropValue), 127);

 

 

3. Integer 캐싱은 실행 시 미리 전부 만들어둔다.

// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;

// Use the archived cache if it exists and is large enough
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;
}
cache = archivedCache;

 

 

4. assert 문으로 최대값이 127이상이어야 한다고 써있다.

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;

 

  assert문은 실제 동작 시 기본적으론 실행되지 않는 부분이라 영향을 끼치지 않는다. 따로 옵션(-ea)을 주면 동작하긴 하는데, 문제는 애초에 이하의 코드에 따라 127 미만으로 값이 내려갈 일이 없다는 점이다.

h = Math.max(parseInt(integerCacheHighPropValue), 127);

 

  JLS 5.1.7을 보면 다음과 같이 적혀있다.

 

   따라서 유추해볼만한 점은, 주석에 적힌 JLS(Java Language Specification, 자바 표준)에 해당 내용이 적혀있으므로, 127 미만으로 내리면 JLS 표준을 만족할 수 없게 되므로 이후 JDK 버전업 시 코드 수정에 주의하라고 적어둔 것이라 생각된다.

 


 

  그럼 이와 같은 Integer 캐싱이 왜 필요할지도 한번 유추해봐야 하는데, 위 JLS 문서의 내용으로 보아, 특히 소형 디바이스에서의 성능 향상을 위해서인 것 같다.

This ensures that in most common cases, the behavior will be the desired one, without imposing an undue performance penalty, especially on small devices. Less memory-limited implementations might, for example, cache all char and short values, as well as int and long values in the range of -32K to +32K.

 

 

  이렇게 캐싱된 Integer 값은 Integer.valueOf(int i) 함수에서 쓰인다. 그리고 autoboxing 시에도 valueOf가 쓰이므로, 주로 자주 쓰일만한 -128~127 범위의 숫자를 미리 캐싱해서 성능향상을 노린 것 같다.

 

  사실 대강 생각해봐도 시간상으로 이득은 거의 없어보인다. 다만 소형디바이스에서 메모리상의 이득은 확실히 클 것 같다. 참고로 자바에서 int는 4byte지만, Integer는 대략 20byte이다. 왜 아냐면, 알고리즘 문제 풀다보면 int쓰냐 Integer 쓰냐에 따라 메모리초과(MLE)가 뜨는 경우가 생각보다 자주 있기 때문이다 ㅠ.

 

  근데 요즘은 확실히 메모리쪽으로 발전이 많이 되었다. 그래서 openjdk 코맨트에도 다음과 같은 코맨트가 적혀 있었다.

 

  그리고, 저 캐싱되는걸 이용해 뭔가를 하려는 시도는 좋지 않다. 예를들어, 캐싱되는 범위내의 값이라고 가정하고 Integer의 비교에 equals 대신 '=='을 굳이 쓸 이유는 없다. JLS 문서에도 이와 관련된 내용이 나온다.

Ideally, boxing a primitive value would always yield an identical reference. In practice, this may not be feasible using existing implementation techniques. The rule above is a pragmatic compromise, requiring that certain common values always be boxed into indistinguishable objects. The implementation may cache these, lazily or eagerly. For other values, the rule disallows any assumptions about the identity of the boxed values on the programmer's part. This allows (but does not require) sharing of some or all of these references. Notice that integer literals of type long are allowed, but not required, to be shared.

 

  오랜만에 재밌게 관련 내용들을 알아보게 된 것 같다.

댓글