Java auto boxing의 컴파일 방식과 Integer 캐시

int와 Integer를 섞어 쓸 때 생기는 auto boxing·unboxing이 Integer.valueOf()와 intValue() 호출로 컴파일되는 과정을 JDK 25의 바이트코드로 확인합니다. IntegerCache의 -128~127 범위와 JLS 5.1.7의 요구사항, -XX:AutoBoxCacheMax 옵션, 박싱된 타입을 반복문에 쓸 때의 비용, 그리고 캐시 구현과 wrapper 생성자 deprecation이 JDK 5부터 25까지 어떻게 바뀌었는지 정리합니다.

int 와 java.lang.Integer를 섞어 쓰면 JDK의 javac이 auto boxing과 unboxing 코드를 대신 넣어 줍니다. 그 변환이 어떤 메서드 호출로 바뀌는지 알아 두면 == 비교의 결과나 반복문에서 생기는 객체 생성 비용을 예측할 수 있습니다.

javac의 변환 방식 자체는 JDK 5에서 auto boxing이 도입된 뒤로 바뀌지 않았지만, 캐시의 구현과 wrapper 생성자의 deprecation 상태는 버전마다 달라졌습니다. 이 글의 바이트코드와 실행 결과는 JDK 25(Temurin 25+36)에서 확인했습니다.

박싱과 언박싱으로 컴파일되는 바이트코드

아래 코드는 int 와 Integer를 서로 대입하고 == 로 비교합니다.

Boxing.java
public class Boxing {
    public static void main(String[] args) {
        int a = 1;
        Integer b = 1;
        int c = b;
        System.out.println(a == b);
        System.out.println(a == c);
    }
}

JDK에 포함된 javap -c Boxing.class 를 실행하면 다음 바이트코드가 나옵니다.

  public static void main(java.lang.String[]);
    Code:
         0: iconst_1
         1: istore_1
         2: iconst_1
         3: invokestatic  #7    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         6: astore_2
         7: aload_2
         8: invokevirtual #13   // Method java/lang/Integer.intValue:()I
        11: istore_3
        12: getstatic     #17   // Field java/lang/System.out:Ljava/io/PrintStream;
        15: iload_1
        16: aload_2
        17: invokevirtual #13   // Method java/lang/Integer.intValue:()I
        20: if_icmpne     27
        ...

박싱과 언박싱만을 위한 바이트코드 명령은 따로 없습니다. JDK의 javac은 int 가 Integer로 바뀔 때 Integer.valueOf()를, Integer가 int 로 바뀔 때 intValue()를 호출하는 바이트코드로 컴파일합니다.

a == b 처럼 한쪽이 int 이고 다른 쪽이 Integer이면 Integer 쪽이 언박싱되어 값 비교(if_icmpne)가 됩니다. 반면 양쪽이 모두 Integer이면 언박싱 없이 참조 비교(if_acmpne)가 되므로, 다음 절에서 다루는 캐시 범위에 따라 결과가 달라집니다.

역컴파일하면 변환 결과를 Java 코드로 읽을 수 있습니다. 다만 요즘 역컴파일러는 valueOf()와 intValue() 호출을 auto boxing 문법으로 복원해서 원본과 거의 같은 코드를 보여 줍니다. CFR에서는 --sugarboxing false 옵션으로 이 복원을 끄고 메서드 호출을 그대로 보여 주게 할 수 있습니다.

java -jar cfr-0.152.jar --sugarboxing false Boxing.class
public class Boxing {
    public static void main(String[] stringArray) {
        int n = 1;
        Integer n2 = Integer.valueOf((int)1);
        int n3 = n2.intValue();
        System.out.println((n == n2.intValue() ? 1 : 0) != 0);
        System.out.println((n == n3 ? 1 : 0) != 0);
    }
}

Integer.valueOf((int)1) 의 (int) 처럼 원본에 없던 캐스팅과 (…​ ? 1 : 0) != 0 같은 표현은 역컴파일러가 붙인 것입니다. 바이트코드에는 boolean 값만 다루는 명령이 없어서, javac은 비교 결과를 if_icmpne 분기와 0·1 정수로 만듭니다. 역컴파일러는 그 흐름을 그대로 옮겨 이런 모양으로 보여 줍니다. 눈여겨볼 곳은 대입문 두 줄입니다. Integer n2 에는 Integer.valueOf()의 반환값이, int n3 에는 intValue()의 반환값이 들어갑니다.

--sugarboxing 옵션을 지정하지 않으면 아래처럼 원본 소스와 같은 코드가 나옵니다. 그래서 역컴파일 결과만으로는 박싱 비용을 판단할 수 없습니다.

java -jar cfr-0.152.jar Boxing.class
public class Boxing {
    public static void main(String[] stringArray) {
        int n = 1;
        Integer n2 = 1;
        int n3 = n2;
        System.out.println(n == n2);
        System.out.println(n == n3);
    }
}

Integer.valueOf()의 캐시와 참조 비교

new Integer(int) 생성자는 매번 새 객체를 만들지만, Integer.valueOf()는 캐시된 인스턴스를 재사용할 수 있습니다. JDK 25의 java.lang.Integer 는 -128부터 127까지의 인스턴스를 IntegerCache 에 담아 둡니다.

java/lang/Integer.java (JDK 25)
    private static final class IntegerCache {
        static final int low = -128;
        static final int high;

        @Stable
        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");
            ...
        }
    }

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

캐시의 기본 상한은 127이고, VM이 별도로 보관하는 saved property로 더 늘릴 수 있습니다. CDS(Class Data Sharing) 아카이브를 사용할 수 있으면 아카이브된 배열을 가져옵니다. 아카이브가 없거나 요청된 캐시 크기보다 작으면 캐시 전체 또는 부족한 부분을 실행 시점에 만듭니다.

JLS는 IntegerCache 라는 구현 자체를 요구하지 않습니다. JLS 5.1.7은 박싱 대상이 상수식일 때 아래 조건에서 두 박싱 결과가 반드시 같은 참조여야 한다고 규정합니다. JDK는 이 요구를 캐시로 충족합니다. 한편 Integer.valueOf(int)의 API 계약은 상수식 여부와 관계없이 -128부터 127까지를 항상 캐시한다고 보장합니다.

If the value p being boxed is the result of evaluating a constant expression of type boolean, byte, char, short, int, or long, and the result is true, false, a character in the range '\u0000' to '\u007f' inclusive, or an integer in the range -128 to 127 inclusive, then let a and b be the results of any two boxing conversions of p. It is always the case that a == b.

— JLS 5.1.7. Boxing Conversion

캐시 범위 때문에 아래 코드의 결과는 값의 크기에 따라 달라집니다.

Identity.java
public class Identity {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        Integer c = 128;
        Integer d = 128;
        System.out.println("a == b : " + (a == b));
        System.out.println("c == d : " + (c == d));
        System.out.println("c.equals(d) : " + c.equals(d));
    }
}
a == b : true
c == d : false
c.equals(d) : true

기본 설정에서 128은 캐시 범위 밖이므로 박싱할 때마다 새 Integer가 만들어집니다. 그래서 c == d 는 false 입니다. 박싱된 타입끼리는 == 대신 equals()로 비교해야 하는 이유입니다.

캐시 범위는 타입마다 다릅니다. JDK 25 기준으로 Long, Short, Byte는 -128~127을, Character는 '\u0000' 부터 '\u007f' 까지를 캐시합니다. Boolean은 Boolean.TRUE와 Boolean.FALSE를 재사용합니다.

-XX:AutoBoxCacheMax로 캐시 범위 조정

Integer는 HotSpot 옵션인 -XX:AutoBoxCacheMax=<max> 로 캐시의 상한을 늘릴 수 있습니다. 이 옵션은 C2 컴파일러가 포함된 HotSpot에서 쓸 수 있습니다. 앞의 예제를 같은 클래스 파일로 다시 실행하면 결과가 바뀝니다.

$ java Identity
a == b : true
c == d : false
c.equals(d) : true

$ java -XX:AutoBoxCacheMax=1000 Identity
a == b : true
c == d : true
c.equals(d) : true

같은 코드라도 실행 옵션에 따라 == 의 결과가 달라지므로, 박싱된 타입은 == 로 비교하면 안 됩니다. 이 옵션은 Integer에만 적용되고 Long이나 Short에는 영향을 주지 않습니다.

OpenJDK 구현에서는 -Djava.lang.Integer.IntegerCache.high=<max> 로 지정해도 같은 효과가 나지만, 이 값은 System.getProperty()로 조회하는 공개 시스템 프로퍼티가 아니라 VM이 별도로 보관하는 saved property입니다. 따라서 이 이름에 의존하기보다는 -XX:AutoBoxCacheMax 옵션을 쓰는 편이 안전합니다. 두 값을 함께 지정하면 VM이 -XX:AutoBoxCacheMax 값으로 이 프로퍼티를 덮어쓰므로 -XX 옵션이 우선합니다.

JDK 9부터 deprecated된 wrapper 생성자

new Integer(int)에는 JDK 9부터 @Deprecated 가 붙었습니다. 그전에는 javac이 이 코드를 경고하지 않았습니다. FindBugs 같은 정적 분석기나 IntelliJ IDEA의 인스펙션을 써야 찾을 수 있었습니다. Eclipse에 FindBugs 플러그인을 설치하면 Problems 뷰에 아래와 같은 경고가 떴습니다.

M P Bx: Method study.Test.testInteger() invokes inefficient new Integer(int) constructor;
        use Integer.valueOf(int) instead

지금은 javac이 알려 줍니다. -Xlint:deprecation 옵션을 붙이면 위치까지 경고합니다.

$ javac -Xlint:deprecation OldStyle.java
OldStyle.java:3: warning: [deprecation] Integer(int) in Integer has been deprecated
        Integer c = new Integer(1);
                    ^
OldStyle.java:4: warning: [deprecation] Double(double) in Double has been deprecated
        Double d = new Double(1.0);
                   ^
2 warnings

JDK 25의 소스에는 @Deprecated(since="9") 가 붙어 있습니다. forRemoval 이 없으므로 제거 예정 API는 아니지만, 새 코드에서 쓸 이유는 없습니다.

java/lang/Integer.java (JDK 25)
    /**
     * @deprecated
     * It is rarely appropriate to use this constructor. The static factory
     * {@link #valueOf(int)} is generally a better choice, as it is
     * likely to yield significantly better space and time performance.
     */
    @Deprecated(since="9")
    public Integer(int value) {

FindBugs의 후속 프로젝트인 SpotBugs에도 같은 규칙이 DM_NUMBER_CTOR 로 남아 있습니다. 설명 문구는 FindBugs 시절과 거의 같습니다.

Using new Integer(int) is guaranteed to always result in a new object whereas Integer.valueOf(int) allows caching of values to be done by the compiler, class library, or JVM. Using of cached values avoids object allocation and the code will be faster.

Values between -128 and 127 are guaranteed to have corresponding cached instances and using valueOf is approximately 3.5 times faster than using constructor. For values outside the constant range the performance of both styles is the same.

Unless the class must be compatible with JVMs predating Java 5, use either autoboxing or the valueOf() method when creating instances of Long, Integer, Short, Character, and Byte.

— SpotBugs DM_NUMBER_CTOR: Method invokes inefficient Number constructor; use static valueOf instead

다만 인용문의 '약 3.5배’라는 수치와 캐시 범위 밖에서는 성능이 같다는 설명은 Java API의 보장이 아닙니다. 실제 차이는 확장된 캐시 범위와 JIT 최적화, 실행 환경에 따라 달라질 수 있습니다.

IDE의 내장 검사로도 찾을 수 있습니다. Eclipse는 Java > Compiler > Errors/Warnings의 'Deprecated and restricted API' 아래 'Deprecated API' 항목의 기본값이 Warning이라 별도 플러그인 없이 deprecation 경고를 표시합니다. IntelliJ IDEA에는 'Number constructor call with primitive argument'(CachedNumberConstructorCall) 인스펙션이 Java > Numeric issues 그룹에 있습니다. Long, Integer, Short, Byte 생성자에 primitive 값을 넘기는 코드를 찾아 valueOf()로 바꾸도록 안내합니다. 기본값으로는 'Report only when constructor is @Deprecated' 옵션이 켜져 있어서, 생성자가 deprecated된 JDK 9 이상에서만 보고합니다.

박싱된 타입을 반복문에 쓸 때의 비용

박싱과 언박싱이 메서드 호출로 컴파일된다는 것을 알면, 반복문에서 박싱된 타입을 누적 변수로 쓸 때 어떤 일이 생기는지도 예측할 수 있습니다. 다음은 Effective Java 3판의 'Item 6: 불필요한 객체 생성을 피하라’에 나오는 예제를 이 글의 측정 코드에 맞게 변형한 것입니다.

static long boxed() {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    return sum;
}

sum += i 한 줄이 언박싱과 박싱을 모두 거치는 코드로 컴파일됩니다. 역컴파일하면 이 한 줄이 longValue()와 Long.valueOf() 호출로 바뀐 것을 볼 수 있습니다.

java -jar cfr-0.152.jar --sugarboxing false SumTest.class
    static long boxed() {
        Long l = Long.valueOf((long)0L);
        for (long i = 0L; i < Integer.MAX_VALUE; ++i) {
            l = Long.valueOf((long)(l.longValue() + i));
        }
        return l.longValue();
    }

바이트코드로도 같은 흐름을 확인할 수 있습니다.

  static long boxed();
    Code:
         0: lconst_0
         1: invokestatic  #7    // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
         4: astore_0
         5: lconst_0
         6: lstore_1
         7: lload_1
         8: ldc2_w        #15   // long 2147483647l
        11: lcmp
        12: ifge          32
        15: aload_0
        16: invokevirtual #17   // Method java/lang/Long.longValue:()J
        19: lload_1
        20: ladd
        21: invokestatic  #7    // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
        24: astore_0
        25: lload_1
        26: lconst_1
        27: ladd
        28: lstore_1
        29: goto          7

반복할 때마다 longValue()로 값을 꺼내 더한 뒤 Long.valueOf()로 다시 박싱합니다. 누적값이 127을 넘은 뒤에는 Long.valueOf()가 새 Long 인스턴스를 반환합니다. 다만 실제 객체 할당은 JIT 최적화로 제거될 수도 있습니다.

누적 변수의 타입만 long 으로 바꾸면 박싱이 사라집니다. 두 방식을 한 번씩 재는 프로그램을 JDK 25에서 세 번 실행해 보면 약 10배 차이가 납니다.

Long: 5122 ms, long: 507 ms
Long: 4987 ms, long: 516 ms
Long: 4919 ms, long: 513 ms

JMH 같은 벤치마크 도구를 쓰지 않고 System.nanoTime() 으로 각 실행에서 한 번씩 잰 값이라 절대 수치를 그대로 믿을 수는 없습니다.

캐시가 없는 Double와 Float

정수 계열과 달리 Double와 Float에는 캐시가 없습니다. JLS 5.1.7도 float 와 double 에는 참조 동일성을 요구하지 않습니다. JDK 25의 Double.valueOf()는 아직도 생성자를 그대로 호출합니다.

java/lang/Double.java (JDK 25)
    public static Double valueOf(double d) {
        return new Double(d);
    }

따라서 JDK 25의 Double.valueOf()는 값이 작아도 캐시된 인스턴스를 반환하지 않습니다. 정수 계열이라면 캐시되었을 -128~127 범위의 값도 박싱할 때마다 새 객체가 됩니다.

JDK 버전별 변경 이력

javac이 박싱과 언박싱을 valueOf()와 intValue() 호출로 컴파일하는 방식은 JDK 5 이후로 바뀌지 않았습니다. 대신 캐시의 구현과 생성자의 deprecation 상태, 그리고 언어 명세가 보장하는 범위가 아래와 같이 달라졌습니다.

JDK 버전 변경 내용

JDK 5

auto boxing과 unboxing 도입. valueOf(int) 등의 팩터리 메서드가 추가되고 -128~127을 캐시하기 시작

JDK 7

HotSpot의 -XX:AutoBoxCacheMax 옵션이 넘기는 java.lang.Integer.IntegerCache.high 값을 IntegerCache 가 읽어 캐시 상한을 정하도록 변경 (JDK-6807702)

JDK 9

wrapper 클래스의 생성자에 @Deprecated(since="9") 부여. JLS 5.1.7의 참조 동일성 보장 조건이 SE 8의 '리터럴’에서 '상수식의 평가 결과’로 바뀌고, 대상 타입에 long 포함

JDK 12

IntegerCache 를 비롯한 박스 캐시를 CDS 아카이브에 저장하고, 아카이브를 사용할 수 있으면 실행 시점에 읽어 오도록 변경 (JDK-8209120, JDK-8213033)

JDK 14

JLS 5.1.7의 참조 동일성 보장 대상 타입 목록에 byte 를 다시 포함 (SE 7에는 있었고 SE 8–13에는 빠져 있었음)

JDK 16

JEP 390으로 wrapper 클래스를 value-based class로 지정하고, 생성자를 forRemoval = true 로 표시

JDK 22

박스 캐시 배열에 @Stable 을 붙여 JIT이 상수로 접을 수 있게 함 (JDK-8295555)

JDK 25

생성자의 forRemoval 표시를 해제. 제거 예정이 아닌 일반 deprecated 상태로 되돌림 (JDK-8354335)

이 표의 JDK 버전은 OpenJDK mainline 최초 반영을 기준으로 합니다. IntegerCache의 상한 설정(JDK-6807702)은 6u14에, CDS 아카이브 반영(JDK-8209120)은 11.0.6에, 박스 캐시의 @Stable 변경(JDK-8295555)은 21.0.2에 backport되었습니다.

forRemoval 이 붙어 있던 JDK 16부터 JDK 24까지는 아무 옵션 없이 컴파일해도 removal 경고가 났습니다. JDK 21과 JDK 25에서 같은 코드를 컴파일해 보면 차이가 드러납니다.

$ ~/.sdkman/candidates/java/21.0.8-tem/bin/javac OldStyle.java
OldStyle.java:3: warning: [removal] Integer(int) in Integer has been deprecated and marked for removal
        Integer c = new Integer(1);
                    ^
OldStyle.java:4: warning: [removal] Double(double) in Double has been deprecated and marked for removal
        Double d = new Double(1.0);
                   ^
2 warnings

$ ~/.sdkman/candidates/java/25-tem/bin/javac OldStyle.java
Note: OldStyle.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

JLS 5.1.7의 문구도 버전마다 달랐습니다. JLS 3판(Java 5·6)과 Java SE 7은 리터럴 여부와 관계없이 true, false, byte 값, '\u0000' 부터 '\u007f' 까지의 char 값, -128부터 127까지의 int·short 값을 박싱한 결과가 같은 참조라고 규정했습니다. Java SE 8은 이 조건을 '리터럴’로 좁혀 적었습니다.

If the value p being boxed is an integer literal of type int between -128 and 127 inclusive, or the boolean literal true or false, or a character literal between '\u0000' and '\u007f' inclusive, then let a and b be the results of any two boxing conversions of p. It is always the case that a == b.

— JLS SE8 5.1.7

Java SE 9는 조건을 상수식의 평가 결과로 바꾸고 long 을 대상 타입에 넣었습니다. Integer x = 100 + 27; 처럼 리터럴이 아닌 상수식도 SE 8과 달리 보장 대상이 됩니다. JDK 구현은 JDK 5부터 valueOf()에서 -128~127을 캐시해 왔으므로, 어느 판의 문구에서도 실제 동작은 같았습니다.

정리

  • JDK의 javac은 auto boxing을 valueOf(), unboxing을 intValue() 계열의 메서드 호출로 컴파일합니다. 전용 바이트코드 명령은 없습니다.

  • Integer.valueOf()는 -128~127을 항상 캐시하고, JLS 5.1.7은 이 범위에 속하는 상수식의 박싱 결과가 같은 참조여야 한다고 요구합니다. Integer에 한해 -XX:AutoBoxCacheMax 로 캐시 상한을 넓힐 수 있습니다.

  • 박싱된 타입끼리는 == 가 아니라 equals()로 비교해야 합니다. == 의 결과가 값의 크기와 실행 옵션에 따라 달라집니다.

  • wrapper 클래스의 생성자는 JDK 9부터 deprecated 상태입니다. JDK 8까지는 javac이 경고하지 않아 FindBugs나 IDE 검사에 기대야 했지만, 지금은 javac이 deprecation을 알리고 Eclipse와 IntelliJ IDEA도 기본 설정으로 경고합니다.

  • 되도록 primitive 타입을 쓰고, Collection의 요소나 generics의 타입 인자처럼 객체가 필요한 자리에만 박싱된 타입을 씁니다. (Effective Java 3판, 'Item 61: 박싱된 기본 타입보다는 기본 타입을 사용하라')

주요 변경이력
  • 2026.09.26

    • JLS 5.1.7 문구의 판본별 변화(3판·SE 7 → SE 8 리터럴 → SE 9 상수식 → SE 14 byte)를 바로잡음

    • JDK-6807702의 설명을 라이브러리 변경으로 고치고 6u14 backport 추가

    • -XX:AutoBoxCacheMax 의 C2 의존성과 -D 프로퍼티와의 우선순위, IntelliJ 인스펙션 기본 옵션 보완

  • 2026.08.29

    • JDK 25 기준으로 바이트코드, IntegerCache 소스, 경고 메시지를 다시 확인해 갱신

    • Eclipse 캡처 화면을 예제 소스와 javap 출력으로 교체하고, 화면에 있던 FindBugs 경고 메시지는 본문에 인용

    • -XX:AutoBoxCacheMax 옵션, JLS 5.1.7 인용, 실행 시간 측정 결과 추가

    • FindBugs 항목을 SpotBugs 기준으로 갱신하고, Eclipse·IntelliJ IDEA의 현재 경고 방식 추가

    • Effective Java 인용을 2판(Item 5, 49)에서 3판(Item 6, 61) 기준으로 수정

    • 'JDK 버전별 변경 이력' 절을 추가하고, 2009년 원문을 언급하던 서술을 버전 기준 서술로 정리

    • 제목을 'Java의 auto boxing과 unboxing은 어떻게 컴파일될까?'에서 'Java auto boxing의 컴파일 방식과 Integer 캐시’로 변경

    • 원문에 있던 역컴파일 결과를 CFR 0.152로 다시 뽑아 수록. 역컴파일러가 박싱을 도로 접는다는 점과 --sugarboxing 옵션 설명 추가

    • JLS와 Integer.valueOf()가 보장하는 범위를 구분하고 CDS, 확장 캐시, JIT 및 backport 관련 표현을 보완

  • 2009.02.10

    • 최초 작성

Spring MVC 2.5을 활용한 파일업로드 Oracle을 사용해 입출력하는 Map-Reduce