本文通過分析源碼和實驗測試總結了Java中的reference類型、Reference類以及四種引用類型的基礎知識。 僅做學習記錄目的,有誤的歡迎指出! ...
總結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 = 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 = 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#