原創博文,轉載請注明出處。謝謝~~
java程序運行時,其對象是怎么進行放置和安排的呢?內存是怎么分配的呢?理解好這個很有好處!java有5個地方可以存儲數據:
1、寄存器。這是最快的存儲區,位於處理器內部。java程序員無法感知到它的存在,所以不用深究。
2、堆棧。位於內存中,通過堆棧指針可以獲得直接的內存分配。對象引用和基本數據(int,boolean等)存放於堆棧中。注意:是對象的引用,對象數據本身卻是存放在堆中的。於對象而言,在堆中存放的只是對象數據的一個地址(類似於C語言的指針),通過它可以訪問到對象本身的屬性和方法。
3、堆。同樣位於內存中,用於存放所有的java對象。需要一個對象的時候,只需要用new寫一行簡單的代碼,當執行這行代碼的時候,會直接在堆中進行空間分配。
4、常量存儲。常量值直接存放在代碼內部,保證安全。
5、非RAM存儲。比如,流對象和持久化對象,可以存放在磁盤文件中。
先來看一看下面這個對象是如何存放的
String s = new String("hello");
下圖表示了其存儲狀態:
new String("hello") 語句中用關鍵字 new 定義了一個String的對象,並為其在 “堆內存” 中分配了合適的存儲空間。
s 是一個String型對象的引用,實際上就是堆內存中一個String型對象的地址。引用 s 保存在 “堆棧” 中。
清楚了對象和其引用的存儲方式以后,我們再來看看方法的參數傳遞是怎么一回事情。看看下面語句:
List-1:
1 public static void main(String[] args) { 2 3 Func f = new Func(); 4 5 A a1 = new A(); 6 f.modify(a1); 7 }
語句很簡單,在main方法中首先定義了一個Func對象f和A對象a1,然后將a1作為參數傳遞給f的modify方法中。程序在第6行的時候會從main方法跳轉到f.modify(...)方法內部執行。下圖展示了數據的保存狀態:
可以清楚的看到main方法向modify方法中傳遞的是對象引用 a1 的一份拷貝(為了方便區分,將拷貝命名為aa1),這兩個引用都指向同一塊堆內存區域(同一個A對象)。所以,我們可以想象,在modify方法中可以通過 aa1 來修改A對象的屬性,而且其變化在由modify返回main以后依然會保留下來。
下面通過一個例子來分析一下:
List-2:
1 package com; 2 3 public class A { 4 int cunt = 5; 5 String str = "hello"; 6 7 public String toString() { 8 return "A [cunt=" + cunt + ", str=" + str + "]"; 9 } 10 }
1 package com; 2 3 public class Func { 4 5 public void modify(A a){ 6 //通過“對象引用”來修改對象的屬性 7 a.cunt = 111; 8 a.str = "modify"; 9 } 10 11 public void newObj(A a){ 12 //創建了一個新的對象,企圖在方法中將新對象引用賦值給形參a的 13 //方式來達到修改外圍方法中實參引用的目的。 14 //注意:這個是做不到的。 15 a = new A(); 16 a.cunt = 222; 17 a.str = "newObj"; 18 } 19 20 public A reNewObj(A a){ 21 //返回一個新對象,在外圍方法中將返回的對象引用賦值給實參 22 //這種方法是可以實現的。 23 a = new A(); 24 a.cunt = 333; 25 a.str = "reNewObj"; 26 return a; 27 } 28 29 public static void main(String[] args) { 30 31 Func f = new Func(); 32 33 //在modify方法中通過形參來改變a1對象的屬性,在modify 34 //方法返回以后,修改的屬性可以保留下來。 35 A a1 = new A(); 36 f.modify(a1); 37 System.out.println(a1); 38 39 //企圖通過newObj方法讓a2指向新的對象。 40 //但是,這樣做不可能達到目的。 41 A a2 = new A(); 42 f.newObj(a2); 43 System.out.println(a2); 44 45 //reNewObj方法會返回一個新的對象引用,並將其賦值給a3 46 //這種方式可以讓a3指向一個新的對象。 47 A a3 = new A(); 48 a3 = f.reNewObj(a3); 49 System.out.println(a3); 50 51 } 52 }
運行結果:
我們知道,A對象初始化的時候 cunt = 5, str = "hello" 。現在來分析上面的三個結果的原理。
1、第一個結果是 “cunt = 111, str = "modify" 說明 f.modify(a1) 方法中通過形參a1對A對象屬性的修改被保留了下來。現在來分析,為什么會保留下來??
解釋:
①. 在main方法中,a1指向堆內存中的一個A對象,a1是作為A對象的一個引用,其值是“引用”(對空間的地址值)。
調用f.modify(a1)的時候,a1作為實參傳遞給modify方法,由於java參數傳遞是屬於“值傳遞”(值的拷貝),所以在modify堆棧中會有a1的一份拷貝,兩個a1指向同一個A對象。
②. 通過modify堆棧中的引用a1可以操作其所指向的對內存中的對象。在modify方法中將堆內存中的A對象屬性值修改為“cunt = 111, str = "modify"”。
③. modify方法返回以后,modify堆棧中的a1引用所占內存被回收,main堆棧中的a1依然指向最開始的那個對象空間。由於在modify方法中已經將對象的屬性修改了,所以在main中的a1可以看見被修改后的屬性值。
2、第二個結果是 “cunt = 5, str = "hello",說明main方法中的a2所指向的對象沒有變化。42行中 f.newObj(a2); 方法的原本意圖是:在newObj方法中新創建一個A對象,並將其賦值給newObj的形參a2,並且期望在newObj方法返回以后main方法的局部變量a2也指向新的A對象。
可是,從打印的結果來看,main方法中的局部變量a2依然指向原來的A對象,並沒有指向newObj方法中新建的A對象。
那么,這個是為什么呢??
解釋:
①. 前面兩個圖和1中的解釋是一樣的,無非就是一個參數的“值傳遞問題”。
②. 在newObj方法中有這樣的三條語句:a = new A(); a.cunt = 222; a.str = "newObj";
其執行過程是這樣的:在堆內存中開辟一塊新的A對象空間,讓newObj堆棧中的a2引用指向新的對象空間 → 后兩句語句會修改新對象的屬性值。
注意,此時main堆棧中的a2依然指向最開始的那個對象空間,而newObj堆棧中的a2卻指向了新的對象。
③. newObj方法返回,newObj堆棧空間被回收,堆空間中的新對象沒有任何變量引用它,在合適的時機會被JVM回收。此時,返回到main方法中,此時的a2依然指向
依然指向最開始的那個A對象,其屬性值沒有發生變化。
所以,外圍方法中的實參無法看見在內部方法中新創建的對象。除非,新創建的對象作為內部方法的返回值,並且在外圍方法中將其賦值給實參變量。就像47~49行所做的那樣:在reNewObj方法中新創建一個對象並賦值給形參,同時在方法最后將新對象作為返回值返回給外圍方法,進一步在外圍方法中將返回的對象賦值為a3,從而達到目的。