从 JVM 底层原理分析数值交换那些事
基础数据类型交换
这个话题,需要从最最基础的一道题目说起,看题目:以下代码 a 和 b 的值会交换么:
结果估计大家都知道,a 和 b 并没有交换:
但是原因呢?先看这张图,先来说说 Java 虚拟机的结构:
运行时区域主要分为:
线程私有:
- 程序计数器:Program Count Register
,线程私有,没有垃圾回收
- 虚拟机栈:VM Stack
,线程私有,没有垃圾回收
- 本地方法栈:Native Method Stack
,线程私有,没有垃圾回收
线程共享:
- 方法区:Method Area
,以HotSpot
为例,JDK1.8
后元空间取代方法区,有垃圾回收。
- 堆:Heap
,垃圾回收最重要的地方。
和这个代码相关的主要是虚拟机栈,也叫方法栈,是每一个线程私有的。
生命周期和线程一样,主要是记录该线程 Java 方法执行的内存模型。虚拟机栈里面放着好多栈帧。*注意虚拟机栈,对应是 Java 方法,不包括本地方法。*
一个 Java 方法执行会创建一个栈帧,一个栈帧主要存储:
局部变量表
操作数栈
动态链接
方法出口
每一个方法调用的时候,就相当于将一个栈帧放到虚拟机栈中(入栈),方法执行完成的时候,就是对应着将该栈帧从虚拟机栈中弹出(出栈)。
每一个线程有一个自己的虚拟机栈,这样就不会混起来,如果不是线程独立的话,会造成调用混乱。
大家平时说的 java 内存分为堆和栈,其实就是为了简便的不太严谨的说法,他们说的栈一般是指虚拟机栈,或者虚拟机栈里面的局部变量表。
局部变量表一般存放着以下数据:
基本数据类型(
boolean
,byte
,char
,short
,int
,float
,long
,double
)对象引用(reference 类型,不一定是对象本身,可能是一个对象起始地址的引用指针,或者一个代表对象的句柄,或者与对象相关的位置)
returAddress(指向了一条字节码指令的地址)
局部变量表内存大小编译期间确定,运行期间不会变化。空间衡量我们叫 Slot(局部变量空间)。64 位的 long 和 double 会占用 2 个 Slot,其他的数据类型占用 1 个 Slot。
上面的方法调用的时候,实际上栈帧是这样的,调用 main()函数的时候,会往虚拟机栈里面放一个栈帧,栈帧里面我们主要关注局部变量表,传入的参数也会当成局部变量,所以第一个局部变量就是参数args
,由于这个是static
方法,也就是类方法,所以不会有当前对象的指针。
如果是普通方法,那么局部变量表里面会多出一个局部变量this
。
如何证明这个东西真的存在呢?我们大概看看字节码,因为局部变量在编译的时候就确定了,运行期不会变化的。下面是IDEA
插件jclasslib
查看的:
上面的图,我们在main()
方法的局部变量表中,确实看到了三个变量:args
,a
,b
。
那在 main()方法里面调用了 swapInt(a, b)呢?
那堆栈里面就会放入swapInt(a,b)
的栈帧,相当于把 a 和 b 局部变量复制了一份,变成下面这样,由于里面一共有三个局部变量:
a:参数
b:参数
temp:函数内临时变量
a 和 b 交换之后,其实swapInt(a,b)
的栈帧变了,a 变为 2,b 变为 1,但是main()
栈帧的 a 和 b 并没有变。
那同样来从字节码看,会发现确实有 3 个局部变量在局部变量表内,并且他们的数值都是 int 类型。
而swap(a,b)
执行结束之后,该方法的堆栈会被弹出虚拟机栈,此时虚拟机栈又剩下main()
方法的栈帧,由于基础数据类型的数值相当于存在局部变量中,swap(a,b)
栈帧中的局部变量不会影响main()
方法的栈帧中的局部变量,所以,就算你在swap(a,b)
中交换了,也不会变。
基础包装数据类型交换
将上面的数据类型换成包装类型,也就是Integer
对象,结果会如何呢?
结果还是一样,交换无效:
这个怎么解释呢?
对象类型已经不是基础数据类型了,局部变量表里面的变量存的不是数值,而是对象的引用了。先用jclasslib
查看一下字节码里面的局部变量表,发现其实和上面差不多,只是描述符变了,从int
变成Integer
。
但是和基础数据类型不同的是,局部变量里面存在的其实是堆里面真实的对象的引用地址,通过这个地址可以找到对象,比如,执行main()
函数的时候,虚拟机栈如下:
假设 a 里面记录的是 1001 ,去堆里面找地址为 1001 的对象,对象里面存了数值 1。b 里面记录的是 1002 ,去堆里面找地址为 1002 的对象,对象里面存了数值 2。
而执行swapInteger(a,b)
的时候,但是还没有交换的时候,相当于把 局部变量复制了一份:
而两者交换之后,其实是SwapInteger(a,b)
栈帧中的 a 里面存的地址引用变了,指向了 b,但是 b 里面的,指向了 a。
而swapInteger()
执行结束之后,其实swapInteger(a,b)
的栈帧会退出虚拟机栈,只留下main()
的栈帧。
这时候,a 其实还是指向 1,b 还是指向 2,因此,交换是没有起效果的。
String,StringBuffer,自定义对象交换
一开始,我以为String
不会变是因为final
修饰的,但是实际上,不变是对的,但是不是这个原因。原因和上面的差不多。
String
是不可变的,只是说堆/常量池内的数据本身不可变。但是引用还是一样的,和上面分析的Integer
一样。
其实StringBuffer
和自定义对象都一样,局部变量表内存在的都是引用,所以交换是不会变化的,因为swap()
函数内的栈帧不会影响调用它的函数的栈帧。
不行我们来测试一下,用事实说话:
执行结果,证明交换确实没有起效果。
总结
基础数据类型交换,栈帧里面存的是局部变量的数值,交换的时候,两个栈帧不会干扰,swap(a,b)
执行完成退出栈帧后,main()
的局部变量表还是以前的,所以不会变。
对象类型交换,栈帧里面存的是对象的地址引用,交换的时候,只是swap(a,b)
的局部变量表的局部变量里面存的引用地址变化了,同样swap(a,b)
执行完成退出栈帧后,main()
的局部变量表还是以前的,所以不会变。
所以不管怎么交换都是不会变的。
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java 源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指 Offer,LeetCode 等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。
平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~
版权声明: 本文为 InfoQ 作者【秦怀杂货店】的原创文章。
原文链接:【http://xie.infoq.cn/article/c836492a69eea42aa85619f90】。文章转载请联系作者。
评论