共计 4040 个字符,预计需要花费 11 分钟才能阅读完成。
根底数据类型替换
这个话题,须要从最最根底的一道题目说起,看题目:以下代码 a 和 b 的值会替换么:
public static void main(String[] args) {
int a = 1, b = 2;
swapInt(a, b);
System.out.println("a=" + a + ", b=" + b);
}
private static void swapInt(int a, int b) {
int temp = a;
a = b;
b = temp;
}
后果预计大家都晓得,a 和 b 并没有替换:
integerA=1 , integerB=2
然而起因呢?先看这张图,先来说说 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
对象, 后果会如何呢?
public static void main(String[] args) {
Integer a = 1, b = 2;
swapInteger(a, b);
System.out.println("a=" + a + ", b=" + b);
}
private static void swapInteger(Integer a, Integer b) {
Integer temp = a;
a = b;
b = temp;
}
后果还是一样,替换有效:
a=1 , b=2
这个怎么解释呢?
对象类型曾经不是根底数据类型了,局部变量表外面的变量存的不是数值,而是对象的援用了。先用 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()
函数内的栈帧不会影响调用它的函数的栈帧。
不行咱们来测试一下,用事实谈话:
public static void main(String[] args) {String a = new String("1"), b = new String("2");
swapString(a, b);
System.out.println("a=" + a + ", b=" + b);
StringBuffer stringBuffer1 = new StringBuffer("1"), stringBuffer2 = new StringBuffer("2");
swapStringBuffer(stringBuffer1, stringBuffer2);
System.out.println("stringBuffer1=" + stringBuffer1 + ", stringBuffer2=" + stringBuffer2);
Person person1 = new Person("person1");
Person person2 = new Person("person2");
swapObject(person1,person2);
System.out.println("person1=" + person1 + ", person2=" + person2);
}
private static void swapString(String s1,String s2){
String temp = s1;
s1 = s2;
s2 = temp;
}
private static void swapStringBuffer(StringBuffer s1,StringBuffer s2){
StringBuffer temp = s1;
s1 = s2;
s2 = temp;
}
private static void swapObject(Person p1,Person p2){
Person temp = p1;
p1 = p2;
p2 = temp;
}
class Person{
String name;
public Person(String name){this.name = name;}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
执行后果, 证实替换的确没有起成果。
a=1 , b=2
stringBuffer1=1 , stringBuffer2=2
person1=Person{name='person1'} , person2=Person{name='person2'}
总结
根底数据类型替换,栈帧外面存的是局部变量的数值,替换的时候,两个栈帧不会烦扰,swap(a,b)
执行实现退出栈帧后,main()
的局部变量表还是以前的,所以不会变。
对象类型替换,栈帧外面存的是对象的地址援用,替换的时候,只是 swap(a,b)
的局部变量表的局部变量外面存的援用地址变动了,同样 swap(a,b)
执行实现退出栈帧后,main()
的局部变量表还是以前的,所以不会变。
所以不管怎么替换都是不会变的。
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使迟缓,驰而不息。集体写作方向:Java 源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指 Offer,LeetCode 等,认真写好每一篇文章,不喜爱题目党,不喜爱花里胡哨,大多写系列文章,不能保障我写的都完全正确,然而我保障所写的均通过实际或者查找材料。脱漏或者谬误之处,还望斧正。
2020 年我写了什么?
开源编程笔记
素日工夫贵重,只能应用早晨以及周末工夫学习写作,关注我,咱们一起成长吧~