總結Java中的reference型別與四種參考型別
本文通過分析原始碼和實驗測驗總結了Java中的reference型別、Reference類以及四種參考型別的基礎知識,
僅做學習記錄目的,有誤的歡迎指出!
一、什么是reference型別
Java資料型別分為兩大類:
基本型別 (primitive type)
8種基本型別 byte, short, int, long, float, double, char , boolean
參考型別(reference)
《Java虛擬機規范》中寫道:
Java虛擬機中有三種參考型別:型別別(class type)、陣列型別(array type)和介面型別(interface type),這些參考型別的值分別指向動態創建的類實體、陣列示例和實作了某個介面的類示例或陣列示例,
可見,參考型別的值其實就是實體在堆記憶體上的地址,可以把參考近似理解為指標,
參考中的null值:當一個參考不指向任何物件的時候,它的值就用null來表示,參考的默認值就是null,
JVM應能通過參考實作兩點:
- 從該參考直接或間接地查找到物件在堆中的資料存放的起止地址索引,
- 從該參考中直接或間接地查到物件所屬類在方法區中存盤的型別資訊,
這是很容易理解的,比如下面的代碼:
User ref = new User();
ref.getUsername(); // 通過參考獲取該類的實體的資料
ref.getClass(); // 通過參考獲取該類的型別資訊 (Class物件)
實際上,在HotSpot的實作中,reference的值并不直接指向實體,而是指向一個句柄,由句柄再指向實際的實體,這樣的好處時,在物件實體資料在記憶體中的位置被移動時(比如GC時),不需要修改堆疊上所有相關的reference的值,只需要修改句柄的值(只需要修改一次),代價是多一次的尋址,

二、什么是Reference類
Reference類是Java.lang.ref包里的一個抽象類,原始碼中對其的描述是:
Abstract base class for reference objects. This class defines the operations common to all reference objects.
我把這個Reference類理解為 (也許不準確):描述reference型別的類,這個類定義了reference型別的行為,提供了reference型別的基本功能,就像Integer類之于int型別,
Reference物件可以“注冊”相關的參考物件,并通過內部的reference佇列提供外部程式監控物件被GC的能力,
部分原始碼:
/**
* 被注冊的參考物件
*/
private T referent; /* Treated specially by GC */
/**
* 當一個Reference物件系結的物件被GC回收時,JVM會將該參考物件被系結到的reference物件(this)推入此佇列,
* 其他程式可以通過輪詢此佇列,來獲得該注冊物件被GC的的“通知”,并完成一些作業
* 如WeakHashMap可以"知道"被GC的Entry并將其從Map中移除
* 實際只是邏輯上的一個標志,標志該物件是否加入到了佇列,
* 佇列里的Reference物件是通過next屬性組成鏈式回圈佇列
*/
volatile ReferenceQueue<? super T> queue;
volatile Reference next;
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
/**
* 回傳注冊的參考物件,若物件已被GC,回傳null
*/
public T get() {
return this.referent;
}
/**
* 清除注冊到該物件的參考物件,但是并不會加入referenceQueue
*/
public void clear() {
this.referent = null;
}
/**
*將"注冊"的物件,加入referenceQueue,
*/
public boolean enqueue() {
this.referent = null;
return this.queue.enqueue(this);
}
三、四種參考型別
JVM把參考型別分為四種型別:強參考、軟參考、弱參考、虛參考,參考的型別可以描述它所指向的實體的可達性,進而供垃圾回收器根據不同型別做出不同的處理的能力,同時也提供了編程者跟蹤物件生命周期的功能,
描述不同的參考型別,由Reference類的子類來實作:
- FinalReference(強參考)
- SoftReference (軟參考)
- WeakReference (弱參考)
- PhantomReference (虛參考)
1、 FinalReference 強參考
強參考是指創建一個物件并它賦值給一個參考,參考是存在JVM中的堆疊(還有方法區)中的,具有強參考的物件,垃圾回收器絕對不會去回收它,直到記憶體不足以分配時,拋出OOM,
大多數情況,我們new一個物件,并把它賦值給一個變數,這個變數就是強參考,
class TestA {
// 方法區中的類靜態屬性參考的物件
private static Object finalRet2 = new Object();
// 方法區中的常量參考的物件
private static final Object finalRet3 = new Object();
void methodA {
// 堆疊上的區域變數參考的物件
Object finalRet1 = new Object();
}
native void methodB {
// JNI中參考的物件
// ......
}
}
以上指向的實體物件,是可達的,
FinalReference 類只用于實作Finalize功能,非public類,用戶是不可用的
2、SoftReference 軟參考
軟參考描述一些還有用但非必需的物件
具有軟參考關聯的物件,記憶體空間足夠時,垃圾回收器不會回收它,當記憶體不足時(接近OOM),垃圾回收器才會去決定是否回收它,
軟參考一般用來實作簡單的記憶體快取,
我們通過以下測驗代碼來驗證它的特性:
public class ReferenceTest {
class User {
// 模擬記憶體占用3M,以更好觀察gc前后的記憶體變化
private byte[] memory = new byte[3*1024*1024];
}
/**
* 測驗弱參考在記憶體足夠時不會被GC,在記憶體不足時才會被GC的特性
* JVM引數 -Xms20m -Xmx20m -Xlog:gc 將記憶體大小限制在20M,并列印出GC日志
*/
public void testSoftReference(){
// 當僅使用強參考,脫離GC Root后將會被回收 (可以通過查看gc日志來確認該物件確實被回收)
// 這是對照組
User retA = new User();
retA = null;
System.gc();
System.out.println("對照組GC后:" + retA);
User retB = new User();
// 創建弱參考類,將該參考系結到弱參考物件上
SoftReference<User> sortRet = new SoftReference<>(retB);
retB = null;
// 此時并不會被GC
System.gc();
retB = sortRet.get();
System.out.println("GC后通過軟參考重新獲取了物件:" + retB);
retB = null;
// 模擬記憶體不足,即將發生OOM
List<User> manyUsers = new ArrayList<>();
for(int i = 1; i < 100000; i++){
System.out.println("將要創建第" + i + "個物件");
manyUsers.add(new User());
System.out.println("創建第" + i + "個物件后, 軟參考物件:" + sortRet.get());
}
}
public static void main(String[] args) {
ReferenceTest referenceTest = new ReferenceTest();
referenceTest.testSoftReference();
}
}
執行結果如下:

3、WeakReference 弱參考
弱參考描述非必需物件,但它的強度比軟參考更弱一些,
WeakReference對其參考的物件并無保護作用,當垃圾回收器進行垃圾回收時,無論記憶體是否充足,都會回收被弱參考關聯的物件,弱參考一般用于實作canonicalizing mappings (正規化映射),典型的應用是WeakHashMap,
我們通過以下代碼來驗證它的特性:
/**
* 測驗弱參考無論記憶體是否足夠都會被GC的特性
*/
public void testWeakReference(){
User user = new User();
WeakReference<User> ret = new WeakReference<>(user);
System.out.println("GC前: " + ret.get());
user = null;
System.gc();
System.out.println("GC后: " + ret.get());
}
執行結果:
[0.014s][info][gc] Using G1
[0.033s][info][gc] Periodic GC disabled
GC前: memory.ReferenceTest$User@b4c966a
[0.098s][info][gc] GC(0) Pause Full (System.gc()) 6M->0M(20M) 2.815ms
GC后: null
4、PhantomReference 虛參考
虛參考也被稱為幽靈參考或幻參考,它是最弱的一種參考關系,
虛參考并不會影響物件的GC,而且并不可以通過PhantomReference物件取得一個參考的物件,
虛參考唯一的作用則是利用其必須和ReferenceQueue關聯使用的特性,當其系結的物件被GC回收后會被推入ReferenceQueue,外部程式可以通過對此佇列輪詢來獲得一個通知,以完成一些目標物件被GC后的清理作業,
PhantomReference 的構造方法,與SoftReference和WeakReference不同,他的構造必須傳入一個ReferenceQueue
public PhantomReference(T referent, ReferenceQueue<? super T> q) { super(referent, q);}
四、應用
1、軟參考實作記憶體快取
上文提到,軟參考關聯的物件在記憶體足夠時不會被GC清理,在記憶體不足時才會被GC清,結合我們可以通過ReferenceQueue獲取一個被GC的物件的Reference參考物件的能力,我們可以實作一個簡單的記憶體快取,該快取在JVM記憶體不足時能夠自動清理,在記憶體充足時可以自動裝入,
實作代碼:
/**
* @ClassName SoftRefCache
* @Description 軟參考實作的記憶體快取(僅做學習目的,實際專案當然是用造好的輪子,memcached、redis等)
*/
public class SoftRefCache<K, V> {
// 實際裝載快取的資料結構,采用Hashtable可以保證執行緒安全
private final Hashtable<K, ValueRef> cache;
// 此佇列用來接收被GC的參考物件,來完成清理作業
private final ReferenceQueue<V> queue;
// 當被快取物件不存在快取中時,呼叫該介面來查詢此物件,以裝入快取
private final QueryForCache<K,V> queryForCache;
public SoftRefCache(QueryForCache<K,V> queryForCache) {
this.cache = new Hashtable<>();
this.queue = new ReferenceQueue<>();
this.queryForCache = queryForCache;
}
/**
* 對value的包裝,使用軟參考來關聯value物件,使其具有軟參考的物件特性,并保存該value物件的key,以便于完成清理作業
*/
private class ValueRef extends SoftReference<V> {
private final K key;
public ValueRef(K key, V referent, ReferenceQueue<? super V> q) {
super(referent, q);
this.key = key;
}
public K getKey() {
return key;
}
}
/**
* 由Key獲取一個物件,若已被快取,則直接回傳,若未被快取,則將其快取
* @param key 要獲取的物件的eky
* @return 要獲取的物件
*/
public V get(K key) {
V val = null;
if (cache.containsKey(key)) {
ValueRef valueRef = cache.get(key);
val = valueRef != null ? valueRef.get() : null;
}
// cache中沒有該key對應的物件實體
if (val == null) {
// 到資料庫或硬碟查詢該物件,并加入到cache中
val = this.queryForCache.query(key);
addToCache(key, val);
}
return val;
}
/**
* 獲取快取內key--value對的數量
*/
public int size(){
this.clearCache();
return cache.size();
}
/**
* 清除快取
*/
public void clearAllCache(){
clearCache();
cache.clear();
// 可以根據實際情況決定是否要GC
System.gc();
}
/**
* 將物件加入快取
*/
private void addToCache(K key, V val){
// 清除垃圾參考
clearCache();
// 加入到快取
ValueRef valueRef = new ValueRef(key, val, queue);
this.cache.put(key, valueRef);
}
/**
* 清除快取中已被GC的Value物件,
* 具體是通過對ReferenceQueue輪詢來實作的
*/
private void clearCache(){
ValueRef valueRef = null;
while((valueRef = (ValueRef) queue.poll()) != null){
cache.remove(valueRef.getKey());
}
}
}
/**
* 該介面定義了一個需要快取的物件不在快取時,應該通過怎樣的方式獲取
* @param <K> key的型別
* @param <V> value的型別
*/
@FunctionalInterface
interface QueryForCache<K,V> {
V query(K key);
測驗代碼:
/**
* @ClassName SortRefCacheTest
* @Description 測驗自己實作的軟參考快取,JVM引數:-Xms20m -Xmx20m -Xlog:gc
*/
public class SortRefCacheTest {
public static void main(String[] args) {
// 這個介面實際應該實作為到資料庫或硬碟查詢實際的資料,這里就簡單模擬,直接new
QueryForCache<Integer, MyImage> queryForCache = key -> new MyImage(key, new byte[2*1024*1024]);
// 創建快取
SoftRefCache<Integer, MyImage> softRefCache = new SoftRefCache<>(queryForCache);
// 此處模擬不斷對快取進行裝入,觀察記憶體和gc情況
for(int i=1; i < 100; i++){
MyImage value = https://www.cnblogs.com/allentechblog/p/softRefCache.get(i);
System.out.println("從快取中獲取到第" + value.getId() + "個MyImage");
}
}
}
class MyImage {
private Integer id;
private byte[] data; // 模擬較大的記憶體占用,以更好觀察gc前后的記憶體變化
public MyImage(Integer id, byte[] data) {
this.id = id;
this.data = https://www.cnblogs.com/allentechblog/p/data;
}
public Integer getId() {
return id;
}
}
執行結果(部分):

執行到最后,并沒有拋出OOM

如果使用普通的HashMap等容器,結果就是OOM,這里就不驗證了
參考文獻
-
《深入理解Java虛擬機》 第二版 周志明著
-
JAVA中reference型別簡述 https://www.iteye.com/blog/shift-alt-ctrl-1839163
-
JAVA四種參考方式 https://blog.csdn.net/u014086926/article/details/52106589#
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/192483.html
標籤:Java
