原文鏈接:https://juejin.im/post/6844903696996941832
1、形參與實參
- 形參:方法被調用時需要傳遞進來的參數,如:func(int a)中的a,它只有在func被調用期間a才有意義,也就是會被分配內存空間,在方法func執行完成后,a就會被銷毀釋放空間,也就是不存在了
- 實參:方法被調用時是傳入的實際值,它在方法被調用前就已經被初始化並且在方法被調用時傳入。
1 public static void func(int a){ 2 a=20; 3 System.out.println(a); 4 } 5 public static void main(String[] args) { 6 int a=10;//實參 7 func(a); 8 }
int a=10;中的a在被調用之前就已經創建並初始化,在調用func方法時,他被當做參數傳入,所以這個a是實參。
而func(int a)中的a只有在func被調用時它的生命周期才開始,而在func調用結束之后,它也隨之被JVM釋放掉,,所以這個a是形參。
2、Java的數據類型
所謂數據類型,是編程語言中對內存的一種抽象表達方式,我們知道程序是由代碼文件和靜態資源組成,在程序被運行前,這些代碼存在在硬盤里,程序開始運行,這些代碼會被轉成計算機能識別的內容放到內存中被執行。
因此,數據類型實質上是用來定義編程語言中相同類型的數據的存儲形式,也就是決定了如何將代表這些值的位存儲到計算機的內存中。所以,數據在內存中的存儲,是根據數據類型來划定存儲形式和存儲位置的。
Java數據類型有基本類型和引用類型兩大類:
- 基本類型:編程語言中內置的最小粒度的數據類型。它包括四大類八種類型:
4種整數類型:byte、short、int、long
2種浮點數類型:float、double
1種字符類型:char
1種布爾類型:boolean
- 引用類型:引用也叫句柄,引用類型,是編程語言中定義的在句柄中存放着實際內容所在地址的地址值的一種數據形式。它主要包括:
類
接口
數組
3、JVM內存划分及職能

由圖可以看出:Java代碼被編譯器編譯成字節碼之后,JVM開辟一片內存空間(也叫運行時數據區),通過類加載器加到到運行時數據區來存儲程序執行期間需要用到的數據和相關信息,在這個數據區中,它由以下幾部分組成:
- 虛擬機棧
- 堆
- 程序計數器
- 方法區
- 本地方法棧
接着來了解一下每部分的原理以及具體用來存儲程序執行過程中的哪些數據。
1.虛擬機棧
虛擬機棧是Java方法執行的內存模型,棧中存放着棧幀,每個棧幀分別對應一個被調用的方法,方法被調用的過程對應棧幀在虛擬機中入棧到出棧的過程。
棧是線程私有的,也就是線程之間的棧是隔離的;當程序中某個線程開始執行一個方法時就會相應的創建一個棧幀並且入棧(位於棧頂),在方法結束后,棧幀出棧。下圖表示了一個Java棧的模型以及棧幀的組成:
棧幀:是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧的棧元素。
每個棧幀中包括:
- 局部變量表:用來存儲方法中的局部變量(非靜態變量、函數形參)。當變量為基本數據類型時,直接存儲值,當變量為引用類型時,存儲的是指向具體對象的引用。
- 操作數棧:Java虛擬機的解釋執行引擎被稱為"基於棧的執行引擎",其中所指的棧就是指操作數棧。
- 指向運行時常量池的引用:存儲程序執行時可能用到常量的引用。
- 方法返回地址:存儲方法執行完成后的返回地址
方法區是一塊所有線程共享的內存邏輯區域,在JVM中只有一個方法區,用來存儲一些線程可共享的內容,它是線程安全的,多個線程同時訪問方法區中同一個內容時,只能有一個線程裝載該數據,其它線程只能等待。
方法區可存儲的內容有:類的全路徑名、類的直接超類的權全限定名、類的訪問修飾符、類的類型(類或接口)、類的直接接口全限定名的有序列表、常量池(字段,方法信息,靜態變量,類型引用(class))等。
4.本地方法棧
本地方法棧的功能和虛擬機棧是基本一致的,並且也是線程私有的,它們的區別在於虛擬機棧是為執行Java方法服務的,而本地方法棧是為執行本地方法服務的。
5.程序技術器
線程私有的。
記錄着當前線程所執行的字節碼的行號指示器,在程序運行過程中,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、異常處理、線程恢復等基礎功能都需要依賴計數器完成。
這里要分以下的情況進行探究:
1. 基本數據類型的存儲:
- A. 基本數據類型的局部變量:方法內定義的變量
- B. 基本數據類型的成員變量(aka.實例變量):類范圍內、方法之外定義的變量
- C. 基本數據類型的靜態變量(aka.類變量):類范圍內、方法之外定義的變量
2. 引用數據類型的存儲
A.基本數據類型的局部變量(方法內定義的變量)
- 定義基本數據類型的局部變量以及數據都是直接存儲在內存中的棧上,也就是前面說到的“虛擬機棧”,數據本身的值就是存儲在棧空間里面。
如上圖,在方法內定義的變量直接存儲在棧中,如:
1 int age = 50; 2 int weight = 50; 3 int grade = 6;
當我們寫“int age=50;”,其實是分為兩步的:
1 int age;//定義變量 2 age=50;//賦值
首先JVM創建一個名為age的變量,存於局部變量表中,然后去棧中查找是否存在有字面量值為50的內容,如果有就直接把age指向這個地址,如果沒有,JVM會在棧中開辟一塊空間來存儲“50”這個內容,並且把age指向這個地址。因此我們可以知道:我們聲明並初始化基本數據類型的局部變量時,變量名以及字面量值都是存儲在棧中,而且是真實的內容。
我們再來看“int weight=50;”,按照剛才的思路:字面量為50的內容在棧中已經存在,因此weight是直接指向這個地址的。由此可見:棧中的數據在當前線程下是共享的。那么如果再執行下面的代碼呢?
1 weight = 40;
當代碼中重新給weight變量進行賦值時,JVM會去棧中尋找字面量為40的內容,發現沒有,就會開辟一塊內存空間存儲40這個內容,並且把weight指向這個地址。由此可知:
基本數據類型的數據本身是不會改變的,當局部變量重新賦值時,並不是在內存中改變字面量內容,而是重新在棧中尋找已存在的相同的數據,若棧中不存在,則重新開辟內存存新數據,並把要重新賦值的局部變量的引用指向新數據所在地址。
B. 基本數據類型的成員變量
成員變量:顧名思義,就是在類體中定義的變量。看下圖:

我們看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();
同樣是局部變量的age、name、grade卻被存儲到了堆中為per對象開辟的一塊空間中。因此可知:基本數據類型的成員變量名和值都存儲於堆中,其生命周期和對象的是一致的
C. 基本數據類型的靜態變量
前面提到方法區用來存儲一些共享數據,因此基本數據類型的靜態變量名以及值存儲於方法區的運行時常量池中,靜態變量隨類加載而加載,隨類消失而消失。
2. 引用數據類型的存儲Person per = new Person();
實際上,它也是有兩個過程:
1 Person per;//定義變量 2 per=new Person();//賦值
對於引用數據類型的對象/數組,變量名存在棧中,變量值存儲的是對象的地址,並不是對象的實際內容,實際內容存儲在堆中。
在方法被調用時,實參通過形參把它的內容副本傳入方法內部,此時形參接收到的內容是實參值的一個拷貝,因此在方法內對形參的任何操作,都僅僅是對這個副本的操作,不影響原始值的內容。
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); }
輸出結果:
1 傳入的age:25 2 傳入的weight:77.5 3 4 方法內重新賦值后的age:33 5 方法內重新賦值后的weight:89.5 6 7 方法執行后的age:25 8 方法執行后的weight:77.5
從上面的打印結果可以看到:
a和w作為實參傳入valueCrossTest之后,無論在方法內做了什么操作,最終a和w都沒變化。
這是什么原因呢?!!
下面我們根據上面學到的知識點,進行詳細的分析:
首先程序運行時,調用mian()方法,此時JVM為main()方法往虛擬機棧中壓入一個棧幀,即為當前棧幀,用來存放main()中的局部變量表(包括參數)、操作棧、方法出口等信息,如a和w都是main()方法中的局部變量,因此可以斷定,a和w是躺在main方法所在的棧幀中
如圖:

而當執行到valueCrossTest()方法時,JVM也為其往虛擬機棧中壓入一個棧,即為當前棧幀,用來存放valueCrossTest()中的局部變量等信息,因此age和weight是躺在valueCrossTest方法所在的棧幀中,而他們的值是從a和w的值copy了一份副本而得,如圖:

因而可以a和age、w和weight對應的內容是不一致的,所以當在方法內重新賦值時,實際流程如圖:
也就是說,age和weight的改動,只是改變了當前棧幀(valueCrossTest方法所在棧幀)里的內容,當方法執行結束之后,這些局部變量都會被銷毀,mian方法所在棧幀重新回到棧頂,成為當前棧幀,再次輸出a和w時,依然是初始化時的內容。因此:值傳遞傳遞的是真實內容的一個副本,對副本的操作不影響原內容,也就是形參怎么變化,不會影響實參對應的內容。
引用傳遞:
先定義一個對象:
1 public class Person { 2 private String name; 3 private int age; 4 public String getName() { 5 return name; 6 } 7 public void setName(String name) { 8 this.name = name; 9 } 10 public int getAge() { 11 return age; 12 } 13 public void setAge(int age) { 14 this.age = age; 15 } 16 }
寫個函數測試一下:
public static void PersonCrossTest(Person person){
System.out.println("傳入的person的name:"+person.getName()); person.setName("我是張小龍"); System.out.println("方法內重新賦值后的name:"+person.getName()); } //測試 public static void main(String[] args) { Person p=new Person(); p.setName("我是馬化騰"); p.setAge(45); PersonCrossTest(p); System.out.println("方法執行后的name:"+p.getName()); }
輸出結果:
1 傳入的person的name:我是馬化騰 2 方法內重新賦值后的name:我是張小龍 3 方法執行后的name:我是張小龍
可以看出,person經過personCrossTest()方法的執行之后,內容發生了改變,這印證了上面所說的“引用傳遞”,對形參的操作,改變了實際對象的內容。
那么,到這里就結題了嗎?
不是的,沒那么簡單,
能看得到想要的效果
是因為剛好選對了例子而已!!!
下面我們對上面的例子稍作修改,加上一行代碼:
public static void PersonCrossTest(Person person){ System.out.println("傳入的person的name:"+person.getName()); person=new Person();//加多此行代碼 person.setName("我是張小龍"); System.out.println("方法內重新賦值后的name:"+person.getName()); }
輸出結果:
1 傳入的person的name:我是馬化騰 2 方法內重新賦值后的name:我是張小龍 3 方法執行后的name:我是馬化騰
為什么這次的輸出和上次的不一樣了呢?
看出什么問題了嗎?
按照上面講到JVM內存模型可以知道,對象和數組是存儲在Java堆區的,而且堆區是共享的,因此程序執行到main()方法中的下列代碼時
Person p=new Person(); p.setName("我是馬化騰"); p.setAge(45); PersonCrossTest(p);
JVM會在堆內開辟一塊內存,用來存儲p對象的所有內容,同時在main()方法所在線程的棧區中創建一個引用p存儲堆區中p對象的真實地址,如圖:
當執行到PersonCrossTest()方法時,因為方法內有這么一行代碼:
person=new Person();
JVM需要在堆內另外開辟一塊內存來存儲new Person(),假如地址為“xo3333”,那此時形參person指向了這個地址,假如真的是引用傳遞,那么由上面講到:引用傳遞中形參實參指向同一個對象,形參的操作會改變實參對象的改變。
可以推出:實參也應該指向了新創建的person對象的地址,所以在執行PersonCrossTest()結束之后,最終輸出的應該是后面創建的對象內容。
然而實際上,最終的輸出結果卻跟我們推測的不一樣,最終輸出的仍然是一開始創建的對象的內容。
由此可見:引用傳遞,在Java中並不存在。
但是有人會疑問:為什么第一個例子中,在方法內修改了形參的內容,會導致原始對象的內容發生改變呢?
這是因為:無論是基本類型和是引用類型,在實參傳入形參時,都是值傳遞,也就是說傳遞的都是一個副本,而不是內容本身。

由圖可以看出,方法內的形參person和實參p並無實質關聯,它只是由p處copy了一份指向對象的地址,此時:p和person都是指向同一個對象。
因此在第一個例子中,對形參p的操作,會影響到實參對應的對象內容。而在第二個例子中,當執行到new Person()之后,JVM在堆內開辟一塊空間存儲新對象,並且把person改成指向新對象的地址,此時:
p依舊是指向舊對象的地址,person指向新對象的地址。
所以此時對person的操作,實際上是對新對象的操作,與實參p中對應的對象毫無關系。
結語
因此可見:在Java中所有的參數傳遞,不管基本類型還是引用類型,都是值傳遞,或者說是副本傳遞。
只是在傳遞過程中:
如果是對基本數據類型的數據進行操作,由於原始內容和副本都是存儲實際值,並且是在不同的棧區,因此形參的操作,不影響原始內容。
如果是對引用類型的數據進行操作,分兩種情況,一種是形參和實參保持指向同一個對象地址,則形參的操作,會影響實參指向的對象的內容。一種是形參被改動指向新的對象地址(如重新賦值引用),則形參的操作,不會影響實參指向的對象的內容。