从JVM底层原理分析数值交换那些事
基础数据类型交换
这个话题,需要从最最基础的一道题目说起,看题目:以下代码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等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。
平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~
从JVM底层原理分析数值交换那些事的更多相关文章
- JVM底层原理及调优之笔记一
JVM底层原理及调优 1.java虚拟机内存模型(JVM内存模型) 1.堆(-Xms -Xmx -Xmn) java堆,也称为GC堆,是JVM中所管理的内存中最大的一块内存区域,是线程共享的,在JVM ...
- 艾编程coding老师:深入JVM底层原理与性能调优
1. Java内存模型JMM,内存泄漏及解决方法:2. JVM内存划分:New.Tenured.Perm:3. 垃圾回收算法:Serial算法.并行算法.并发算法:4. JVM性能调优,CPU负载不足 ...
- synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解
本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁 ...
- 并发之volatile底层原理
15.深入分析Volatile的实现原理 14.java多线程编程底层原理剖析以及volatile原理 13.Java中Volatile底层原理与应用 12.Java多线程-java.util.con ...
- JVM 内部原理(六)— Java 字节码基础之一
JVM 内部原理(六)- Java 字节码基础之一 介绍 版本:Java SE 7 为什么需要了解 Java 字节码? 无论你是一名 Java 开发者.架构师.CxO 还是智能手机的普通用户,Java ...
- 10分钟看懂, Java NIO 底层原理
目录 写在前面 1.1. Java IO读写原理 1.1.1. 内核缓冲与进程缓冲区 1.1.2. java IO读写的底层流程 1.2. 四种主要的IO模型 1.3. 同步阻塞IO(Blocking ...
- 字节码技术---------动态代理,lombok插件底层原理。类加载器
字节码技术应用场景 AOP技术.Lombok去除重复代码插件.动态修改class文件等 字节技术优势 Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用 ...
- (前篇:NIO系列 推荐阅读) Java NIO 底层原理
出处: Java NIO 底层原理 目录 1.1. Java IO读写原理 1.1.1. 内核缓冲与进程缓冲区 1.1.2. java IO读写的底层流程 1.2. 四种主要的IO模型 1.3. 同步 ...
- springAop:Aop(Xml)配置,Aop注解配置,spring_Aop综合案例,Aop底层原理分析
知识点梳理 课堂讲义 0)回顾Spring体系结构 Spring的两个核心:IoC和AOP 1)AOP简介 1.1)OOP开发思路 OOP规定程序开发以类为模型,一切围绕对象进行,OOP中完成某个任务 ...
随机推荐
- 【uva 11054】Wine trading in Gergovia(算法效率--等价转换)
题意:N个等距村庄,买(>0)卖(<0)酒,供需平衡,运K则需K劳动力.问所需的最小劳动力. 解法:由于运出或运入1的都需经过2,所以无论如何,都可以等价于从2本身运入或运出.因此可以将1 ...
- [IOI1998] Polygon (区间dp,和石子合并很相似)
题意: 给你一个多边形(可以看作n个顶点,n-1条边的图),每一条边上有一个符号(+号或者*号),这个多边形有n个顶点,每一个顶点有一个值 最初你可以把一条边删除掉,这个时候这就是一个n个顶点,n-2 ...
- Codeforces Round #681 (Div. 1, based on VK Cup 2019-2020 - Final) B. Identify the Operations (模拟,双向链表)
题意:给你一组不重复的序列\(a\),每次可以选择一个数删除它左边或右边的一个数,并将选择的数append到数组\(b\)中,现在给你数组\(b\),问有多少种方案数得到\(b\). 题解:我们可以记 ...
- Codeforces Round #669 (Div. 2) A. Ahahahahahahahaha (构造)
题意:有一个长度为偶数只含\(0\)和\(1\)的序列,你可以移除最多\(\frac{n}{2}\)个位置的元素,使得操作后奇数位置的元素和等于偶数位置的元素和,求新序列. 题解:统计\(0\)和\( ...
- MySQL——时间、字符串、时间戳相互转换
一.时间转字符串 select data_format(now(),'%Y-%m-%d %H:%i:%s'); 二.时间转时间戳 select unix_timestamp(now()); 三.字符串 ...
- VMware虚拟化与Kubernetes(K8s)类比阐述-适合VMware用户
概述 容器技术是最近几年非常热门的技术,它似乎就是为云端的应用量身定制的,所以它也被贴上了云原生应用 (Cloud Native Application) 技术的标签.目前最为流行的容器管理调度平台是 ...
- 一句话木马的简单例子 网站webshell & 远程连接
一 概述 本地 kail linux 目标 windows nt 服务器 二 过程 首先编写一句话木马 index.php 一句话木马的原理就是把C=xxx 字符串当成php语句执行 注意这里用 ...
- openssl的用法
Openssl详细用法: OpenSSL 是一个开源项目,其组成主要包括一下三个组件: openssl:多用途的命令行工具 libcrypto:加密算法库 libssl:加密模块应用库,实现了ssl及 ...
- 正则表达式: javascript Unicode 中文字符 编码区间:\u4e00-\u9fa5
正则表达式: javascript Unicode 中文字符 编码区间:\u4e00-\u9fa5 RegExp 对象 javascript Unicode 中文字符的 编码区间: \u4e00-\ ...
- 1GB === 1000MB & 1GB === 1024MB
1GB === 1000MB & 1GB === 1024MB 字节单位换算 1 Gigabyte = 1000 Megabytes 1 Gibibyte = 1024 Mebibytes 十 ...