美文网首页
Java的JVM介绍以及java的值传递和引用传递

Java的JVM介绍以及java的值传递和引用传递

作者: JasonChen8888 | 来源:发表于2020-05-07 17:36 被阅读0次

    背景

    面试的时候碰到的了一个java基础问题,竟然给问蒙了,回来之后感觉针对这个问题总结一下

    java中 值的传递和引用传递

    这边再将具体的值传递和引用传递,之前先普及一下基本知识

    数据类型

    Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。

    基本类型 byte, short, int, long, char, float, double, Boolean
    引用类型 类,接口和数组

    有了数据类型,JVM对程序数据的管理就规范化了,不同的数据类型,它的存储形式和位置是不一样的,要想知道JVM是怎么存储各种类型的数据,就得先了解JVM的内存划分以及每部分的职能。

    JVM内存的划分及职能

    Java语言本身是不能操作内存的,它的一切都是交给JVM来管理和控制的,因此Java内存区域的划分也就是JVM的区域划分,在说JVM的内存划分之前,我们先来看一下Java程序的执行过程,如下图:


    JVM结构图.png

    上图可以看出:Java代码被编译器编译成字节码之后,JVM开辟一片内存空间(也叫运行时数据区),通过类加载器加到到运行时数据区来存储程序执行期间需要用到的数据和相关信息。

    JVM的成员:
    1. 虚拟机栈
    2. 程序计数器
    3. 方法区
    4. 本地方法栈
    • 虚拟机栈
      虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。

    栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。


    Java栈的模型以及栈帧结构图.png

    栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
    每个栈帧中包括:
    1、 局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。
    2、 操作数栈:Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。
    3、指向运行时常量池的引用:存储程序执行时可能用到常量的引用。
    4、方法返回地址:存储方法执行完成后的返回地址。
    相关的异常
    StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
    OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。


    • 堆是用来存储对象本身和数组的,在JVM中只有一个堆,因此,堆是被所有线程共享的。

    • 方法区

    1. 方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。
    2. 方法区可存储的内容有:类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class))等
    • 本地方法栈
      区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

    • 程序计数器
      内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

    java数据类型在JVM的位置
    • 局部变量的存储位置
      在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因。
      在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。
      (1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中。
      (2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。
      如图:


      局部变量在栈表现.png
    • 全局变量的存储位置
      在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
      同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量
      (1)当声明的是基本类型的变量其变量名及其值放在堆内存中的
      (2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中


      image.png

      图中per的地址指向的是堆内存中的一块区域

    public class Person{
       private int age;
       private String name;
       private int grade;
     //篇幅较长,省略setter getter方法
       static void run(){
          System.out.println("run...."); 
        };
     }
    
    //调用
    Person per=new Person();
    
    • 基本数据类型的静态变量的存储位置
      方法区用来存储一些共享数据,因此基本数据类型的静态变量名以及值存储于方法区的运行时常量池中,静态变量随类加载而加载,随类消失而消失。
    说说堆和栈的关系
    1. 栈是运行时的单位,而堆是存储的单位。
    2. 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
    3. 在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有 线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对 象信息。

    java的值的引用在堆和栈的表现

    在方法中有 Print p1 = new Print(0, 0);


    image.png

    变量p1里存储着实际对象的地址,一般称这种变量为"引用",引用指向实际对象,我们称实际对象为该引用的值;赋值操作符=实际上做的就是将引用指向值的地址的工作,如果我们有p1 = new Print(3,3); 的话,情形就是这样:


    image.png
    要注意到,在堆中的对象Print(0,0) 并没有发生改变,改变的只有引用p1 指向的地址。

    java的中值的传递和引用传递

    值传递

    在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。

    • 举个栗子:
    public static void valueCrossTest(int age,float weight){
        System.out.println("传入的age:"+age);
        System.out.println("传入的weight:"+weight);
        age=33;
        weight=89.5f;
        System.out.println("方法内重新赋值后的age:"+age);
        System.out.println("方法内重新赋值后的weight:"+weight);
     }
    
    public static void main(String[] args) {
          int a=25;
          float w=77.5f;
          valueCrossTest(a,w);
          System.out.println("方法执行后的age:"+a);
          System.out.println("方法执行后的weight:"+w);
    }
    

    输出结果:
    传入的age:25
    传入的weight:77.5
    方法内重新赋值后的age:33
    方法内重新赋值后的weight:89.5
    方法执行后的age:25
    方法执行后的weight:77.5
    从上面输出结果可以看出:a和w作为实参传入valueCrossTest之后,无论在方法内做了什么操作,最终a和w都没变化。

    • 结论:
      age和weight的改动,只是改变了当前栈帧(valueCrossTest方法所在栈帧)里的内容,当方法执行结束之后,这些局部变量都会被销毁,mian方法所在栈帧重新回到栈顶,成为当前栈帧,再次输出a和w时,依然是初始化时的内容。
      因此:
      值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。
    引用传递

    在方法调用时,传入方法内部的是实参引用的拷贝,因此对形参的任何操作都不会影响到实参。

    • 举个栗子
    public class Print {
        private int x;
        private int y;
    
        public Print(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        public void setLocation(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        private static void modifyPrint(Print p1, Print p2) {
            Print tmpPrint = p1;
            p1 = p2;
            p2 = tmpPrint;
            p1.setLocation(5, 5);
            p2 = new Print(5, 5);
        }
    
        public static void main(String[] args) {
            Print p1 = new Print(0, 0);
            Print p2 = new Print(0, 0);
            modifyPrint(p1, p2);
            System.out.println("[" + p1.x + "," + p1.y + "],[" + p2.x + "," + p2.y + "]");
        }
    
    }
    

    运行结果: [0,0],[5,5] 这个结果是不是很意外,不是想象中的[5,5],[5,5]
    在modifyPrint()方法中下面的代码

    Point tmpPoint = p1;
     p1 = p2;
     p2 = tmpPoint;
    

    可以理解在方法内部有形参p1'=实参p1,形参p2'=实参p2,这样我们在方法里操作的p1'和p2'实际只是个临时变量,它的生命周期仅限于方法里。


    image.png

    然后调用了p1'.setLocation(5, 5); 如果实例对象本身提供了改变自身的方法,那么在形参调用该方法后也会改变实参的,因为它们都指向了同一个实例,所以这时实参p2 也变为Point[5.5] 。
    代码走到下一行p2' = new Point(5, 5); 基于整篇文章的论述,这一行我们可以直接跳过不管了,因为形参的重赋值操作不会影响到实参。最后的堆栈信息如下:


    image.png
    所以最终答案显而易见:[0,0],[5,5]
    • 再看一个栗子:
     public static void main(String[] args) {
            List<String> colorList = new ArrayList<>();
            colorList.add("BLUE");
            colorList.add("RED");
            colorList.add("GRAY");
            System.out.println(colorList);
            removeFirst(colorList);
            System.out.println(colorList);
        }
    
        private static void removeFirst(List colorList) {
            if (!colorList.isEmpty()) colorList.remove(0);
        }
    
    // 输出的结果
    // [BLUE, RED, GRAY]
    // [RED, GRAY]
    

    上面代码中,调用removeFirst方法,传进入的参数List,方法内部调用的是实例对象本身提供了改变自身的方法remove(int index),那么在形参调用该方法后也会改变实参的,因为它们都指向了同一个实例List, 所以colorlist的内容变为[RED, GRAY], 该在栈和堆的表现,如下:


    image.png

    如果调整上面的代码: 修改removeFirst的方法内容改为下面的代码:

     private static void removeFirst(List colorList) {
            //if (!colorList.isEmpty()) colorList.remove(0);
            if (!colorList.isEmpty())
                colorList = colorList.subList(1, colorList.size());
        }
        
        //再次运行的结果为:
        //[BLUE, RED, GRAY]
        //[BLUE, RED, GRAY]
    

    colorList.subList(1, colorList.size()); 虽然也是实例对象本身提供的方法,但是该方法是返回的是一个new的新对象。相当于colorList = new ArrayList(); 相当于在方法内创建一个新的对象,形参colorList' 该对象声明周期,只在removeFirst有效,当方法结束,形参colorList' 被回收,原来的实参还是不变。


    image.png

    ArrayList类中subList方法的源码:SubList也是实现了List接口

        public List<E> subList(int fromIndex, int toIndex) {
            subListRangeCheck(fromIndex, toIndex, size);
            return new SubList(this, 0, fromIndex, toIndex);
        }
    

    因此:值传递(Call by value)和引用传递(Call by reference),描述的是函数调用时参数的求值策略(Evaluation strategy),是对调用函数时,求值和取值方式的描述,而非传递的内容。

    参考文献

    https://my.oschina.net/u/1455908/blog/392009
    https://www.cnblogs.com/lfxiao/p/10599546.html
    https://blog.csdn.net/bntx2jsqfehy7/article/details/83508006
    https://blog.csdn.net/hongzhen91/article/details/90666066
    https://www.cnblogs.com/zwbg/p/6194470.html

    相关文章

      网友评论

          本文标题:Java的JVM介绍以及java的值传递和引用传递

          本文链接:https://www.haomeiwen.com/subject/mocaghtx.html