引用類型
JDK1.2之后,Java擴充了引用的概念,將引用分為強引用、軟引用、弱引用和虛引用四種。
-
強引用
類似于"Object a = new Object()"這類的引用,只要垃圾強引用存在,垃圾回收器就不會回收掉被引用的對象。
-
軟引用
對于軟引用關聯的對象,在系統將要發生內存溢出異常之前,會把這些對象列入垃圾回收范圍中進行回收。如果這次回收還沒有足夠內存,則拋出內存異常。
使用SoftReference類實現軟引用
-
弱引用
強度比軟引用更弱,被弱引用關聯的對象只能存活到下一次垃圾回收發生之前。當發生GC時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
使用WeakReference類實現弱引用
-
虛引用
一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用取得一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是能夠在這個對象被垃圾回收器回收掉后收到一個通知。
使用PhantomReference類實現虛引用
使用場景
強引用代碼中隨處可見,對于其他幾種引用則不太熟悉,他們有什么作用呢?
假設有這樣一個需求:每次創建一個數據庫Connection的時候,需要將用戶信息User與之關聯。典型的用法就是在一個全局的Map中存儲Connection和User的映射。
public class ConnManager {
private Map<Connection,User> m = new HashMap<Connection,User>();
public void setUser(Connection s, User u) {
m.put(s, u);
}
public User getUser(Connection s) {
return m.get(s);
}
public void removeUser(Connection s) {
m.remove(s);
}
}
這種方法的問題是User的生命周期與Connection掛鉤,我們無法準確預支Connection在什么時候結束,所以需要在每個Connection關閉之后,手動從Map中移除鍵值對,否則Connection和User將一直被Map引用,即使Connection的生命周期已經結束了,GC也無法回收對應的Connection和User。這些對象留在內存中不受控制,可能會造成內存溢出。
那么,如何避免手動的從Map中刪除對象呢?
利用 WeakHashMap 即可實現:
public class ConnManager {
private Map<Connection,User> m = new WeakHashMap<Connection,User>();
public void setUser(Connection s, User u) {
m.put(s, u);
}
public User getUser(Connection s) {
return m.get(s);
}
}
WeakHashMap 與 HashMap類似,但是在其內部,key是經過WeakReference包裝的。使用WeakHashMap情況會變得怎樣呢?
每當垃圾回收發生時,那些已經結束生命周期的Connection對象(沒有強引用指向它)不受WeakHashMap中key(WeakReference)的影響,可以直接回收掉。同時,WeakHashMap利用ReferenceQueue(下文會提到) 可以做到刪除那些已經被回收的Connection對應的User。是不是做到了內存的自動管理呢?
可達性分析算法
Java執行GC時,需要判斷對象是否存活。判斷一個對象是否存活使用了"可達性分析算法"。
基本思路就是通過一系列稱為GC Roots
的對象作為起點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連,即從GC Roots到這個對象不可達時,證明此對象不可用。
可以作為GC Roots的對象包括:
虛擬機棧中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧JNI引用的對象
往往到達一個對象的引用鏈會存在多條,垃圾回收時會依據兩個原則來判斷對象的可達性:
單一路徑中,以最弱的引用為準
多路徑中,以最強的引用為準
Reference && ReferenceQueue
SoftReference,WeakReference,PhantomReference擁有共同的父類Reference,看一下其內部實現:
Reference的構造函數最多可以接受兩個參數:Reference(T referent, ReferenceQueue<? super T> queue)
referent:即Reference所包裝的引用對象
queue:此Reference需要注冊到的引用隊列
ReferenceQueue本身提供隊列的功能,ReferenceQueue對象同時保存了一個Reference類型的head節點,Reference封裝了next字段,這樣就是可以組成一個單向鏈表。
ReferenceQueue主要用來確認Reference的狀態。Reference對象有四種狀態:
-
active
GC會特殊對待此狀態的引用,一旦被引用的對象的可達性發生變化(如失去強引用,只剩弱引用,可以被回收),GC會將引用放入pending隊列并將其狀態改為pending狀態
-
pending
位于pending隊列,等待ReferenceHandler線程將引用入隊queue
-
enqueue
ReferenceHandler將引用入隊queue
-
inactive
引用從queue出隊后的最終狀態,該狀態不可變
Reference與ReferenceQueue之間是如何工作的呢?
Reference里有個靜態字段pending,同時還通過靜態代碼塊啟動了Reference-handler thread。當一個Reference的referent被回收時,垃圾回收器會把reference添加到pending這個鏈表里,然后Reference-handler thread不斷的讀取pending中的reference,把它加入到對應的ReferenceQueue中。
當reference與referenQueue聯合使用的主要作用就是當reference指向的referent回收時,提供一種通知機制,通過queue取到這些reference,來做額外的處理工作。
PhantomReference的一個使用案例
上文提到 當reference與referenQueue聯合使用的主要作用就是當reference指向的referent回收時,提供一種通知機制,通過queue取到這些reference,來做額外的處理工作。通過PhantomReference的一個例子來加深體會:用PhantomReference來自動關閉文件流
使用PhantomReference封裝引用
public class ResourcePhantomReference<T> extends PhantomReference<T> {
private List<Closeable> closeables;
public ResourcePhantomReference(T referent, ReferenceQueue<? super T> q, List<Closeable> resource) {
super(referent, q);
closeables = resource;
}
public void cleanUp() {
if (closeables == null || closeables.size() == 0)
return;
for (Closeable closeable : closeables) {
try {
closeable.close();
System.out.println("clean up:"+closeable);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
守護者線程利用ReferenceQueue做自動清理
public class ResourceCloseDeamon extends Thread {
private static ReferenceQueue QUEUE = new ReferenceQueue();
//保持對reference的引用,防止reference本身被回收
private static List<Reference> references=new ArrayList<>();
@Override
public void run() {
this.setName("ResourceCloseDeamon");
while (true) {
try {
ResourcePhantomReference reference = (ResourcePhantomReference) QUEUE.remove();
reference.cleanUp();
references.remove(reference);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void register(Object referent, List<Closeable> closeables) {
references.add(new ResourcePhantomReference(referent,QUEUE,closeables));
}
}
封裝的文件操作
public class FileOperation {
private FileOutputStream outputStream;
private FileInputStream inputStream;
public FileOperation(FileInputStream inputStream, FileOutputStream outputStream) {
this.outputStream = outputStream;
this.inputStream = inputStream;
}
public void operate() {
try {
inputStream.getChannel().transferTo(0, inputStream.getChannel().size(), outputStream.getChannel());
} catch (IOException e) {
e.printStackTrace();
}
}
}
測試
ublic class PhantomTest {
public static void main(String[] args) throws Exception {
//打開回收
ResourceCloseDeamon deamon = new ResourceCloseDeamon();
deamon.setDaemon(true);
deamon.start();
// touch a.txt b.txt
// echo "hello" > a.txt
//保留對象,防止gc把stream回收掉,其不到演示效果
List<Closeable> all=new ArrayList<>();
FileInputStream inputStream;
FileOutputStream outputStream;
for (int i = 0; i < 100000; i++) {
inputStream = new FileInputStream("/Users/robin/a.txt");
outputStream = new FileOutputStream("/Users/robin/b.txt");
FileOperation operation = new FileOperation(inputStream, outputStream);
operation.operate();
TimeUnit.MILLISECONDS.sleep(100);
List<Closeable>closeables=new ArrayList<>();
closeables.add(inputStream);
closeables.add(outputStream);
all.addAll(closeables);
ResourceCloseDeamon.register(operation,closeables);
//用下面命令查看文件句柄,如果把上面register注釋掉,就會發現句柄數量不斷上升
//jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs lsof -p | grep /User/robin
System.gc();
}
}
}
WeakHashMap
WeakHashMap實現原理很簡單,它除了實現標準的Map接口,里面的機制也和HashMap的實現類似。從它entry子類中可以看出,它的key是用WeakReference封裝的。
WeakHashMap里聲明了一個queue,Entry繼承WeakReference,構造函數中用key和queue關聯構造一個weakReference。當key所封裝的對象被GC回收后,GC自動將key注冊到queue中。
WeakHashMap中有代碼檢測這個queue,取出其中的元素,找到WeakHashMap中相應的鍵值對進行remove。這部分代碼就是expungeStaleEntries方法:
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
這段代碼會在resize,getTable,size里執行,清除失效的entry。
FinalReference
在Reference的子類中,還有一個名為FinalReference的類,這個類用來做什么呢?
FinalReference僅僅繼承了Reference,沒有做其他的邏輯,只是將訪問權限聲明為package,所以我們不能夠直接使用它。
需要關注的是其子類 Finalizer,看一下他的實現:
首先,哪些類對象是Finalizer reference類型的referent呢? 只要類覆寫了Object 上的finalize方法,方法體非空。那么這個類的實例都會被Finalizer引用類型引用。這個工作是由虛擬機完成的,對于我們來說是透明的。
Finalizer 中有兩個字段需要關注:
queue:private static ReferenceQueue queue = new ReferenceQueue()
即上文提到的ReferenceQueue,用來實現通知
unfinalized:private static Finalizer unfinalized
維護了一個未執行finalize方法的reference列表。維護靜態字段unfinalized的目的是為了一直保持對未未執行finalize方法的reference的強引用,防止被gc回收掉。
在Finalizer的構造函數中通過add()方法把Finalizer引用本身加入到unfinalized列表中,同時關聯finalizee和queue,實現通知機制。
Finalizer靜態代碼塊里啟動了一個deamon線程 FinalizerThread,FinalizerThread run方法不斷的從queue中去取Finalizer類型的reference,然后調用Finalizer的runFinalizer方法,該方法最后執行了referent所重寫的finalize方法。
private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);
/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
觀察上面的代碼,hasBeenFinalized()
判斷了finalize是否已經執行,如果執行,則把這個referent從unfinalized隊列中移除。所以,任何一個對象的finalize方法只會被系統自動調用一次。當下一次GC發生時,由于unfinalized已經不再持有該對象的referent,故該對象被直接回收掉。
從上面的過程也可以看出,覆蓋了finalize方法的對象至少需要兩次GC才可能被回收。第一次GC把覆蓋了finalize方法的對象對應的Finalizer reference加入referenceQueue等待FinalizerThread來執行finalize方法。第二次GC才有可能釋放finalizee對象本身,前提是FinalizerThread已經執行完finalize方法了,并把Finalizer reference從Finalizer靜態unfinalized鏈表中剔除,因為這個鏈表和Finalizer reference對finalizee構成的是一個強引用。