从JVM底层原理分析数值交换那些事

基础数据类型交换

这个话题,须要从最最基础的一道题目提及,看题目:如下代码a和b的值会交换么:java

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并无交换:git

integerA=1 , integerB=2

可是缘由呢?先看这张图,先来讲说Java虚拟机的结构:
github

运行时区域主要分为:redis

  • 线程私有:
    • 程序计数器:Program Count Register,线程私有,没有垃圾回收
    • 虚拟机栈:VM Stack,线程私有,没有垃圾回收
    • 本地方法栈:Native Method Stack,线程私有,没有垃圾回收
  • 线程共享:
    • 方法区:Method Area,以HotSpot为例,JDK1.8后元空间取代方法区,有垃圾回收。
    • 堆:Heap,垃圾回收最重要的地方。

和这个代码相关的主要是虚拟机栈,也叫方法栈,是每个线程私有的。
生命周期和线程同样,主要是记录该线程Java方法执行的内存模型。虚拟机栈里面放着好多栈帧注意虚拟机栈,对应是Java方法,不包括本地方法。shell

一个Java方法执行会建立一个栈帧,一个栈帧主要存储:编程

  • 局部变量表
  • 操做数栈
  • 动态连接
  • 方法出口
    每个方法调用的时候,就至关于将一个栈帧放到虚拟机栈中(入栈),方法执行完成的时候,就是对应着将该栈帧从虚拟机栈中弹出(出栈)。

每个线程有一个本身的虚拟机栈,这样就不会混起来,若是不是线程独立的话,会形成调用混乱。分布式

你们平时说的java内存分为堆和栈,其实就是为了简便的不太严谨的说法,他们说的栈通常是指虚拟机栈,或者虚拟机栈里面的局部变量表。ide

局部变量表通常存放着如下数据:函数

  • 基本数据类型(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,ab

那在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年我写了什么?

开源编程笔记

平日时间宝贵,只能使用晚上以及周末时间学习写做,关注我,咱们一块儿成长吧~

相关文章
相关标签/搜索