聊一聊Integer的缓存机制问题

在Java编程中,Integer类作为基本类型int的包装器,提供了对象化的操作和自动装箱与拆箱的功能。从JDK5开始引入了一项特别的优化措施——Integer缓存机制,它对于提升程序性能和减少内存消耗具有重要意义。接下来我们由一段代码去打开Integer缓存机制的秘密。

public static void main(String[] args) {  
    Integer i1 = 100;  
    Integer i2 = 100;  
    System.out.println(i1 == i2);  
    Integer i3 = 1000;  
    Integer i4 = 1000;  
    System.out.println(i3 == i4);  
}

至于答案是什么呢?我们接着往下看,等你看完就明白了。

当你在你的Idea中写出这段代码的时候,Idea就会提示你要使用equals()方法区比较大小,因为Integer是对象,对象的值比较要用equals()方法,而不是使用==,这里我们主要是研究一下Integer的缓存机制。

Integer缓存是什么

Java的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象。默认情况下,这个范围是-128至127。当通过Integer.valueOf(int)方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象。我们看一下Integer.valueOf(int)的源码:

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

对于Integer.valueOf(int)方法来说,由于这个方法经常用于将基本类型int转换为包装器对象,所以它使用了@HotSpotIntrinsicCandidate注解,这样HotSpot JVM可能会提供一种更为高效的内部实现来处理自动装箱操作。而IntegerCacheInteger内部的一个静态类,负责缓存整数对象。它在类加载时被初始化,创建并缓存范围内的所有整数对象。我们看一下IntegerCache的源码:

private static class IntegerCache {  
    // 缓存范围的下限,默认为-128  
    static final int low = -128;  
    // 缓存范围的上限,初始化时动态计算(基于系统属性或默认值127)  
    static final int high;  
    // 存储在缓存范围内所有Integer对象的数组  
    static final Integer cache[];  
    // 静态初始化块,在类加载时执行  
    static {  
        // 初始设定high为127  
        int h = 127;  
        // 尝试从系统属性获取用户自定义的最大整数值  
        String integerCacheHighPropValue =  
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");  
        // 如果系统属性存在并且可以转换为int类型,则更新high值  
        if (integerCacheHighPropValue != null) {  
            try {  
                int i = parseInt(integerCacheHighPropValue);  
                // 确保high至少为127,并且不超过Integer.MAX_VALUE允许的最大数组大小  
                h = Math.max(i, 127);  
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);  
            } catch( NumberFormatException nfe) {  
            }  
        }  
  
        // 设置最终确定的high值  
        high = h;  
        // 初始化cache数组,长度等于缓存范围内的整数数量  
        cache = new Integer[(high - low) + 1];  
        // 使用循环填充cache数组,创建并存储对应的Integer对象  
        int j = low;  
        for(int k = 0; k < cache.length; k++) {  
            cache[k] = new Integer(j++);  
        }  
        // 检查,确保缓存范围至少包含[-128, 127]  
        // 这是Java语言规范对小整数自动装箱共享的要求  
        assert IntegerCache.high >= 127;  
    }  
    // 私有构造器,防止外部实例化此内部类的对象  
    private IntegerCache() {}  
}

IntegerCache类在Java虚拟机启动时创建了一个固定大小的数组,用于缓存指定范围内所有的Integer对象。这样在后续程序运行过程中,对于这些范围内的整数进行装箱操作时,可以直接从缓存中获取已存在的对象,以提升性能并减少内存开销。同时,它也提供了根据系统属性(-Djava.lang.Integer.IntegerCache.high)来自定义缓存上限的能力,并确保满足Java语言规范关于小整数自动装箱共享的规定。

Integer.value(int)方法中,如果int的值在IntegerCache返回的lowhigh之内,则直接返回IntegerCache中缓存的对象,否则重新new一个新的Integer对象。

而文章开头示例中,我们使用Interge i1 = 100的方式其实是Java的自动装箱机制,整数字面量100是一个基本类型的int值。当赋值给一个Integer引用变量i时,编译器会隐式地调用Integer.valueOf(int)方法将这个基本类型的int值转换为Integer对象。

整数在编程中经常被使用,特别是在循环计数等场景中,通过缓存整数对象,可以大幅度减少相同整数值的对象创建,从而减小内存占用。

由此我们可以看出因为100在[-128, 127]之内,所以i1 == i2打印true,而1000不在[-128, 127]之内,所以i3 == i4打印false
image.png

我们尝试使用java.lang.Integer.IntegerCache.high调整一下high为1000,然后看一下效果:
image.png
image.png
打印结果都是true。

当然这个上限不要随意去调整,调整之前,需要仔细评估应用程序的实际需求和性能影响。尽量选择在[-128, 127]范围内的整数值,以充分利用Integer缓存机制。

注意事项

  • 比较: 由于缓存的存在,在-128至127之间的Integer对象在进行==运算符比较时,结果可能是true,因为它们指向的是同一个内存地址。而在缓存范围之外创建的Integer对象即使值相等,也会视为不同的对象,因此使用==比较会返回false。不论是否启用缓存,对于任何两个Integer对象,只要其包含的整数值相同,调用equals()方法始终会返回true。所以我们在比较对象时一定要使用equals()方法。

  • 不适用于所有场景: 当使用new Integer(i)直接创建Integer对象时,不会利用缓存。

  • 不要随意去扩展缓存的上下限

总结

Integer缓存机制是Java中的一项性能优化措施,通过缓存一定范围内的整数对象,既能减小内存开销,又能提高性能。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

posted @ 2024-03-06 10:21  码农Academy  阅读(203)  评论(0编辑  收藏  举报