乐趣区

关于jvm:从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,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 年我写了什么?

开源编程笔记

素日工夫贵重,只能应用早晨以及周末工夫学习写作,关注我,咱们一起成长吧~

退出移动版