Java 堆、栈、常量池和值传递、引用传递详解

先不要太关注参数到底是值传递还是引用传递,抛开这个想法,先搞清楚Java中值、对象、对象的引用是怎么存储的?


  • 栈:存放8种基本数据类型的变量和对象的引用(对象的引用保存的只是对象本身的地址),对象本身不存放在栈中,而是存放在堆和常量池中。
  • 堆:存放所有new出来的对象或数组。JVM不定时查看堆中的对象,如果没有引用指向这个对象就回收。
  • 常量池:存放字符串常量和基本类型常量(public static final)。

一、基本数据类型

基本数据类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。
以int为例:

int m1 = 7;  
int m2 = 7;  
public static final int m3 = 7;  
public static final int m4 = 7;  

这里写图片描述
基本数据类型跟堆没有任何关系。


二、数组,字符串和其他引用类型

下面的例子对于数组,字符串和其他引用类型都是一样的,只是用字符串举例。
以String为例:
对于字符串:其对象的引用都是存储在栈中,对象本身如果是编译期已经创建好(直接用双引号赋值的:String s1 = “mistra1”; )的就存储在常量池中,如果是运行期(new出来的)就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

String m1 = "mistra";  
String m2 = "mistra";  
String m3 = new String("mistra");  
String m4 = new String("mistra");  

这里写图片描述
String m1 = “mistra”; ,这行代码被执行的时候,JVM首先在常量池中查找是否已经存在了值为”mistra”的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。

通过new产生一个字符串(假设为”mistra”)时,会先去常量池中查找是否已经有了”mistra”对象,如果没有则在常量池中创建一个此字符串对象,然后在堆中再创建一个常量池中此”mistra”对象的拷贝对象。
String s = new String(“mistra”);产生几个对象?一个或两个,如果常量池中原来没有”mistra”,就是两个。

  • 对于成员变量和局部变量:成员变量就是在方法外部,类的内部定义的变量;局部变量就是在方法或语句块内部定义的变量。局部变量必须初始化。 形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。 成员变量存储在堆中的对象里面,由垃圾回收器负责回收。

三、到底是值传递还是引用传递?

基本数据类型:值就直接保存在变量中。
引用类型:变量中保存的只是实际对象的地址。这里的变量就是对象的引用,引用指向实际对象,实际对象中保存着内容。

|>>>>>赋值运算符(=)的作用
赋值运算符对基本数据类型和引用类型的作用看下图就可以明白:
这里写图片描述
对于基本数据类型num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象(“mistra”)不会被改变。如上图所示,”mistra” 字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)

1、基本数据类型:
public class ParameterTransferTest1 {
    public static void main(String[] args) {
        int a = 3;
        change(a);
        System.out.println(a);
    }
    public static void change(int i) {
        i =10;
    }
}
输出结果:3

基本数据类型是值传递,传递的只是一个副本,与原值无关。 相当于把a的值3 copy给了 i,i 改变值并不会影响a的值。

2、没有提供改变自身方法的引用类型:

这里涉及到String与StringBuilder的知识点了。

public class ParameterTransferTest2 {
    public static void main(String [] args){
        String str = "a";
        change(str);
        System.out.println(str);
    }
    public static void change(String i) {
        i = "b";
    }
}
输出结果:a

每次改变String对象的值都是新创建了一个对象,原对象还在。而StringBuilder对象不同,每次改变都是在对象本身改变,不创建新对象。
假设str指向了地址x010,把这个地址引用传递给了 i,i 也指向x010,但是执行 i = “b”;之后,i 指向了新地址x011,并不影响str指向的地址,所以没有改变。

扫描二维码关注公众号,回复: 934392 查看本文章
3、提供了改变自身方法的引用类型:
public class ParameterTransferTest3 {
    public static void main(String [] args){
        StringBuilder sb = new StringBuilder("a");
        change(sb);
        System.out.println(sb);
    }
    public static void change(StringBuilder i) {
        i.append("b");
    }
}
输出结果:ab

假设sb指向了地址x010,把这个地址引用传递给了 i,i 也指向x010,但是执行 i.append(“b”);之后,i 指向的地址不变,还是指向x010,所以影响了sb的值。


这里放几道例题加深理解:例题原博客地址

public class foo {
    public static void main(String sgf[]) {
        StringBuffer a=new StringBuffer(“A”);
        StringBuffer b=new StringBuffer(“B”);
        operate(a,b);
        System.out.println(a+”.”+b);
    }

    static void operate(StringBuffer x,StringBuffer y) {
        x.append(y);
        y=x;
    }
}

这里写图片描述

public class Example {    
    String str = "abc";    
    char[] ch = { 'a', 'b', 'c' };    
    public static void main(String args[]) {    
        Example ex = new Example();    
        ex.change(ex.str, ex.ch);    
        System.out.println(ex.str);    
        System.out.println(ex.ch);    
    }    
    public void change(String str, char ch[]) {  
   *** //  ch = new char[]{'a','b','c'};    
        str = "change";    
        ch[0] = 'c';    
    }    
} 
输出结果:
abc
cba

变量ch和引用传递是一样的,都是操作的为同一对象,如果有* * *的一行不注释的话,结果就为
abc
abc

猜你喜欢

转载自blog.csdn.net/axela30w/article/details/79167361
今日推荐