哈嘍,大家好🎉,我是世傑。
本文我為大家介紹面試官經常考察的「Java物件引用相關內容」
照例在開頭留一些面試考察內容~~
面試連環call
- Java物件引用都有哪些型別?
- Java引數傳遞是值傳遞還是引用傳遞? 為什麼?
- Java物件引用訪問方式有哪些?
上一篇文章講了 JVM 在執行 new 物件的過程中都執行了那些操作。現在我來講一下建立物件之後的的使用
1. Java物件引用
《Java程式設計思想》: 每種程式語言都有自己的資料處理方式。有些時候,程式設計師必須注意將要處理的資料是什麼型別。你是直接操縱元素,還是用某種基於特殊語法的間接表示(例如C/C++裡的指標)來操作物件。所有這些在 Java 裡都得到了簡化,一切都被視為物件。因此,我們可採用一種統一的語法。儘管將一切都“看作”物件,但操縱的識別符號實際是指向一個物件的“引用”(reference)。
Java程式需要透過棧上的引用資料來操作堆上的具體物件。物件的訪問方式取決於虛擬機器實現,目前主流的訪問方式有使用控制代碼和直接指標兩種。
- 直接指標:指向物件,代表物件的記憶體地址。
- 控制代碼:控制代碼是一種特殊的指標,可以理解為指向指標的指標,維護指向物件的指標變化,而物件的控制代碼本身不發生變化
1.1 直接指標
如果使用直接指標訪問,那麼Java堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而引用中儲存的直接就是物件地址。
- 優勢:速度更快,節省了一次指標定位的時間開銷。當物件的訪問非常頻繁,此開銷積也可節省非常可觀的成本。
1.2 控制代碼
Java堆中劃分出一塊記憶體來作為控制代碼池,引用中儲存物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊。
- 優勢:引用中儲存的是穩定的控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而引用本身不需要修改。
- 缺點:需要兩次指標訪問才能訪問到物件資料。
2. 物件引用型別
在Java中一共有四種引用型別,分為了:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4 種,這 4 種引用的強度依次減弱。
2.1 強引用
強引用是最常見的引用型別。
- 當我們建立一個物件並將其賦值給一個引用變數時,這個引用就被稱為強引用。只要強引用存在,垃圾回收器就不會回收被引用的物件。即使系統記憶體空間不足,JVM寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的存活物件來解決記憶體不足問題。
- 強引用的特性是隻要有強引用存在,被引用的物件就不會被垃圾回收。
- 可以將強引用賦值為 null,這樣一來,JVM 就可以適時的回收物件了(注意不是立刻回收)
public class StrongReferenceUsage {
@Test
public void stringReference(){
Object obj = new Object();
}
}
上面我們new了一個Object物件,並將其賦值給obj,這個obj就是new Object()的強引用。
2.2 軟引用
軟引用是用來描述一些還有用但並非必需的物件。
-
對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,將會把這些物件列進回收範圍進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在Java中,軟引用透過java.lang.ref.SoftReference類來實現。
-
這種特性常常被用來實現快取技術,比如網頁快取,圖片快取等。
@Test
public void softReference(){
Object obj = new Object();
SoftReference<Object> soft = new SoftReference<>(obj);
obj = null;
log.info("{}",soft.get());
System.gc();
log.info("{}",soft.get());
}
輸出結果:
22:50:43.733 [main] INFO com.flydean.SoftReferenceUsage - java.lang.Object@71bc1ae4
22:50:43.749 [main] INFO com.flydean.SoftReferenceUsage - java.lang.Object@71bc1ae4
可以看到在記憶體充足的情況下,SoftReference引用的物件是不會被回收的。
2.3 弱引用
弱引用也是用來描述非必需物件的,不過它的強度比軟引用更弱一些。
-
被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收被弱引用的物件。
-
在Java中,弱引用透過java.lang.ref.WeakReference類來實現。
@Test
public void weakReference() throws InterruptedException {
Object obj = new Object();
WeakReference<Object> weak = new WeakReference<>(obj);
obj = null;
log.info("{}",weak.get());
System.gc();
log.info("{}",weak.get());
}
輸出結果:
22:58:02.019 [main] INFO com.flydean.WeakReferenceUsage - java.lang.Object@71bc1ae4
22:58:02.047 [main] INFO com.flydean.WeakReferenceUsage - null
可以看到即便在記憶體充足的情況下,WeakReference引用的物件也被回收了。
2.4 虛引用
虛引用是最弱的一種引用關係。
- 一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法透過虛引用來取得一個物件例項。
- 為一個物件設定虛引用的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
- 在Java中,虛引用透過java.lang.ref.PhantomReference類來實現。
- 虛引用必須和引用佇列(ReferenceQueue)聯合使用
PhantomReference的作用是跟蹤垃圾回收器收集物件的活動,在GC的過程中,如果發現有PhantomReference,GC則會將引用放到ReferenceQueue中,由程式設計師自己處理,當程式設計師呼叫ReferenceQueue.pull()方法,將引用佇列中的物件引用移除之後,Reference物件會變成Inactive狀態,意味著被引用的物件可以被回收了。
@Slf4j
public class PhantomReferenceUsage {
@Test
public void usePhantomReference(){
ReferenceQueue<Object> rq = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj,rq);
obj = null;
log.info("{}",phantomReference.get());
System.gc();
Reference<Object> r = (Reference<Object>)rq.poll();
log.info("{}",r);
}
}
輸出結果:
07:06:46.336 [main] INFO com.flydean.PhantomReferenceUsage - null
07:06:46.353 [main] INFO com.flydean.PhantomReferenceUsage - java.lang.ref.PhantomReference@136432db
我們看到get的值是null,而GC過後,poll是有值的。
因為PhantomReference引用的是需要被垃圾回收的物件,所以在類的定義中,get一直都是返回null:
public T get() {
return null;
}
『總結』
- 強引用是最常見的型別,但如果記憶體空間不足,可能會導致OutOfMemoryError錯誤。
- 軟引用的物件在系統記憶體即將溢位時會被回收,適用於快取等情況。
- 弱引用的物件無論當前記憶體空間足夠與否都會被回收,適用於實現物件的單例模式等。
- 虛引用的主要目的是為了能收到系統通知,以便在物件被回收時進行相應的處理。
在程式設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速 JVM 對垃圾記憶體的回收速度,可以維護系統的執行安全,防止記憶體溢位(OOM)等問題的產生。
3. 值傳遞 vs 引用傳遞
3.1 定義
- 值傳遞:方法引數傳入的是引數的備份,對引數的修改並不會影響本身
- 引用傳遞:方法引數傳入的都是引數的引用,對引數的修改會影響本身
Java的引數傳遞是值傳遞還是引用傳遞?
先說結論:Java本質上都是值傳遞
3.2 Java物件型別
說到值傳遞和引用傳遞我們不得不提到兩個概念:值型別和引用型別。
基本型別是值型別。JVM賦值時,直接在棧上生成值
類、介面、陣列、物件、基本型別的包裝類都是引用型別。JVM賦值時,在棧上生成引用,在堆中生成資料
3.3 為什麼?
先看一段程式碼
@Test
public void testReference() {
Base base = new Base();
base.setName("main");
set2(base);
System.out.println(base.getName());
}
private void set1(Base base) {
base.setName("set1");
}
private void set2(Base base) {
base = new Base();
base.setName("set2");
}
上述的方法,呼叫set1的執行結果是【set1】,呼叫set2的執行結果是【main】
由此可以看出,方法入參傳入的都是副本
- 當是基本型別時,傳入的是基本型別的複製副本,修改基本型別的【入參】並不會影響【實參】
- 當是引用型別時,傳入的引用型別依舊是複製副本,但是此時的副本是複製地址,因此修改入參會影響到實現的值,因此入參和實參的指向都是堆中同一個記憶體區域,例如set1方法。但當為入參重新賦值一個新的地址,並在修改此地址指向的記憶體區域,就不會影響實參中的值,例如set2方法。
所以Java是值傳遞
參考文章
-
https://mp.weixin.qq.com/s/r5cMTmQjNwYwiX7pLx_52Q
-
JAVA物件的建立及記憶體分配詳解
-
物件和引用
-
理解java物件的引用
-
一文讀懂java中的Reference和引用型別
-
這一次,徹底解決Java的值傳遞和引用傳遞