关于 Java 的自动装箱 (autoboxing) 的一些意想不到的结果

红薯 发布于 2012/02/01 22:39
阅读 2K+
收藏 11

译自:vanillajava

理解 Java 核心的真正工作原理可以帮你编写更简单和快速的应用程序。

概要

Java 的自动装箱有很多意想不到的结果,其中有些是广为人知的,这些奇怪的结果多数是因为自动装箱对象的缓存导致的。

== 和 equals 可以也可以不匹配

如果你使用默认的参数来运行 AutoboxEqualsMain 程序,执行结果如下:
All Boolean are cached.
All Byte are cached.
The first auto-boxed char is 0 and the last is 127
The first auto-boxed short is -128 and the last is 127
The first auto-boxed int is -128 and the last is 127
The first auto-boxed long is -128 and the last is 127
No Float are cached.
No Double are cached.
大家都知道 == 比较的是引用,而对于 Integer 和其他任何 Object 类型,使用 Java 7 的-XX:+AggressiveOpts 参数,你将得到近乎相同的结果:
All Boolean are cached.
All Byte are cached.
The first auto-boxed char is 0 and the last is 127
The first auto-boxed short is -128 and the last is 127
The first auto-boxed int is -128 and the last is 20000
The first auto-boxed long is -128 and the last is 127
No Float are cached.
No Double are cached.
注意:该选项增加了 Integer 缓存的最大值

性能的提升

对使用了缓存来说,一个 int[]ArrayList<Integer> 对象占用的内存大小几乎是相同的,如果你运行该程序来构建一个从 1 到 16,000 的集合时,参数是: -XX:-UseTLAB-XX:+AgressiveOpts ,那么你将得到以下结果:
The int[16000] took 64592 bytes and new ArrayList() with 16000 values took 65048 bytes

导致这个结果有两个原因:

首先,这是运行在 64 位的 JVM 虚拟机上;ArrayList 是一个数组的引用。而 JVM 可使用 32位来引用最高达 32G的堆内存。注意:这里你可以使用直接内存以及内存映射文件来使用超过32GB存储,但如果你的堆小于 32G,就可以使用 32位的引用。

其次,对象本身是不会占用任何空间,因为所有值在程序启动前都已缓存。

注意:这么短的一个测试,每次得到的值都有所不同。经过测试我得到的最高和最低的值相差不超过20%。

自动装箱对象不被垃圾收集

如果你使用自动装箱对象作为 key,它可能永远不会被垃圾收集掉。
Map<Long, String> keyValueMap = new WeakHashMap<Long, String>(10000);
for (long i = 1; i <= 8192; i *= 2)
    keyValueMap.put(i, "key-" + i);
System.out.println("Before GC: " + keyValueMap);
System.gc();
System.out.println("After GC: " + keyValueMap);
Map 的键不会被保存,因此你希望用 GC 来清除,但事实并不如你所愿。
Before GC: {8192=key-8192, 4096=key-4096, 2048=key-2048, 1024=key-1024, 512=key-512, 256=key-256, 
    128=key-128, 64=key-64, 32=key-32, 16=key-16, 8=key-8, 4=key-4, 2=key-2, 1=key-1}
After GC: {64=key-64, 32=key-32, 16=key-16, 8=key-8, 4=key-4, 2=key-2, 1=key-1}
注意:我提升了 WeakHashMap 的初始容量,所以所有的键都可以在一个预先设定好的顺序。

不要使用自动装箱的对象作为锁

不要使用字符串做为锁对象
synchronized("lock one") { // DON'T do this.
下面这种方法也不要使用:
Object o = 
Integer lock = o.hashCode() & 255;
synchronized(lock)  { // DON'T do this either.
看起来这似乎是一种很聪明的方法来处理锁对象,但千万别这样用,因为你会发现程序会意外失败,而且很难重现。这还取决于你是否使用了 -XX:+AggressiveOpts 参数。

下载本文示例代码

加载中
0
Fred
Fred
倒数第二点很实用~~多谢
0
tianpeng91
tianpeng91
我想知道为啥不能使用 自动装箱的对象作为锁呢???楼主能给个解释吗?
0
coda
coda
理解了Integer的缓冲池,上面的4条其实是一码事儿。
Integer类默认有个缓冲池,大家看下Integer的嵌套类IntegerCache就清楚了。
 
简单理解就是那个池里的都是单例,在此范围内数值相同则是一个对象。

1. == 和 equals 可以也可以不匹配

      == 比较的是内存地址,是同一个对象则返回true。 这和一般的对象比较没有区别。

3. 自动装箱对象不被垃圾收集

    请注意,只是缓存起来的没被收集,这是单例的特性。

4. 不要使用自动装箱的对象作为锁

   和使用string对象一样,容易造成死锁。
 
附IntegerCache源码(jdk1.7):
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];
    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
        // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low));
    }
    
    high = h;
    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);
    }
    private IntegerCache() {}
}
0
coda
coda

补充几点:

1.  两个操作数都是包装类型时进行的是引用比较即==

Integer a = new Integer(100);
Integer b = new Integer(100);
a == b ; //false;

     相反: 

Integer a = Ingeter.valueOf(100);
Integer a = Ingeter.valueOf(100);
a == b ; // true;

     原因是Integer.valueOf()会取缓冲池里的对象加以利用。

     这也是我们提倡使用Integer.valueOf()而不是new Integer()的原因,就像我们不提倡new  String()一样。

2.  一方是原始类型,一方是包装类时,进行拆箱,即值比较。    

100 == new Integer(100) ; // tue;

陆云帆
说的好!
0
Silencer
Silencer
张见识了
0
g
guyingll
学习了~~
0
杨延庆
杨延庆
那个死锁和垃圾自动收集真的很有用,以后使用得特别注意
返回顶部
顶部