3.27日,王忠杰老师在微信群中通过抛出多个问题来引出了一些java的细节知识点。
本文试图为之集成讲解。
一.Integer生成方法对比
上图是王老师在微信群中发出的第一个问题,将问题中的情景复现后得到下图。(//后为输出结果)
在上列测试中,我们始终采取的是==号进行比较。换句话说,我们比较的实际上是==两边的对象是否是指向同一块堆上内存的同一对象。(因为==针对引用类型,比较的就是内存地址,而不是任何类中的属性)。总而言之,如果比较的两个变量(例如a,b) 是指向同一个实例的指针,那么a==b返回值为true,否则就是false。
在此基础上,我们就能理解测试结果中的,a与b与c为指向不同的Integer实例的三个指针,而不是指向同一内存中实例的指针。
然而相同的生成方法,在我们将Integer的值修改为2的时候却出现与值为200的情况完全相反的结论。
这是为啥呢?
这就要从Integer类的生成方法讲起了。
Integer类的构造器(constructor)只有两种,分别是以字符串与int类型为参数的构造器。
通过构造器生成的Integer一定是一个全新的在堆上存储的实例,这点毫无疑问。但是Integer除了构造器生成外,还存在静态工厂生成。在上图测试中Integer.valueOf()正是Integer的一个静态工厂。而之所以会出现上面的相反结论,与该静态工厂直接挂钩。
先看 Integer a = 200;
这里存在语法糖,200肯定不是一个引用类型,但是我们最后发现a得到了一个Integer实例,这就证明这句话在编译或者编译前会被替换成能明确生成一个引用类型的语句。在反编译后,我们会发现这句话实际等价于
Integer a = Integer.valueOf(200);
也就是说上面我们比较的几个变量的生成方式其实都是一样的,都是静态工厂Integer.valueOf()。但是前面比较都是false,而后面比较都是true,就说明Integer.valueOf()是选择性返回相同实例,选择性创建全新实例。为更深入理解问题本质,我们查看Integer.valueOf()的源代码。
可以从中看出,valueOf()的返回值要么是IntegerCache中的一个缓存,要么就是一个全新的Integer实例。而当i值满足 IntegerCache.low<=i<=IntegerCache.high时返回的是IntegerCache.cache中的元素,其余情况就是返回一个new Integer(i),这里可以判断200应该是不满足这个范围要求,所以返回的值都是不同的实例。
继续追踪IntegerCache,我们会发现IntegerCache是Integer类的私有静态内部类。
且在其加载时会执行一段静态初始化操作,这段操作写明了IntegerCache的缓存机制。
源码如下:
由源码我们可以看出,在IntegerCache被加载时它会初始化执行static中的代码,也就是说它会开辟一个low到high的Integer数组来缓存这两个值中所有Integer,而这个数组low值已被确定,high值是一个可以通过VM参数输入改变的值,它将取至少127,可能多不可能少。
那么到这一步,针对上述问题我们已经可以很轻易地解决了。
Integer存在缓存类IntegerCache,而缓存类会在第一次加载时初始化一个Integer缓存数组,这个数组大小不是固定的,但是一定大于或等于256,覆盖范围为[-128,127]。
除此以外,再放上一段IntegerCache的spec方便大家更好理解
二.IntegerCache大小修改
其实参考上文spec,大家已经可以看出答案了,cache的大小可以通过jvm指令修改:
-XX:AutoBoxCacheMax==<size>
并且这个指令实际上修改的是sun.misc.VM中保存的java.lang.Integer.IntegerCache.high值的副本,而由于IntegerCache初始化时会去与VM类中副本更新因此才改变了IntegerCache的high值。
三.jvm实现cache的数据类型
目前我们已知实现有String,Integer。(王老师说的)
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.
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.
A boxing conversion may result in an OutOfMemoryError if a new instance of one of the wrapper classes (Boolean, Byte, Character, Short, Integer, Long, Float, or Double) needs to be allocated and insufficient storage is available.
https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.7
那么我贴这么长一段话是为了什么呢?
这段话是摘自oracle的jls文档也就是java语言规范文档,这里并没有讲述到底目前哪些数据实现了缓存(黑色加粗的地方是作者想表述实际内存大小与渴求目标妥协的一种情况,而不是明确告诉我们哪些类实现了缓存),但是它讲述出了通用数据类型缓存的意义所在。
缓存是为了在对相同值进行封装时返回相同引用而建立的。
作者在上文中表示出的意思其实就是:缓存是为了在你反复封装整数2的时候,返回给你一个相同的封装值为2的整数封装实例,而不是反复新建包含2的新实例给你去浪费空间。但是实际上现有技术可能做不到这一点,于是在与内存的权衡中,开发者选择开创一片缓存区,保证至少缓存区中的封装类能做到返回相同引用,至于缓存区外的,他就不管了,你也不许乱用。
好,说完了缓存的意义,那到底有哪些通用数据类型实现缓存了呢?
答案是除float和double以外的所有基础数据类型对应的封装类型都实现了缓存。他们缓存都是通过valueOf()静态工厂完成的。(可以亲自去看源码对比,反正我是不放图了)
四.封装类的autoBoxing与unboxing
(放图放图)
(先看实际结果吧)
我们根据上面的分析已经知道了,Integer在生成的时候存在语法糖,这里会不会也有语法糖的存在呢?(怎么感觉这问的有点作)
直接说吧,其实我们在上面提到的Integer生成时调用valueOf()方法和这里a++操作其实都是名为autoboxing和unboxing的语法糖。
语法糖的含义其实就是你在书写代码时感觉代码可以写的很人性化像吃糖一样爽,所以叫语法糖。
那什么叫人性化呢?举例我们知道Integer明明是引用类型,但是它却可以直接用基本数据类型的加减乘除符号去处理,还能用++等,就和int值一样。这是不是很爽,这种场景下,我们清晰地知道这个类就代表一个int值从而不需要去严格遵守java语法,只需要表达出我们的意思,让编译器替我们完成矫正语法的工作,从而简化编程。
autoboxing和unboxing就是两个语法糖,他们解释起来很容易,就是你在某一个场景下需要Integer但是提供却是int,那么java编译器会自动把int封装。同理如果你需要int,比如进行++操作,*,/操作但是提供的却是Integer实例,那么java编译器会自动把Integer解包。
知道这一点了我们再看看上面的代码,a++==b++。这一步中a是Integer要++肯定需要解包。并且经过实验,这里解包得到的int在执行完++操作后,会重新被打包。那么实际上发生的事就是:
a++操作将a(Integer)压入栈中,随后在对a取加一操作时,进行解包,操作完后,将a.intValue()+1的值经过valueOf()封装赋值给a,而将原先的压入栈中的a(Integer)返回给表达式。
这一段操作看起来比较绕,其实这与++操作在字节码层面的具体步骤有关,关于字节码层面的解释我就不多写了。
对b++也是同理,所以最后==比较的其实就是最开始的a,与b。
那么根据我们的解释,经过a++==b++后a与b应该是重新通过静态工厂生成的value为3的实例,那么根据缓存两者应当相等,那么实际情况是不是这样呢?
同理我们再看一下++a==++b。先拆包,再封装。那么返回的值肯定也是valueOf()对4的缓存的相同引用。相等自然成立。
总结
java设计者经过多个版本的迭代,积累了很多从不同角度对这门语言进行的优化,对于这些优化如果作为开发者我们能熟练知道的话,那么驾驭语言的能力想必也会提升很多。(至少bug会少一点)