值传递和引用传递

Lou.Chen
大约 6 分钟

一、堆(heap)和栈(statck)

1、栈(stack)

  • 每个线程运行时所分配的内存为虚拟机栈,jvm为每个线程分配一个虚拟机栈,每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。

  • 每个栈有多个栈帧(Frame)组成,对应着每个方法调用时的所占用的内存(每个运行的方法对应一个栈帧)

  • 每个线程只能有一个活动栈帧(正在执行的那个方法)、对应着当前正在执行的那个方法

  • 栈是一种先进后出的数据结构,每一次方法的调用都对应一次方法的压栈,方法执行完毕后出栈 。有时候方法的递归调用就会出现栈内存溢出(StackOverflowError)

  • 栈中主要存放一些基本数据类型的变量(byte,short,int,long,float,double,boolean,char)和堆中对象引用。

  • 栈的优势是,存取速度比堆快,且数据存储按照一定顺序排列的。每个线程分配一个stack,即stack是线程独占的,即数据是独占的。但缺点是,存放在栈中的数据占用多少内存空间需要在编译时确定下来,缺乏灵活性。

2、堆(heap)

  • 存储的全部是对象实例,每个对象都包含一个与之对应的class的信息(class信息存放在方法区)。

  • jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身,几乎所有的对象实例和数组都在堆中分配。

  • Java的堆是一个运行时数据区,类的对象从堆中分配空间。这些对象通过new等指令建立,通过垃圾回收器来销毁。

  • 堆的优势是可以动态地分配内存空间,需要多少内存空间不必事先告诉编译器,因为它是在运行时动态分配的。但缺点是,由于需要在运行时动态分配内存,所以存取速度较慢。

5、字符串常量池存储在堆还是方法区?

JDK1.7 及之后版本的 JVM 已经将字符串常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

JDK1.8开始,方法区的实现由永久代变成了元空间。

字符串常量池存在堆中:

这也就是有道面试题:String s = new String(“abc”);产生几个对象?答:一个或两个,如果常量池中原来没有”abc”,就是两个。

二、值传递和引用传递

1、什么是值传递?

当一个参数按照值的方式在两个方法之间传递时,调用者和被调用者其实是用的两个不同的变量——被调用者中的变量(原始值)是调用者中变量的一份拷贝,对它们当中的任何一个变量修改都不会影响到另外一个变量。

2、什么是引用传递?

而当一个参数按照引用传递的方式在两个方法之间传递时,调用者和被调用者其实用的是同一个变量,当该变量被修改时,双方都是可见的。

我们都知道数据基本数据类型有八种byte,char,short,int,long,float,double,boolean

基本类型字节数位数最大值最小值
byte1byte8bit2^7-1-2^7
short2byte16bit2^15-1-2^15
int4byte32bit2^31-1-2^31
long8byte64bit2^63-1-2^63
float4byte32bit3.4028235E381.4E - 45
double8byte64bit1.7976931348623157E3084.9E - 324
char2byte16bit2^16 - 10

其中还有一种常用类型 String ,该类型为引用类型

基本类型的变量存储的都是实际的值,而引用类型的变量存储的是对象的引用——指向了对象在内存中的地址。值和引用存储在 stack(栈)中,而对象存储在 heap(堆)中。

之所以有这个区别,是因为:

  • **栈的优势是,存取速度比堆要快,**仅次于直接位于 CPU 中的寄存器。但缺点是,栈中的数据大小与生存周期必须是确定的。
  • 堆的优势是可以动态地分配内存大小,生存周期也不必事先告诉编译器,Java 的垃圾回收器会自动收走那些不再使用的数据。但由于要在运行时动态分配内存存取速度较慢。

3、基本类型的参数传递

众所周知,Java 有 8 种基本数据类型,分别是 int、long、byte、short、float、double 、char 和 boolean。它们的值直接存储在栈中,每当作为参数传递时,都会将原始值(实参)复制一份新的出来,给形参用。形参将会在被调用方法结束时从栈中清除。

请看如下代码:

public class Test {
    public static void main(String[] args) {
        int age=10;
        modify(age);
        System.out.println(age);
    }
    public static void modify(int valueAge) {
        valueAge-=3;
    }
}

1、main方法中的age类型为基本数据,直接将18存储在栈中

2、调用 modify() 方法的时候,将为实参 age 创建一个副本(形参 valueAge),它的值也为 18,不过是在栈中的其他位置。

3、对形参 age 的任何修改都只会影响它自身而不会影响实参。

4、引用类型传递

不多bb,先看代码:

public class Person {

    private String name;
    private Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    //getter 
    //setter
}
Person person;
person=new Person("lc", 20);

person执行 ‘=’ 操作之前,他仅仅只是一个未被初始化的变量而已。那谁是对象呢?

new Person("lc",20),它是对象,存储在堆中;然后 ‘=’ 将对象的引用赋值给了变量person,于是person变量拥有了对象在堆中保存的地址。person将会储存在栈中。

每当该引用类型person当做参数进行传递时,都会创建一个对象引用(实参)的副本(形参),该形参保存的地址和实参的一致,即都指向同一地址空间。

继续看如下代码:

public class Test1 {
    public static void main(String[] args) {
        Person a=new Person("lc", 20);
        Person b=new Person("lc", 20);
        modify(a, b);
        System.out.println(a.getAge());
        System.out.println(b.getAge());
    }

    static void modify(Person a1, Person b1) {
        a1.setAge(10);

        b1 = new Person("lc", 18);
        b1.setAge(25);
    }
}

结果:

10
20

1、在执行modify()之前,a,b所执行的对象不一样,尽管创建的内容一样。

2、在执行modify()方法时,创建的临时变量a1,b1拷贝了实参a,b的引用的地址,即a和a1,b和b1他们指向的是同一地址对象。

3、在modify()方法中,形参a1变量将age改为10,即指向堆中的地址修改age,即变量a所指向的age也为10。

形参b1重新指向了一个对象,修改其age属性为25。此时b和b1所指向的并不是同一个对象。

4、最后modify()指向完毕时,临时变量a1,b1也会销毁。堆中没有没有被引用的对象将会在合适的时机由jvm自动帮我们销毁。