關於值類型和引用類型的話題,C++、JAVA、python、go、C#等等高級語言都有相關的概念,只要理解了其底層工作原理,可以說即使是不同的語言,在面試學習工作實踐中都可以信手拈來(不要太糾集語言),當然此處我選擇了JAVA,雖然我是搞C++的,具體原因都懂就不廢話了。
一、值類型與引用類型
1、變量初始化
int num=10;
String str="hello"
2、變量賦值
從上圖可以顯而易見,num是int基本類型變量,值就直接保存在變量中。str是String引用類型變量,變量中保存的只是實際對象對應的地址信息,而不是實際對象數據。對於而這特性,如下:
num=20; str="java";
對於基本類型變量num,賦值運算符將會直接修改變量的值,原來的數據將被覆蓋掉,被替換為新的值。對於引用類型變量str,賦值運算符只會改變變量中所保存的對象的地址信息,原來對象的地址被覆蓋掉,重新寫入新對象的地址數據。但原來的對象本身並不會被改變,只是不再被任何引用所指向的對象,即“垃圾對象”,后續會被垃圾回收器回收。
3、函數傳參
// 基本類型參數,原始value不會被更改
public void func(int value) { value = 100; } // 對於沒有提供修改自身的成員方法引用類型,原始str不會被更改
public void func(String str) { str = "hello"; } StringBuilder sb = new StringBuilder("test"); // 對於提供修改自身的成員方法引用類型,原始的sBuilder會被更改
public void func(StringBuilder sBuilder) { sBuilder.append("aa"); } // 原始的sBuilder不會被更改
public void test(StringBuilder sBuilder) { sBuilder = new StringBuilder("111"); }
說明:對於第三種情況:
對於第四種情況:
二、數據存儲方式
1、局部變量/方法參數
對於局部變量和方法傳遞的參數在jvm中的存儲方式是相同的,都是存儲在棧上開辟的空間中。方法參數空間在進入方法時開辟,方法退出時進行回收。以32為JVM為例,boolean、byte、short、char、int、float以及對應的引用類型都是分配4字節大小的空間,long、double分配8字節大小空間。對於每一個方法來說,最多占用空間大小是固定的,在編譯時就已經確定了。當在方法中聲明一個int變量i=0或Object變量obj=null時,此時僅僅在棧上分配空間,不影響到堆空間。當new Object()時,將會在堆中開辟一段內存空間並初始化Object對象。
2、數組類型引用和對象
當聲明數組時,int[] arr=new int[2];數組也是對象,arr實際上是引用,棧上占用4個字節大小的存儲空間,而是會在堆中開辟相應大小空間進行存儲,然后arr變量指向它。當聲明一個二維數組時,如:int[][] arr2=new int[2][4],arr2同樣在棧中占用4個字節,在堆內存中開辟長度為2,類型為int[]的數組對象,然后arr2指向這個數組。這個數組內部有兩個引用類型(大小為4個字節),分別指向兩個長度為4類型為int的數組。內存分布如圖:
所以當傳遞一個數組給一個方法時,數組的元素在方法內部是可以被修改的,但是無法讓數組引用指向新的數組。其實,還可以聲明:int [][] arr3=new int[3][],內存分布如下:
3、String類型數據
對於String類型,其對象內部需要維護三個成員變量,char[] chars,int startIndex, int length。chars是存儲字符串數據的真正位置,在某些情況下是可以共用的,實際上String類型是不可變類型。例如:String str=new String("hello"),內存分布如下:
三、JAVA引用類型
在JAVA中提供了四種引用類型:強引用、軟引用、軟引用和虛引用。在四種引用類型中,只有強引用FinalReference類型變量是包內可見的,其他三種引用類型均為public,可以在程序中直接使用。
1、強引用
強引用是使用最普遍的引用。如果一個對象具有強引用,那么垃圾回收器絕不會回收它。例如:StringBuilder sb = new StringBuilder("test");變量str指向StringBuffer實例所在的堆空間,通過str可以操作該對象。如下:
強引用特點:
-
- 強引用可以直接訪問目標對象
- 只要有引用變量存在,垃圾回收器永遠不會回收。JVM即使拋出OOM異常,也不會回收強引用所指向的對象。
- 強引用可能導致內存泄漏問
2、軟引用
軟引用是除了強引用外,最強的引用類型。可以通過java.lang.ref.SoftReference使用軟引用。一個持有軟引用的對象,不會被JVM很快回收,JVM會根據當前堆的使用情況來判斷何時回收。當堆使用率臨近閾值時,才會去回收軟引用的對象。因此,軟引用可以用於實現對內存敏感的高速緩存。SoftReference的特點是它的一個實例保存對一個Java對象的軟引用,該軟引用的存在不妨礙垃圾收集線程對該Java對象的回收。也就是說,一旦SoftReference保存了對一個Java對象的軟引用后,在垃圾線程對 這個Java對象回收前,SoftReference類所提供的get()方法返回Java對象的強引用。一旦垃圾線程回收該Java對象之后,get()方法將返回null。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj = null; sf.get();//有時候會返回null
sf是對obj的一個軟引用,通過sf.get()方法可以取到這個對象,當這個對象被標記為需要回收的對象時,則返回null;
軟引用主要用戶實現類似緩存的功能,在內存足夠的情況下直接通過軟引用取值,無需從繁忙的真實來源查詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。使用軟引用能防止內存泄露,增強程序的健壯性。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。也就是說,ReferenceQueue中保存的對象是Reference對象,而且是已經失去了它所軟引用的對象的Reference對象。當調用它的poll()方法的時候,如果這個隊列中不是空隊列,那么將返回隊列前面的那個Reference對象。在任何時候,都可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的非強可及對象被回收。如果隊列為空,將返回一個null,否則該方法返回隊列中前面的一個Reference對象。利用這個方法,可以檢查哪個SoftReference所軟引用的對象已經被回收,於是可以把這些失去所軟引用的對象的SoftReference對象清除掉。
3、弱引用
弱引用是一種比軟引用較弱的引用類型。在系統GC時,只要發現弱引用,不管系統堆空間是否足夠,都會將對象進行回收。在java中,可以用java.lang.ref.WeakReference實例來保存對一個Java對象的弱引用。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命周期。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。弱引用主要用於監控對象是否已經被垃圾回收器標記為即將回收的垃圾,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器標記。弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; wf.get();//有時候會返回null
wf.isEnQueued();//返回是否被垃圾回收器標記為即將回收的垃圾
4、虛引用
虛引用是所有類型中最弱的一個。一個持有虛引用的對象和沒有引用幾乎是一樣的,隨時可能被垃圾回收器回收,當試圖通過虛引用的get()方法取得強引用時,總是會失敗。並且虛引用必須和引用隊列一起使用,它的作用在於檢測對象是否已經從內存中刪除,跟蹤垃圾回收過程。當垃圾回收器准備回收一個對象時,如果發現它還有虛引用,就會在垃圾回收后,銷毀這個對象,將這個虛引用加入引用隊列。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj); obj=null; pf.get();//永遠返回null
pf.isEnQueued();//返回是否從內存中已經刪除
@Test public void test(){ Map map; map = new WeakHashMap<String,Object>(); for (int i =0;i<10000;i++){ map.put("key"+i,new byte[i]); } // map = new HashMap<String,Object>(); // for (int i =0;i<10000;i++){ // map.put("key"+i,new byte[i]); // } }
使用-Xmx2M限定堆內存,使用WeakHashMap的代碼正常運行結束,而使用HashMap的代碼段拋出異常:java.lang.OutOfMemoryError: Java heap space。由此可見,WeakHashMap會在系統內存緊張時使用弱引用,自動釋放掉持有弱引用的內存數據。但如果WeakHashMap的key都在系統內持有強引用,那么WeakHashMap就退化為普通的HashMap,因為所有的數據項都無法被自動清理。