赞
踩
上篇文章《结构型:7.享元模式(上):享元模式原理和应用》,通过棋牌游戏和文本编辑器的例子,学习了享元模式的原理、实现以及应用场景。用一句话总结下,享元模式中的 “享元” 指被共享的单元。享元模式通过复用对象,已达到节省内存的目的。
本章,带你剖析一下享元模式在 Java Integer、String中的应用。
先看下面这段代码。先思考下,这段代码会输出什么样的结果。
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
如果你不熟悉 Java,你可能会觉得 i1 和 i2 值都是 56,i3 和 i4 值都是 129,所以 i1 跟 i2 值相等, i3 跟 i4 值相等,所以输出的结果应该是两个 true,这个图的分析是不对的,主要还是因为你对 Java 语法不熟悉。要正确分析上面的代码,需要弄清楚下面两个问题。
Java 为基本数据类型提供了对应地包装器类型,具体如下表所示。
基本数据类型 | 对应地包装器类型 |
---|---|
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
short | Short |
byte | Byte |
char | Character |
所谓自动装箱,就是自动将基本数据类型转换为包装器类型。所谓的自动拆箱,也就是自动将包装器类型转化为基本数据类型。具体代码如下所示:
Integer i = 56; // 自动装箱
int j = i; // 自动拆箱
数值 56 是基本数据类型,当赋值给包装类型(Integer) 变量的时候,触发自动装箱操作,创建一个 Integer 类型的对象,并赋值给变量 i。其底层相当于执行了下面这条语句:
Integer i = 56; 底层执行了 Integer i = Integer.valueOf(56);
反过来,当把包装器类型的变量 i,赋值给基本数据类型的变量 j 时,触发自动拆箱操作,将 i 中的数据取出,赋值给 j。其底层相当于执行了下面这条语句:
int j = i; 底层执行了 int j = i.intValue();
弄清楚了自动装箱和拆箱,再看下,如何判定两个类型是否相等? 在此之前,我们要先搞清楚,Java 对象在内存中是如何存储的。通过下面的例子来说明下。
User a = new User(123,123);
针对这条语句,我画了一张内存存储结构图,如下所示。 a 存储的值是 User 对象的内存地址,在图中表现为 a 指向 User 对象。
通过 “==” 来判定两个对象是否相等时,实际上是在判断两个局部变量存储的地址是否相同,换句话说,是在判断两个局部变量是否指向相同的对象。
我们在重新看下开头的那段代码。
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
前 4 行触发自动装箱操作,也就是会创建 Integer 对象,并赋值给 i1、i2、i3、i4 这四个变量。根据刚刚的讲解,i1、i2 尽管存储的值都是 56,但是指向不同的 Integer 对象,所以通过 “==” 来判定是否相同时,会返回 false。同理,i3 == i4
判定语句也会返回 fasel。
但是,上面的分析还是不对,答案并非是两个 false,而是一个 true,一个 fasle。这正是因为 Integer 用到了享元模式来复用对象,才会导致这样的运行结果。当我们自动装箱,也就是调用 valueOf()
来创建 Integer
对象的时候,如果要创建的对象的值在 -128~127
之间,会从 IntegerCache
类中直接返回,否则才会调用 new 方法创建。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
实际上,这里的 IntegerCache
相当于上篇文章的生成享元对象的工厂类,只不过名字不叫 xxxFactory
而已。我们来看下 IntegerCache
的具体代码实现,它是 Integer
的内部类,你可以自行查看 JDK 源码。
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
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) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
为什么 IntegerCache
只缓存 -128~127
之间的整型数呢?
在 IntegerCache
的代码实现中,当这个类被加载的时候,缓存的享元对象会被集中一次性的创建好。我们不可能在 IntegerCache
类中预先创建好所有的整型值,这样既占用太多内存,也使得 IntegerCache
类的加载时间过长。所以,只选择缓存对于大部分应用来说最常用的整型值,也就是一个字节的大小(-128~127
之间的数据)。
实际上,JDK 也提供了方法让我们可以自定义缓存的最大值,有下面两种方式。
// 方法一
-Djava.lang.Integer.IntegerCache.high=255
// 方法二
-XX:AutoBoxCacheMax=255
如果你通过分析应用的 JVM 内存占用情况,发现 -128~255
之间的数据占用的内存比较多,你就可以采用上面的方式,将缓存的最大值从 127 调整至 255。不过, JDK 并没有提供最小值的设置方法。
现在,再回到最开始的问题,因为 56 处于 -128~127
之间,i1、i2 会指向相同的享元对象,所以 i1 == i2
会返回 true。而 128 大于 127,并不会被缓存,每次都会创建一个全新的对象,也就是说 i3、i4 会指向不同的 Integer 对象,所以 i3 == i4
会返回 false。
除了 Integer
之外,其他包装器类型,比如 Long
、Short
、Byte
等,也都利用了享元模式来缓存 -128~127
之间的数据。比如,Long
类型对应的 LongCache
享元工厂类及 valueOf() 函数代码如下所示:
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
/**
* Returns a {@code Long} instance representing the specified
* {@code long} value.
* If a new {@code Long} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Long(long)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* Note that unlike the {@linkplain Integer#valueOf(int)
* corresponding method} in the {@code Integer} class, this method
* is <em>not</em> required to cache values within a particular
* range.
*
* @param l a long value.
* @return a {@code Long} instance representing {@code l}.
* @since 1.5
*/
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
我们平时的开发中,对于下面这样三种创建整型对象的方式,优先使用后两种。
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);
第一种创建方式不会用到 IntegerCache
,而后两种创建方法可以利用 IntegerCache
缓存返回共享的对象,已达到节省内存的目的。
刚刚我们讲了享元模式在 Java Integer
类中的应用,现在在看下享元模式在 Java String
类中的应用。同样,还是先来看一段代码,你觉得这段代码输出的结果是什么呢?
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
上面代码运行的结果一个是 true,一个是false。跟 Integer
的设计思路类似,String
类利用享元模式来复用相同的字符串常量(也就是代码中的 “Hello”)。JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫做 “字符串常量池”。上面代码对应的内存存储结构如下所示:
不过 String 类的享元模式的设计,跟 Integer
类稍微不同。Integer
类中要共享的对象,是在类加载的时候,就集中一次性地创建好。但是,对于字符串来说,没法事先知道要共享哪些字符串常量,只能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已存在的即可,就不需要再重新创建了。
在 Java Integer
的实现中,-128 到 127 之间的整型数会被实现创建好,缓存在 IntegerCache
类中。当我们使用自动装箱或者 valueOf()
来创建这个数值区间的整型对象时,会复用 IntegerCache
类事先创建好的对象。这里的 IntegerCache
类就是 享元工厂类,事先创建好的整型对象就是享元对象。
在 Java String
类的实现中,JVM 开辟一块存储区专门存储字符串常量,这块存储区叫做字符串常量池,类似于 Integer
中的 IntegerCache
。不过,跟 IntegerCache
不同的是,它并非事先创建好需要共享的对象,而是在程序运行期间,根据需要来创建和缓存字符串常量。
此外,还要补偿强调一下。
享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保持了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此,在某些情况下,如果对象生命周期很短,也不会被密集使用,利用享元模式反倒可能会浪费更多的内存。所以,除非经过线上验证,利用享元模式真的可以大大节省内存,否则,就不要过度使用这个模式,为了一点点内存的节省而引入一个复杂的设计,得不偿失。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。