JDK 1.4開始添加了新的I/O類,引入了一種基於通道和緩衝區執行I/O的新方式,就像Java堆上的內存支持I/O緩衝區同樣,NIO添加了對直接ByteBuffer的支持,ByteBuffer受本機內存而不是Java堆的支持,直接ByteBuffer能夠直接傳遞到本機操做系統庫函數,以執行I/O,這種狀況雖然提升了Java程序在I/O的執行效率,可是會對本機內存進行直接的內存開銷。ByteBuffer直接操做和非直接操做的區別以下:
對於在何處存儲直接 ByteBuffer 數據,很容易產生混淆。應用程序仍然在 Java 堆上使用一個對象來編排 I/O 操做,但持有該數據的緩衝區將保存在本機內存中,Java 堆對象僅包含對本機堆緩衝區的引用。非直接 ByteBuffer 將其數據保存在 Java 堆上的 byte[] 數組中。直接ByteBuffer對象會自動清理本機緩衝區,但這個過程只能做爲Java堆GC的一部分執行,它不會自動影響施加在本機上的壓力。GC僅在Java堆被填滿,以致於沒法爲堆分配請求提供服務的時候,或者在Java應用程序中顯示請求它發生。
6)線程:
應用程序中的每一個線程都須要內存來存儲器堆棧(用於在調用函數時持有局部變量並維護狀態的內存區域)。每一個 Java 線程都須要堆棧空間來運行。根據實現的不一樣,Java 線程能夠分爲
本機線程和
Java 堆棧。除了堆棧空間,每一個線程還須要爲線程
本地存儲(thread-local storage)和內部數據結構提供一些本機內存。儘管每一個線程使用的內存量很是小,但對於擁有數百個線程的應用程序來講,線程堆棧的總內存使用量可能很是大。若是運行的應用程序的線程數量比可用於處理它們的處理器數量多,效率一般很低,而且可能致使糟糕的性能和更高的內存佔用。
ii.本機內存耗盡:
Java運行時善於以不一樣的方式來處理
Java堆空間的耗盡和
本機堆空間的耗盡,可是這兩種情形具備相似症狀,當Java堆空間耗盡的時候,Java應用程序很難正常運行,由於Java應用程序必須經過分配對象來完成工做,只要Java堆被填滿,就會出現糟糕的GC性能,而且拋出OutOfMemoryError。相反,一旦 Java 運行時開始運行而且應用程序處於穩定狀態,它能夠在本機堆徹底耗盡以後繼續正常運行,不必定會發生奇怪的行爲,由於須要分配本機內存的操做比須要分配 Java 堆的操做少得多。儘管須要本機內存的操做因 JVM 實現不一樣而異,但也有一些操做很常見:
啓動線程、
加載類以及
執行某種類型的網絡
和
文件 I/O。本機內存不足行爲與 Java 堆內存不足行爲也不太同樣,由於沒法對本機堆分配進行控制,儘管全部 Java 堆分配都在 Java 內存管理系統控制之下,但任何本機代碼
(不管其位於 JVM、Java 類庫仍是應用程序代碼中)均可能執行本機內存分配,並且會失敗。嘗試進行分配的代碼而後會處理這種狀況,不管設計人員的意圖是什麼:它可能經過 JNI 接口拋出一個 OutOfMemoryError,在屏幕上輸出一條消息,發生無提示失敗並在稍後再試一次,或者執行其餘操做。
iii.例子:
這篇文章一致都在講概念,這裏既然提到了ByteBuffer,先提供一個簡單的例子演示該類的使用:
——[$]使用NIO讀取txt文件——
package org.susan.java.io;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ExplicitChannelRead {
public static void main(String args[]){
FileInputStream fileInputStream;
FileChannel fileChannel;
long fileSize;
ByteBuffer byteBuffer;
try{
fileInputStream = new FileInputStream("D://read.txt");
fileChannel = fileInputStream.getChannel();
fileSize = fileChannel.size();
byteBuffer = ByteBuffer.allocate((int)fileSize);
fileChannel.read(byteBuffer);
byteBuffer.rewind();
for( int i = 0; i < fileSize; i++ )
System.out.print((char)byteBuffer.get());
fileChannel.close();
fileInputStream.close();
}catch(IOException ex){
ex.printStackTrace();
}
}
}
在讀取文件的路徑放上該txt文件裏面寫入:Hello World,上邊這段代碼就是
使用NIO的方式讀取文件系統上的文件,這段程序的輸入就爲:
Hello World
——[$]獲取ByteBuffer上的字節轉換爲Byte數組——
package org.susan.java.io;
import java.nio.ByteBuffer;
public class ByteBufferToByteArray {
public static void main(String args[]) throws Exception{
// 從byte數組建立ByteBuffer
byte[] bytes = new byte[10];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
// 在position和limit,也就是ByteBuffer緩衝區的首尾之間讀取字節
bytes = new byte[buffer.remaining()];
buffer.get(bytes, 0, bytes.length);
// 讀取全部ByteBuffer內的字節
buffer.clear();
bytes = new byte[buffer.capacity()];
buffer.get(bytes, 0, bytes.length);
}
}
上邊代碼就是從
ByteBuffer到byte數組的
轉換過程,有了這個過程在開發過程當中可能更加方便,ByteBuffer的詳細講解我保留到IO部分,這裏僅僅是涉及到了一些,因此提供兩段實例代碼。
iv.共享內存:
在Java語言裏面,
沒有共享內存的概念,可是在某些引用中,共享內存卻很受用,例如Java語言的分佈式系統,存着大量的Java分佈式共享對象,不少時候須要查詢這些對象的狀態,以查看系統是否運行正常或者瞭解這些對象目前的一些統計數據和狀態。若是使用的是網絡通訊的方式,顯然會增長應用的額外開銷,也增長了沒必要要的應用編程,若是是共享內存方式,則能夠直接經過共享內存查看到所須要的對象的數據和統計數據,從而減小一些沒必要要的麻煩。
1)共享內存特色:
- 能夠被多個進程打開訪問
- 讀寫操做的進程在執行讀寫操做的時候其餘進程不能進行寫操做
- 多個進程能夠交替對某一個共享內存執行寫操做
- 一個進程執行了內存寫操做事後,不影響其餘進程對該內存的訪問,同時其餘進程對更新後的內存具備可見性
- 在進程執行寫操做時若是異常退出,對其餘進程的寫操做禁止自動解除
- 相對共享文件,數據訪問的方便性和效率
2)出現狀況:
- 獨佔的寫操做,相應有獨佔的寫操做等待隊列。獨佔的寫操做自己不會發生數據的一致性問題;
- 共享的寫操做,相應有共享的寫操做等待隊列。共享的寫操做則要注意防止發生數據的一致性問題;
- 獨佔的讀操做,相應有共享的讀操做等待隊列;
- 共享的讀操做,相應有共享的讀操做等待隊列;
3)Java中共享內存的實現:
JDK 1.4裏面的MappedByteBuffer爲開發人員在Java中實現共享內存提供了良好的方法,該緩衝區其實是
一個磁盤文件的內存映象,兩者的變化會保持同步,即內存數據發生變化事後會當即反應到磁盤文件中,這樣會有效地保證共享內存的實現,將共享文件和磁盤文件簡歷聯繫的是文件通道類:FileChannel,該類的加入是JDK爲了統一外圍設備的訪問方法,而且增強了多線程對同一文件進行存取的安全性,這裏能夠使用它來創建共享內存用,它創建了共享內存和磁盤文件之間的一個通道。打開一個文件可以使用RandomAccessFile類的getChannel方法,該方法直接返回一個文件通道,該文件通道因爲對應的文件設爲隨機存取,一方面能夠進行讀寫兩種操做,另一個方面使用它不會破壞映象文件的內容。這裏,若是使用FileOutputStream和FileInputStream則不能理想地實現共享內存的要求,由於這兩個類同時實現自由讀寫很困難。
下邊代碼段實現了上邊說起的共享內存功能
// 得到一個只讀的隨機存取文件對象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 得到相應的文件通道
FileChannel fc = RAFile.getChannel();
// 取得文件的實際大小
int size = (int)fc.size();
// 得到共享內存緩衝區,該共享內存只讀
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);git
// 得到一個可讀寫的隨機存取文件對象
RAFile = new RandomAccessFile(filename,"rw");程序員
// 得到相應的文件通道
fc = RAFile.getChannel();算法
// 取得文件的實際大小,以便映像到共享內存
size = (int)fc.size();編程
// 得到共享內存緩衝區,該共享內存可讀寫
mapBuf = fc.map(FileChannel.MAP_RW,0,size);bootstrap
// 獲取頭部消息:存取權限 數組
mode = mapBuf.getInt();
若是多個應用映象使用同一文件名的共享內存,則意味着這多個應用共享了同一內存數據,這些應用對於文件能夠具備同等存取權限,一個應用對數據的刷新會更新到多個應用中。爲了防止多個應用同時對共享內存進行寫操做,能夠在該共享內存的頭部信息加入寫操做標記,該共享文件的頭部基本信息至少有:
共享文件的頭部信息是私有信息,多個應用能夠對同一個共享內存執行寫操做,執行寫操做和結束寫操做的時候,能夠使用以下方法:
public boolean startWrite()
{
if(mode == 0) // 這裏mode表明共享內存的存取模式,爲0表明可寫
{
mode = 1; // 意味着別的應用不可寫
mapBuf.flip();
mapBuf.putInt(mode); //寫入共享內存的頭部信息
return true;
}
else{
return false; //代表已經有應用在寫該共享內存了,本應用不可以針對共享內存再作寫操做
}
}
public boolean stopWrite()
{
mode = 0; // 釋放寫權限
mapBuf.flip();
mapBuf.putInt(mode); //寫入共享內存頭部信息
return true;
}
【*:上邊提供了對共享內存執行寫操做過程的兩個方法,這兩個方法其實理解起來很簡單,真正須要思考的是一個針對
存取模式的設置,其實這種機制和最前面提到的內存的
鎖模式有點相似,一旦當mode(存取模式)設置稱爲可寫的時候,startWrite才能返回true,不只僅如此,某個應用程序在向共享內存寫入數據的時候還會修改其存取模式,由於若是不修改的話就會致使其餘應用一樣針對該內存是可寫的,這樣就使得共享內存的實現變得混亂,而在中止寫操做stopWrite的時候,須要將mode設置稱爲1,也就是上邊註釋段提到的
釋放寫權限。】
關於鎖的知識這裏簡單作個補充【*:上邊代碼的這種模式能夠理解爲一種簡單的鎖模式】:通常狀況下,計算機編程中會常常遇到鎖模式,在整個鎖模式過程當中能夠將鎖分爲兩類(這裏只是輔助理解,不是嚴格的鎖分類)——
共享鎖和
排他鎖(也稱爲獨佔鎖),鎖的定位是定位於針對全部與計算機有關的資源好比內存、文件、存儲空間等,針對這些資源均可能出現鎖模式。在上邊堆和棧一節講到了Java對象鎖,其實不只僅是對象,只要是計算機中會出現
寫入和讀取共同操做的資源,都有可能出現鎖模式。
共享鎖——當應用程序得到了資源的共享鎖的時候,那麼應用程序就能夠直接訪問該資源,資源的共享鎖能夠被多個應用程序拿到,在Java裏面線程之間有時候也存在對象的共享鎖,可是有一個很明顯的特徵,也就是內存共享鎖
只能讀取數據,不可以寫入數據,不管是什麼資源,當應用程序僅僅只能拿到該資源的共享鎖的時候,是不可以針對該資源進行寫操做的。
獨佔鎖——當應用程序得到了資源的獨佔鎖的時候,應用程序訪問該資源在共享鎖上邊多了一個權限就是寫權限,針對資源自己而言,
一個資源只有一把獨佔鎖,也就是說一個資源只能同時被一個應用或者一個執行代碼程序容許寫操做,Java線程中的對象寫操做也是這個道理,若某個應用拿到了獨佔鎖的時候,不只僅能夠讀取資源裏面的數據,並且能夠向該資源進行數據寫操做。
數據一致性——當資源同時被應用進行讀寫訪問的時候,有可能會出現數據一致性問題,好比A應用拿到了資源R1的獨佔鎖,B應用拿到了資源R1的共享鎖,A在針對R1進行寫操做,而兩個應用的操做——A的寫操做和B的讀操做出現了一個時間差,s1的時候B讀取了R1的資源,s2的時候A寫入了數據修改了R1的資源,s3的時候B又進行了第二次讀,而兩次讀取相隔時間比較短暫並且初衷沒有考慮到A在B的讀取過程修改了資源,這種狀況下針對鎖模式就須要考慮到數據一致性問題。獨佔鎖的排他性在這裏的意思是該鎖只能被一個應用獲取,獲取過程只能由這個應用寫入數據到資源內部,除非它釋放該鎖,不然其餘拿不到鎖的應用是沒法對資源進行寫入操做的。
按照上邊的思路去理解代碼裏面實現共享內存的過程就更加容易理解了。
若是執行寫操做的應用異常停止,那麼映像文件的共享內存將再也不能執行寫操做。爲了在應用異常停止後,寫操做禁止標誌自動消除,必須讓運行的應用獲知退出的應用。在多線程應用中,能夠用同步方法得到這樣的效果,可是在多進程中,同步是不起做用的。方法能夠採用的多種技巧,這裏只是描述一可能的實現:採用文件鎖的方式。寫共享內存應用在得到對一個共享內存寫權限的時候,除了判斷
頭部信息的寫權限標誌外,還要判斷一個臨時的鎖文件是否能夠獲得,若是能夠獲得,則即便頭部信息的寫權限標誌爲1(上述),也能夠
啓動寫權限,其實這已經代表寫權限得到的應用已經異常退出,這段代碼以下:
// 打開一個臨時文件,注意統一共享內存,該文件名必須相同,能夠在共享文件名後邊添加「.lock」後綴
RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
// 獲取文件通道
FileChannel lockFileChannel = files.getChannel();
// 獲取文件的獨佔鎖,該方法不產生任何阻塞直接返回
FileLock fileLock = lockFileChannel.tryLock();
// 若是爲空表示已經有應用佔有了
if( fileLock == null ){
// ...不可寫
}else{
// ...能夠執行寫操做
}
4)共享內存的應用:
在Java中,共享內存通常有兩種應用:
[1]永久對象配置——在java服務器應用中,用戶可能會在運行過程當中配置一些參數,而這些參數須要永久 有效,當服務器應用從新啓動後,這些配置參數仍然能夠對應用起做用。這就能夠用到該文 中的共享內存。該共享內存中保存了服務器的運行參數和一些對象運行特性。能夠在應用啓動時讀入以啓用之前配置的參數。
[2]查詢共享數據——一個應用(例 sys.java)是系統的服務進程,其系統的運行狀態記錄在共享內存中,其中運行狀態多是不斷變化的。爲了隨時瞭解系統的運行狀態,啓動另外一個應用(例 mon.java),該應用查詢該共享內存,彙報系統的運行狀態。
v.小節:
提供本機內存以及共享內存的知識,主要是爲了讓讀者可以更順利地理解JVM內部內存模型的物理原理,包括JVM如何和操做系統在內存這個級別進行交互,理解了這些內容就讓讀者對Java內存模型的認識會更加深刻,並且不容易遺忘。其實Java的內存模型遠不及咱們想象中那麼簡單,並且其結構極端複雜,看過《Inside JVM》的朋友應該就知道,結合JVM指令集去寫點小代碼測試.class文件的裏層結構也不失爲一種好玩的學習方法。
4.防止內存泄漏
Java中會有內存泄漏,聽起來彷佛是很不正常的,由於Java提供了垃圾回收器針對內存進行自動回收,可是Java仍是會出現內存泄漏的。
i.什麼是Java中的內存泄漏:
在Java語言中,
內存泄漏就是存在一些被分配的對象,這些對象有兩個特色:
這些對象可達,即在對象內存的有向圖中存在通路能夠與其相連;其次,這些對象是無用的,即程序之後不會再使用這些對象了。若是對象知足這兩個條件,該對象就能夠斷定爲Java中的內存泄漏,這些對象不會被GC回收,然而它卻佔用內存,這就是
Java語言中的內存泄漏。Java中的內存泄漏和C++中的內存泄漏還存在必定的區別,在C++裏面,內存泄漏的範圍更大一些,有些對象被分配了內存空間,可是卻不可達,因爲C++中沒有GC,這些內存將會永遠收不回來,在Java中這些不可達對象則是被GC負責回收的,所以程序員不須要考慮這一部分的內存泄漏。兩者的圖以下:
所以按照上邊的分析,Java語言中也是
存在內存泄漏的,可是其內存泄漏範圍比C++要小不少,由於Java裏面有個特殊程序回收全部的不可達對象:
垃圾回收器。對於程序員來講,GC基本是透明的,不可見的。雖然,咱們只有幾個函數能夠訪問GC,例如運行GC的函數System.gc(),可是根據Java語言規範定義,該函數
不保證JVM的垃圾收集器必定會執行。由於,不一樣的JVM實現者可能使用不一樣的算法管理GC。一般,GC的線程的優先級別較低,JVM調用GC的策略也有不少種,有的是內存使用到達必定程度時,GC纔開始工做,也有
定時執行的,有的是
平緩執行GC,有的是
中斷式執行GC。但一般來講,咱們不須要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對於基於Web的實時系統,如網絡遊戲等,用戶不但願GC忽然中斷應用程序執行而進行垃圾回收,那麼咱們須要調整GC的參數,讓GC可以經過平緩的方式釋放內存,例如將垃圾回收分解爲一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。
舉個例子:
——[$]內存泄漏的例子——
package org.susan.java.collection;
import java.util.Vector;
public class VectorMemoryLeak {
public static void main(String args[]){
Vector<String> vector = new Vector<String>();
for( int i = 0; i < 1000; i++ ){
String tempString = new String();
vector.add(tempString);
tempString = null;
}
}
}
從上邊這個例子能夠看到,循環申請了String對象,而且將申請的對象放入了一個Vector中,若是僅僅是釋放對象自己,由於Vector仍然引用了該對象,因此這個對象對CG來講是不可回收的,所以若是對象加入到Vector後,還必須從Vector刪除纔可以回收,最簡單的方式是將
Vector引用設置成null。實際上這些對象已經沒有用了,可是仍是被代碼裏面的引用引用到了,這種狀況GC拿它就沒有了任何辦法,這樣就能夠致使了內存泄漏。
【*:Java語言由於提供了垃圾回收器,照理說是不會出現內存泄漏的,Java裏面致使內存泄漏的主要緣由就是,先前申請了內存空間而忘記了釋放。若是程序中存在對無用對象的引用,這些對象就會駐留在內存中消耗內存,由於沒法讓GC判斷這些對象是否可達。若是存在對象的引用,這個對象就被定義爲「有效的活動狀態」,同時不會被釋放,要肯定對象所佔內存被回收,必需要確認該對象再也不被使用。典型的作法就是把對象數據成員設置成爲null或者中集合中移除,當局部變量不須要的狀況則不須要顯示聲明爲null。】
ii.常見的Java內存泄漏
1)全局集合:
在大型應用程序中存在各類各樣的全局數據倉庫是很廣泛的,好比一個JNDI樹或者一個Session table(會話表),在這些狀況下,必須注意管理
存儲庫的大小,必須有某種機制從存儲庫中
移除再也不須要的數據。
[$]解決:
[1]經常使用的解決方法是週期運做清除做業,該做業會驗證倉庫中的數據而後清楚一切不須要的數據
[2]另一種方式是
反向連接計數,集合負責統計集合中每一個入口的反向連接數據,這要求反向連接告訴集合合適會退出入口,當反向連接數目爲零的時候,該元素就能夠移除了。
2)緩存:
緩存一種用來快速查找已經執行過的操做結果的數據結構。所以,若是一個操做執行須要比較多的資源並會屢次被使用,一般作法是把經常使用的輸入數據的操做結果進行緩存,以便在下次調用該操做時使用緩存的數據。緩存一般都是以動態方式實現的,若是緩存設置不正確而大量使用緩存的話則會出現內存溢出的後果,所以須要將所使用的內存容量與檢索數據的速度加以平衡。
[$]解決:
[1]經常使用的解決途徑是使用java.lang.ref.SoftReference類堅持將對象放入緩存,這個方法能夠保證當虛擬機用完內存或者須要更多堆的時候,能夠釋放這些對象的引用。
3)類加載器:
Java類裝載器的使用爲內存泄漏提供了許多可乘之機。通常來講類裝載器都具備複雜結構,由於類裝載器不只僅是隻與"常規"對象引用有關,同時也和對象內部的引用有關。好比
數據變量,
方法和
各類類。這意味着只要存在對數據變量,方法,各類類和對象的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器能夠同不少的類關聯,同時也能夠和靜態數據變量關聯,那麼至關多的內存就可能發生泄漏。
iii.Java引用【摘錄自前邊的《Java引用總結》】:
Java中的對象引用主要有如下幾種類型:
1)強可及對象(strongly reachable):
能夠經過強引用訪問的對象,通常來講,咱們平時寫代碼的方式都是使用的
強引用對象,好比下邊的代碼段:
StringBuilder builder= new StringBuilder();
上邊代碼部分引用obj這個引用將引用內存堆中的一個對象,這種狀況下,只要obj的引用存在,垃圾回收器就永遠不會釋放該對象的存儲空間。這種對象咱們又成爲
強引用(Strong references),這種強引用方式就是Java語言的原生的Java引用,咱們幾乎天天編程的時候都用到。上邊代碼JVM存儲了一個StringBuilder類型的對象的強引用在變量builder呢。強引用和GC的交互是這樣的,若是一個對象經過強引用可達或者經過強引用鏈可達的話這種對象就成爲強可及對象,這種狀況下的對象垃圾回收器不予理睬。若是咱們開發過程不須要垃圾回器回收該對象,就直接將該對象賦爲強引用,也是普通的編程方法。
2)軟可及對象(softly reachable):
不經過強引用訪問的對象,即不是強可及對象,可是能夠經過
軟引用訪問的對象就成爲
軟可及對象,軟可及對象就須要使用類SoftReference(java.lang.ref.SoftReference)。此種類型的引用主要用於
內存比較敏感的高速緩存,並且此種引用仍是具備較強的引用功能,當內存不夠的時候GC會回收這類內存,所以若是
內存充足的時候,這種引用一般不會被回收的。不只僅如此,這種引用對象在JVM裏面
保證在拋出OutOfMemory異常以前,設置成爲null。通俗地講,這種類型的引用保證在JVM內存不足的時候所有被清除,可是有個關鍵在於:垃圾收集器在運行時是否釋放軟可及對象是不肯定的,並且使用垃圾回收算法並不能保證一次性尋找到全部的軟可及對象。當垃圾回收器每次運行的時候均可以隨意釋放不是強可及對象佔用的內存,若是垃圾回收器找到了軟可及對象事後,可能會進行如下操做:
- 將SoftReference對象的referent域設置成爲null,從而使該對象再也不引用heap對象。
- SoftReference引用過的內存堆上的對象一概被生命爲finalizable。
- 當內存堆上的對象finalize()方法被運行並且該對象佔用的內存被釋放,SoftReference對象就會被添加到它的ReferenceQueue,前提條件是ReferenceQueue自己是存在的。
既然Java裏面存在這樣的對象,那麼咱們在編寫代碼的時候如何建立這樣的對象呢?建立步驟以下:
先建立一個對象,並使用普通引用方式
【強引用】,而後再
建立一個SoftReference來引用該對象,最後將普通引用
設置爲null,經過這樣的方式,這個對象就僅僅保留了一個SoftReference引用,同時這種狀況咱們所建立的對象就是SoftReference對象。通常狀況下,咱們能夠使用該引用來完成Cache功能,就是前邊說的用於高速緩存,保證最大限度使用內存而不會引發內存泄漏的狀況。下邊的代碼段:
public static void main(String args[])
{
//建立一個強可及對象
A a = new A();
//建立這個對象的軟引用SoftReference
SoftReference sr = new SoftReference(a);
//將強引用設置爲空,以遍垃圾回收器回收強引用
a = null;
//下次使用該對象的操做
if( sr != null ){
a = (A)sr.get();
}else{
//這種狀況就是因爲內存太低,已經將軟引用釋放了,所以須要從新裝載一次
a = new A();
sr = new SoftReference(a);
}
}
軟引用技術使得Java系統能夠更好地管理內存,保持系統穩定,防止內存泄漏,避免系統崩潰,所以在處理一些內存佔用大並且生命週期長使用不頻繁的對象能夠使用該技術。
3)弱可及對象(weakly reachable):
不是強可及對象一樣也不是軟可及對象,僅僅經過弱引用WeakReference(java.lang.ref.WeakReference)訪問的對象,這種對象的用途在於
規範化映射(canonicalized mapping),對於生存週期相對比較長並且從新建立的時候開銷少的對象,弱引用也比較有用,和軟引用對象不一樣的是,垃圾回收器若是碰到了弱可及對象,將釋放WeakReference對象的內存,可是垃圾回收器須要運行不少次纔可以
找到
弱可及對象。弱引用對象在使用的時候,能夠配合ReferenceQueue類使用,若是弱引用被回收,JVM就會把這個弱引用加入到相關的引用隊列中去。最簡單的弱引用方法如如下代碼:
WeakReference weakWidget = new WeakReference(classA);
在上邊代碼裏面,當咱們使用weakWidget.get()來獲取classA的時候,因爲弱引用自己是沒法阻止垃圾回收的,因此咱們也許會拿到一個
null爲返回。【*:這裏提供一個小技巧,若是咱們但願取得某個對象的信息,可是又不影響該對象的垃圾回收過程,咱們就能夠使用WeakReference來記住該對象,通常咱們在開發調試器和優化器的時候使用這個是很好的一個手段。】
若是上邊的代碼部分,咱們經過weakWidget.get()返回的是null就證實該對象已經被垃圾回收器回收了,而這種狀況下弱引用對象就失去了使用價值,GC就會定義爲須要進行清除工做。這種狀況下弱引用沒法引用任何對象,因此在JVM裏面就成爲了一個
死引用,這就是爲何咱們有時候須要經過ReferenceQueue類來配合使用的緣由,使用了ReferenceQueue事後,就使得咱們更加容易監視該引用的對象,若是咱們經過一ReferenceQueue類來構造一個弱引用,當弱引用的對象已經被回收的時候,系統將自動使用對象引用隊列來代替對象引用,並且咱們能夠經過ReferenceQueue類的運行來決定是否真正要從垃圾回收器裏面將該
死引用(Dead Reference)清除。
弱引用代碼段:
//建立普通引用對象
MyObject object = new MyObject();
//建立一個引用隊列
ReferenceQueue rq = new ReferenceQueue();
//使用引用隊列建立MyObject的弱引用
WeakReference wr = new WeakReference(object,rq);
這裏提供兩個實在的場景來描述弱引用的相關用法:
[1]你想給對象附加一些信息,因而你用一個 Hashtable 把對象和附加信息關聯起來。你不停的把對象和附加信息放入 Hashtable 中,可是當對象用完的時候,你不得不把對象再從 Hashtable 中移除,不然它佔用的內存變不會釋放。萬一你忘記了,那麼沒有從 Hashtable 中移除的對象也能夠算做是內存泄漏。理想的情況應該是當對象用完時,Hashtable 中的對象會自動被垃圾收集器回收,否則你就是在作垃圾回收的工做。
[2]你想實現一個圖片緩存,由於加載圖片的開銷比較大。你將圖片對象的引用放入這個緩存,以便之後可以從新使用這個對象。可是你必須決定緩存中的哪些圖片再也不須要了,從而將引用從緩存中移除。無論你使用什麼管理緩存的算法,你實際上都在處理垃圾收集的工做,更簡單的辦法(除非你有特殊的需求,這也應該是最好的辦法)是讓垃圾收集器來處理,由它來決定回收哪一個對象。
當Java回收器遇到了弱引用的時候有可能會執行如下操做:
- 將WeakReference對象的referent域設置成爲null,從而使該對象再也不引用heap對象。
- WeakReference引用過的內存堆上的對象一概被生命爲finalizable。
- 當內存堆上的對象finalize()方法被運行並且該對象佔用的內存被釋放,WeakReference對象就會被添加到它的ReferenceQueue,前提條件是ReferenceQueue自己是存在的。
4)清除:
當引用對象的referent域設置爲null,而且引用類在內存堆中引用的對象聲明爲可結束的時候,該對象就能夠清除,清除不作過多的講述
5)虛可及對象(phantomly reachable):
不是
強可及對象,也不是
軟可及對象,一樣不是
弱可及對象,之因此把虛可及對象放到最後來說,主要也是由於它的特殊性,有時候咱們又稱之爲
「幽靈對象」,已經結束的,能夠經過虛引用來訪問該對象。咱們使用類PhantomReference(java.lang.ref.PhantomReference)來訪問,這個類只能用於跟蹤被引用對象進行的收集,一樣的,能夠用於執行per-mortern清除操做。PhantomReference必須與ReferenceQueue類一塊兒使用。須要使用ReferenceQueue是由於它可以充當通知機制,當垃圾收集器肯定了某個對象是虛可及對象的時候,PhantomReference對象就被放在了它的ReferenceQueue上,這就是一個通知,代表PhantomReference引用的對象已經結束,能夠收集了,通常狀況下咱們恰好在對象內存在回收以前採起該行爲。這種引用不一樣於弱引用和軟引用,這種方式經過get()獲取到的對象老是返回null,僅僅當這些對象在ReferenceQueue隊列裏面的時候,咱們能夠知道它所引用的哪些對對象是死引用(Dead Reference)。而這種引用和弱引用的區別在於:
弱引用(WeakReference)是在對象不可達的時候儘快進入ReferenceQueue隊列的,在finalization方法執行和垃圾回收以前是確實會發生的,理論上這類對象是不正確的對象,可是WeakReference對象能夠繼續保持Dead狀態,
虛引用(PhantomReference)是在對象確實已經從物理內存中移除事後才進入的ReferenceQueue隊列,並且get()方法會一直返回null
當垃圾回收器遇到了虛引用的時候將有可能執行如下操做:
- PhantomReference引用過的heap對象聲明爲finalizable;
- 虛引用在堆對象釋放以前就添加到了它的ReferenceQueue裏面,這種狀況使得咱們能夠在堆對象被回收以前採起操做【*:再次提醒,PhantomReference對象必須通過關聯的ReferenceQueue來建立,就是說必須和ReferenceQueue類配合操做】
看似沒有用處的虛引用,有什麼用途呢?
- 首先,咱們能夠經過虛引用知道對象究竟何時真正從內存裏面移除的,並且這也是惟一的途徑。
- 虛引用避過了finalize()方法,由於對於此方法的執行而言,虛引用真正引用到的對象是異常對象,若在該方法內要使用對象只能重建。通常狀況垃圾回收器會輪詢兩次,一次標記爲finalization,第二次進行真實的回收,而每每標記工做不能實時進行,或者垃圾回收其會等待一個對象去標記finalization。這種狀況頗有可能引發MemoryOut,而使用虛引用這種狀況就會徹底避免。由於虛引用在引用對象的過程不會去使得這個對象由Dead復活,並且這種對象是能夠在回收週期進行回收的。
在JVM內部,虛引用比起使用finalize()方法更加安全一點並且更加有效。而finaliaze()方法回收在虛擬機裏面實現起來相對簡單,並且也能夠處理大部分工做,因此咱們仍然使用這種方式來進行對象回收的掃尾操做,可是有了虛引用事後咱們能夠選擇是否手動操做該對象使得程序更加高效完美。
iv.防止內存泄漏[來自IBM開發中心]:
1)使用軟引用阻止泄漏:
[1]在Java語言中有一種形式的內存泄漏稱爲對象遊離(Object Loitering):
——[$]對象遊離——
// 注意,這段代碼屬於概念說明代碼,實際應用中不要模仿
public class LeakyChecksum{
private byte[] byteArray;
public synchronized int getFileCheckSum(String filename)
{
int len = getFileSize(filename);
if( byteArray == null || byteArray.length < len )
byteArray = new byte[len];
readFileContents(filename,byteArray);
// 計算該文件的值而後返回該對象
}
}
上邊的代碼是類LeakyChecksum用來講明對象遊離的概念,裏面有一個getFileChecksum()方法用來計算文件內容
校驗和,getFileCheckSum方法將文件內容讀取到緩衝區中計算校驗和,更加直觀的實現就是簡單地將緩衝區做爲getFileChecksum中的本地變量分配,可是上邊這個版本比這種版本更加「聰明」,不是將緩衝區緩衝在實例中字段中減小內存churn。該
「優化」一般不帶來預期的好處,對象分配比不少人指望的更加便宜。(還要注意,將緩衝區從本地變量提高到實例變量,使得類若不帶有附加的同步,就再也不是線程安全的了。直觀的實現不須要將 getFileChecksum() 聲明爲 synchronized,而且會在同時調用時提供更好的可伸縮性。)
這個類存在不少的問題,可是咱們着重來看內存泄漏。緩存緩衝區的決定極可能是根據這樣的假設得出的,即該類將在一個程序中被調用許屢次,所以它應該更加有效,以重用緩衝區而不是從新分配它。可是結果是,緩衝區永遠不會被釋放,由於它對程序來講老是可及的(除非LeakyChecksum對象被垃圾收集了)。更壞的是,它能夠增加,卻不能夠縮小,因此 LeakyChecksum 將永久保持一個與所處理的最大文件同樣大小的緩衝區。退一萬步說,這也會給垃圾收集器帶來壓力,而且要求更頻繁的收集;爲計算將來的校驗和而保持一個大型緩衝區並非可用內存的最有效利用。LeakyChecksum 中問題的緣由是,緩衝區對於 getFileChecksum() 操做來講邏輯上是本地的,可是它的生命週期已經被人爲延長了,由於將它提高到了實例字段。所以,該類必須本身管理緩衝區的生命週期,而不是讓 JVM 來管理。
這裏能夠提供一種策略就是使用Java裏面的軟引用:
弱引用如何能夠給應用程序提供當對象被程序使用時另外一種到達該對象的方法,可是不會延長對象的生命週期。Reference 的另外一個子類——軟引用——可知足一個不一樣卻相關的目的。其中
弱引用容許應用程序建立不妨礙垃圾收集的引用,
軟引用容許應用程序經過將一些對象指定爲 「expendable」 而利用垃圾收集器的幫助。儘管垃圾收集器在找出哪些內存在由應用程序使用哪些沒在使用方面作得很好,可是肯定可用內存的最適當使用仍是取決於應用程序。若是應用程序作出了很差的決定,使得對象被保持,那麼性能會受到影響,由於垃圾收集器必須更加辛勤地工做,以防止應用程序消耗掉全部內存。
高速緩存是一種常見的性能優化,容許應用程序重用之前的計算結果,而不是從新進行計算。高速緩存是 CPU 利用和內存使用之間的一種折衷,這種折衷理想的平衡狀態取決於有多少內存可用。若高速緩存太少,則所要求的性能優點沒法達到;若太多,則性能會受到影響,由於太多的內存被用於高速緩存上,致使其餘用途沒有足夠的可用內存。由於垃圾收集器比應用程序更適合決定內存需求,因此應該利用垃圾收集器在作這些決定方面的幫助,這就是件引用所要作的。若是一個對象唯一剩下的引用是
弱引用或軟引用,那麼該對象是
軟可及的(softly reachable)。垃圾收集器並不像其收集弱可及的對象同樣儘可能地收集軟可及的對象,相反,它只在真正
「須要」 內存時才收集軟可及的對象。軟引用對於垃圾收集器來講是這樣一種方式,即 「只要內存不太緊張,我就會保留該對象。可是若是內存變得真正緊張了,我就會去收集並處理這個對象。」 垃圾收集器在能夠拋出OutOfMemoryError 以前須要清除全部的軟引用。經過使用一個軟引用來管理高速緩存的緩衝區,能夠解決 LeakyChecksum中的問題,如上邊代碼所示。如今,只要不是特別須要內存,
緩衝區就會被
保留,可是在須要時,也可被垃圾收集器回收:
——[$]使用軟引用修復上邊代碼段——
public class CachingChecksum
{
private SoftReference<byte[]> bufferRef;
public synchronized int getFileChecksum(String filename)
{
int len = getFileSize(filename);
byte[] byteArray = bufferRef.get();
if( byteArray == null || byteArray.length < len )
{
byteArray = new byte[len];
bufferRef.set(byteArray);
}
readFileContents(filename,byteArray);
}
}
一種廉價緩存:
CachingChecksum使用一個
軟引用來緩存單個對象,並讓 JVM 處理從緩存中取走對象時的細節。相似地,軟引用也常常用於 GUI 應用程序中,用於
緩存位圖圖形。是否可以使用軟引用的關鍵在於,應用程序是否可從大量緩存的數據恢復。若是須要緩存不止一個對象,您能夠使用一個 Map,可是能夠選擇如何使用軟引用。您能夠將緩存做爲 Map<K, SoftReference<V>> 或SoftReference<Map<K,V>> 管理。後一種選項一般更好一些,由於它給垃圾收集器帶來的工做更少,而且容許在特別須要內存時以較少的工做回收整個緩存。弱引用有時會錯誤地用於取代軟引用,用於構建緩存,可是這會致使差的緩存性能。在實踐中,弱引用將在對象變得弱可及以後被很快地清除掉——一般是在緩存的對象再次用到以前——由於小的垃圾收集運行得很頻繁。對於在性能上很是依賴高速緩存的應用程序來講,
軟引用是一個
無論用的手段,它確實不能取代可以提供
靈活終止期、
複製和
事務型高速緩存的複雜的
高速緩存框架。可是做爲一種 「
廉價(cheap and dirty)」 的高速緩存機制,它對於下降價格是頗有吸引力的。正如弱引用同樣,軟引用也可建立爲具備一個相關的引用隊列,引用在被垃圾收集器清除時進入隊列。引用隊列對於軟引用來講,沒有對弱引用那麼有用,可是它們能夠用於發出
管理警報,說明應用程序開始
缺乏內存。
2)垃圾回收對引用的處理:
弱引用和軟引用都擴展了抽象的 Reference 類
虛引用(
phantom references
),引用對象被垃圾收集器特殊地看待。垃圾收集器在跟蹤堆期間遇到一個 Reference 時,不會標記或跟蹤該引用對象,而是在已知活躍的 Reference 對象的隊列上放置一個 Reference。在跟蹤以後,垃圾收集器就識別軟可及的對象——這些對象上除了軟引用外,沒有任何強引用。垃圾收集器而後根據當前收集所回收的內存總量和其餘策略考慮因素,判斷軟引用此時是否須要被清除。將被清除的軟引用若是具備相應的引用隊列,就會進入隊列。其他的軟可及對象
(沒有清除的對象)而後被看做一個
根集
(root set),堆跟蹤繼續使用這些新的根,以便經過活躍的軟引用而可及的對象可以被標記。處理軟引用以後,弱可及對象的集合被識別 —— 這樣的對象上不存在強引用或軟引用。這些對象被清除和加入隊列。全部 Reference 類型在加入隊列
以前被清除,因此處理過後檢查(post-mortem)清除的線程永遠不會具備 referent 對象的訪問權,而只具備Reference 對象的訪問權。所以,當 References 與引用隊列一塊兒使用時,一般須要細分適當的引用類型,並將它直接用於您的設計中(與 WeakHashMap 同樣,它的 Map.Entry 擴展了 WeakReference)或者存儲對須要清除的實體的引用。
3)使用弱引用堵住內存泄漏:
[1]全局Map形成的內存泄漏:
無心識對象保留最多見的緣由是使用 Map 將元數據與
臨時對象(transient object)相關聯。假定一個對象具備中等生命週期,比分配它的那個方法調用的生命週期長,可是比應用程序的生命週期短,如客戶機的套接字鏈接。須要將一些元數據與這個套接字關聯,如生成鏈接的用戶的標識。在建立 Socket 時是不知道這些信息的,而且不能將數據添加到 Socket 對象上,由於不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全局 Map 中存儲這些信息:
public class SocketManager{
private Map<Socket,User> m = new HashMap<Socket,User>();
public void setUser(Socket s,User u)
{
m.put(s,u);
}
public User getUser(Socket s){
return m.get(s);
}
public void removeUser(Socket s){
m.remove(s);
}
}
SocketManager socketManager;
//...
socketManager.setUser(socket,user);
這種方法的問題是元數據的生命週期須要與套接字的生命週期掛鉤,可是除非準確地知道何時程序再也不須要這個套接字,並記住從 Map 中刪除相應的映射,不然,Socket 和 User 對象將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 Socket 和User 對象被垃圾收集,即便應用程序不會再使用它們。這些對象留下來不受控制,很容易形成程序在長時間運行後內存爆滿。除了最簡單的狀況,在幾乎全部狀況下找出何時 Socket 再也不被程序使用是一件很煩人和容易出錯的任務,須要人工對內存進行管理。
[2]弱引用內存泄漏代碼:
程序有內存泄漏的第一個跡象一般是它拋出一個 OutOfMemoryError,或者由於頻繁的垃圾收集而表現出糟糕的性能。幸運的是,垃圾收集能夠提供可以用來診斷內存泄漏的大量信息。若是以 -verbose:gc 或者 -Xloggc 選項調用 JVM,那麼每次 GC 運行時在控制檯上或者日誌文件中會打印出一個診斷信息,包括它所花費的時間、當前堆使用狀況以及恢復了多少內存。記錄 GC 使用狀況並不具備干擾性,所以若是須要分析內存問題或者調優垃圾收集器,在生產環境中默認啓用 GC 日誌是值得的。有工具能夠利用 GC 日誌輸出並以圖形方式將它顯示出來,
JTune 就是這樣的一種工具。觀察 GC 以後堆大小的圖,能夠看到程序內存使用的趨勢。對於大多數程序來講,能夠將內存使用分爲兩部分:baseline 使用和 current load 使用。對於服務器應用程序,baseline 使用就是應用程序在沒有任何負荷、可是已經準備好接受請求時的內存使用,current load 使用是在處理請求過程當中使用的、可是在請求處理完成後會釋放的內存。只要負荷大致上是恆定的,應用程序一般會很快達到一個穩定的內存使用水平。若是在應用程序已經完成了其初始化而且負荷沒有增長的狀況下,內存使用持續增長,那麼程序就可能在處理前面的請求時保留了生成的對象。
public class MapLeaker{
public ExecuteService exec = Executors.newFixedThreadPool(5);
public Map<Task,TaskStatus> taskStatus
= Collections.synchronizedMap(new HashMap<Task,TaskStatus>());
private Random random = new Random();
private enum TaskStatus {
NOT_STARTED,
STARTED,
FINISHED };
private class Task implements Runnable{
private int[] numbers = new int[random.nextInt(200)];
public void run()
{
int[] temp = new int[random.nextInt(10000)];
taskStatus.put(this,TaskStatus.
STARTED);
doSomework();
taskStatus.put(this,TaskStatus.
FINISHED);
}
}
public Task newTask()
{
Task t = new Task();
taskStatus.put(t,TaskStatus.
NOT_STARTED);
exec.execute(t);
return t;
}
}
[3]使用弱引用堵住內存泄漏:
SocketManager 的問題是 Socket-User 映射的生命週期應當與 Socket 的生命週期相匹配,可是語言沒有提供任何容易的方法實施這項規則。這使得程序不得不使用人工內存管理的老技術。幸運的是,從
JDK 1.2 開始,垃圾收集器提供了一種聲明這種對象生命週期依賴性的方法,這樣垃圾收集器就能夠幫助咱們防止這種內存泄漏——
利用弱引用。弱引用是對一個對象
(稱爲
referent
)的引用的持有者。使用弱引用後,能夠維持對 referent 的引用,而不會阻止它被垃圾收集。當垃圾收集器跟蹤堆的時候,若是對一個對象的引用只有弱引用,那麼這個 referent 就會成爲垃圾收集的候選對象,就像沒有任何剩餘的引用同樣,並且全部剩餘的弱引用都被清除。
(只有弱引用的對象稱爲
弱可及(weakly reachable)
)WeakReference 的 referent 是在構造時設置的,在沒有被清除以前,能夠用 get() 獲取它的值。若是弱引用被清除了
(無論是 referent 已經被垃圾收集了,仍是有人調用了 WeakReference.clear()),get() 會返回
null。相應地,在使用其結果以前,應當老是檢查get() 是否返回一個非
null 值,由於 referent 最終老是會被垃圾收集的。用一個普通的(強)引用拷貝一個對象引用時,限制 referent 的生命週期至少與被拷貝的引用的生命週期同樣長。若是不當心,那麼它可能就與程序的生命週期同樣——若是將一個對象放入一個全局集合中的話。另外一方面,在建立對一個對象的弱引用時,徹底沒有擴展 referent 的生命週期,只是在對象仍然存活的時候,保持另外一種到達它的方法。弱引用對於構造弱集合最有用,如那些在應用程序的其他部分使用對象期間存儲關於這些對象的元數據的集合——這就是 SocketManager 類所要作的工做。由於這是弱引用最多見的用法,WeakHashMap 也被添加到
JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。若是在一個普通 HashMap 中用一個對象做爲鍵,那麼這個對象在映射從 Map 中刪除以前不能被回收,WeakHashMap 使您能夠用一個對象做爲 Map 鍵,同時不會阻止這個對象被垃圾收集。下邊的代碼給出了 WeakHashMap 的 get() 方法的一種可能實現,它展現了弱引用的使用:
public class WeakHashMap<K,V> implements Map<K,V>
{
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>
{
private V value;
private final int hash;
private Entry<K,V> next;
// ...
}
public V get(Object key)
{
int hash = getHash(key);
Entry<K,V> e = getChain(hash);
while(e != null)
{
k eKey = e.get();
if( e.hash == hash && (key == eKey || key.equals(eKey)))
return e.value;
e = e.next;
}
return null;
}
}
調用 WeakReference.get() 時,
它返回一個對 referent 的強引用(若是它仍然存活的話),所以不須要擔憂映射在 while 循環體中消失,由於強引用會防止它被垃圾收集。WeakHashMap 的實現展現了弱引用的一種常見用法——一些內部對象擴展 WeakReference。其緣由在下面一節討論引用隊列時會獲得解釋。在向 WeakHashMap 中添加映射時,請記住映射可能會在之後「脫離」,由於鍵被垃圾收集了。在這種狀況下,get() 返回 null,這使得測試 get() 的返回值是否爲 null 變得比平時更重要了。
[4]使用WeakHashMap堵住泄漏
在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就好了,以下邊代碼所示。
(若是
SocketManager
須要線程安全,那麼能夠用
Collections.synchronizedMap()
包裝
WeakHashMap
)。當映射的生命週期必須與鍵的生命週期聯繫在一塊兒時,能夠使用這種方法。不過,應當當心不濫用這種技術,大多數時候仍是應當使用普通的 HashMap 做爲 Map 的實現。
public class SocketManager{
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
public void setUser(Socket s, User s)
{
m.put(s,u);
}
public User getUser(Socket s)
{
return m.get(s);
}
}
引用隊列:
WeakHashMap 用弱引用承載映射鍵,這使得應用程序再也不使用鍵對象時它們能夠被垃圾收集,get() 實現能夠根據 WeakReference.get() 是否返回 null 來區分死的映射和活的映射。可是這只是防止 Map 的內存消耗在應用程序的生命週期中不斷增長所須要作的工做的一半,還須要作一些工做以便在鍵對象被收集後從 Map 中刪除死項。不然,Map 會充滿對應於死鍵的項。雖然這對於應用程序是不可見的,可是它仍然會形成應用程序耗盡內存,由於即便鍵被收集了,Map.Entry 和值對象也不會被收集。能夠經過週期性地掃描 Map,對每個弱引用調用 get(),並在 get() 返回 null 時刪除那個映射而消除死映射。可是若是 Map 有許多活的項,那麼這種方法的效率很低。若是有一種方法能夠在弱引用的 referent 被垃圾收集時發出通知就行了,這就是引用隊列的做用。引用隊列是垃圾收集器嚮應用程序返回關於對象生命週期的信息的主要方法。弱引用有兩個構造函數:一個只取 referent 做爲參數,另外一個還取引用隊列做爲參數。若是用關聯的
引用隊列建立弱引用,在 referent 成爲 GC 候選對象時,這個引用對象
(不是referent)就在引用清除後加入 到引用隊列中。以後,應用程序從引用隊列提取引用並瞭解到它的 referent 已被收集,所以能夠進行相應的清理活動,如去掉已不在弱集合中的對象的項。(引用隊列提供了與 BlockingQueue 一樣的出列模式 ——
polled、timed blocking 和 untimed blocking。)WeakHashMap 有一個名爲 expungeStaleEntries() 的私有方法,大多數 Map 操做中會調用它,它去掉引用隊列中全部失效的引用,並刪除關聯的映射。
4)關於Java中引用思考:
先觀察一個列表:
級別 |
回收時間 |
用途 |
生存時間 |
強引用 |
歷來不會被回收 |
對象的通常狀態 |
JVM中止運行時終止 |
軟引用 |
在內存不足時 |
在客戶端移除對象引用事後,除非再次激活,不然就放在內存敏感的緩存中 |
內存不足時終止 |
弱引用 |
在垃圾回收時,也就是客戶端已經移除了強引用,可是這種狀況下內存仍是客戶端引用可達的 |
阻止自動刪除不須要用的對象 |
GC運行後終止 |
虛引用[幽靈引用] |
對象死亡以前,就是進行finalize()方法調用附近 |
特殊的清除過程 |
不定,當finalize()函數運行事後再回收,有可能以前就已經被回收了。 |
能夠這樣理解:
SoftReference:假定垃圾回收器肯定在某一時間點某個對象是軟可到達對象。這時,它能夠選擇自動清除針對該對象的全部軟引用,以及經過強引用鏈,從其能夠到達該對象的針對任何其餘軟可到達對象的全部軟引用。在同一時間或晚些時候,它會將那些已經向引用隊列註冊的新清除的軟引用加入隊列。 軟可到達對象的全部軟引用都要保證在虛擬機拋出 OutOfMemoryError 以前已經被清除。不然,清除軟引用的時間或者清除不一樣對象的一組此類引用的順序將不受任何約束。然而,虛擬機實現不鼓勵清除最近訪問或使用過的軟引用。 此類的直接實例可用於實現簡單緩存;該類或其派生的子類還可用於更大型的數據結構,以實現更復雜的緩存。只要軟引用的指示對象是強可到達對象,即正在實際使用的對象,就不會清除軟引用。例如,經過保持最近使用的項的強指示對象,並由垃圾回收器決定是否放棄剩餘的項,複雜的緩存能夠防止放棄最近使用的項。通常來講,WeakReference咱們用來防止內存泄漏,保證內存對象被VM回收。
WeakReference:弱引用對象,它們並不由止其指示對象變得可終結,並被終結,而後被回收。弱引用最經常使用於實現
規範化的映射。假定垃圾回收器肯定在某一時間點上某個對象是弱可到達對象。這時,它將自動清除針對此對象的全部弱引用,以及經過
強引用鏈和軟引用,能夠從其到達該對象的針對任何其餘弱可到達對象的全部弱引用。同時它將聲明全部之前的弱可到達對象爲可終結的。在同一時間或晚些時候,它將那些已經向引用隊列註冊的新清除的弱引用加入隊列。 SoftReference多用做來實現cache機制,保證cache的有效性。
PhantomReference:虛引用對象,在回收器肯定其指示對象可另外回收以後,被加入隊列。虛引用最多見的用法是以某種可能比使用 Java 終結機制更靈活的方式來指派 pre-mortem 清除操做。若是垃圾回收器肯定在某一特定時間點上虛引用的指示對象是虛可到達對象,那麼在那時或者在之後的某一時間,它會將該引用加入隊列。爲了確保可回收的對象仍然保持原狀,虛引用的指示對象不能被檢索:虛引用的 get 方法老是返回 null。與軟引用和弱引用不一樣,虛引用在加入隊列時並無經過垃圾回收器
自動清除。經過虛引用可到達的對象將仍然保持原狀,直到全部這類引用都被清除,或者它們都變得
不可到達。
如下是不肯定概念
【*:Java引用的深刻部分一直都是討論得比較多的話題,上邊大部分爲摘錄整理,這裏再談談我我的的一些見解。從整個JVM框架結構來看,
Java的引用和
垃圾回收器造成了針對Java
內存堆的一個對象的
「閉包管理集」,其中在基本代碼裏面經常使用的就是強引用,強引用主要使用目的是就是編程的正常邏輯,這是全部的開發人員最容易理解的,而弱引用和軟引用的做用是比較回味無窮的。按照引用強弱,其排序能夠爲:
強引用——軟引用——弱引用——虛引用,爲何這樣寫呢,實際上針對垃圾回收器而言,強引用是它絕對不會隨便去動的區域,由於在內存堆裏面的對象,只有當前對象不是強引用的時候,該對象纔會進入
垃圾回收器的目標區域。
軟引用又能夠理解爲
「內存應急引用」,也就是說它和GC是完整地
配合操做的,爲了防止內存泄漏,當GC在回收過程出現
內存不足的時候,軟引用會被
優先回收,從垃圾回收算法上講,軟引用在設計的時候是
很容易被垃圾回收器發現的。爲何軟引用是處理告訴緩存的優先選擇的,主要有兩個緣由:第一,它對內存很是敏感,從抽象意義上講,咱們甚至能夠任何它和內存的變化牢牢綁定到一塊兒操做的,由於內存一旦不足的時候,它會優先向垃圾回收器
報警以提示
內存不足;第二,它會盡可能保證系統在OutOfMemoryError以前將對象直接設置成爲不可達,以保證不會出現內存溢出的狀況;因此使用軟引用來處理Java引用裏面的高速緩存是很不錯的選擇。其實軟引用
不只僅和內存敏感,實際上和垃圾回收器的交互也是
敏感的,這點能夠這樣理解,由於當內存不足的時候,軟引用會
報警,而這種報警會提示垃圾回收器針對目前的一些內存進行
清除操做,而在有軟引用存在的內存堆裏面,垃圾回收器會
第一時間反應,不然就會MemoryOut了。按照咱們正常的思惟來考慮,
垃圾回收器針對咱們調用System.gc()的時候,是不會輕易理睬的,由於僅僅是收到了來自強引用層代碼的請求,至於它是否回收還得看JVM內部
環境的條件是否知足,可是若是是軟引用的方式去申請垃圾回收器會
優先反應,只是咱們在開發過程不能控制軟引用對垃圾回收器發送垃圾回收申請,而JVM規範裏面也指出了軟引用不會
輕易發送申請到垃圾回收器。這裏還須要解釋的一點的是軟引用
發送申請
不是說軟引用像咱們調用System.gc()這樣直接申請垃圾回收,而是說
軟引用會設置對象引用爲
null,而垃圾回收器針對該引用的這種作法也會
優先響應,咱們能夠理解爲是軟引用對象在向垃圾回收器發送申請。反應快並不表明垃圾回收器會實時反應,仍是會在尋找軟引用引用到的對象的時候遵循必定的
回收規則,反應快在這裏的解釋是相對強引用設置對象爲null,當軟引用設置對象爲null的時候,該對象的被收集的
優先級比較高。
弱引用是一種比軟引用相對複雜的引用,其實
弱引用和軟引用都是Java程序能夠控制的,也就是說能夠經過代碼
直接使得引用針對
弱可及對象以及
軟可及對象是可引用的,軟引用和弱引用引用的對象實際上經過必定的代碼操做是可
從新激活的,只是通常不會作這樣的操做,這樣的用法
違背了最初的設計。弱引用和軟引用在垃圾回收器的目標範圍有一點點不一樣的就是,使用垃圾回收算法是很難找到弱引用的,也就是說弱引用用來監控垃圾回收的整個流程也是一種很好的選擇,它
不會影響垃圾回收的
正常流程,這樣就能夠規範化整個對象從設置爲null了事後的一個生命週期的代碼監控。並且由於弱引用是否存在對垃圾回收整個流程都不會形成影響,能夠這樣認爲,垃圾回收器
找獲得弱引用,該引用的對象就會被回收,若是
找不到弱引用,一旦等到GC完成了垃圾回收事後,弱引用引用的對象佔用的內存也會自動釋放,這就是軟引用在垃圾回收事後的自動終止。
最後談談
虛引用,虛引用應該是JVM裏面最厲害的一種引用,它的厲害在於它能夠在
對象的內存從
物理內存中清除掉了事後再引用該對象,也就是說當虛引用引用到對象的時候,這個對象實際已經從
物理內存堆中
清除掉了,若是咱們不用手動對
對象死亡或者
瀕臨死亡進行處理的話,JVM會默認調用finalize函數,可是虛引用存在於該函數附近的
生命週期內,因此能夠手動對對象的這個範圍的週期進行
監控。它之因此稱爲
「幽靈引用」就是由於該對象的物理內存已經不存在的,我我的以爲JVM保存了一個對象狀態的
鏡像索引,而這個鏡像索引裏面包含了對象在這個生命週期須要的全部內容,這裏的所須要就是
這個生命週期內須要的對象數據內容,也就是
對象死亡和瀕臨死亡以前finalize函數附近,至於強引用所須要的其餘對象附加內容是不須要在這個鏡像裏面包含的,因此即便物理內存不存在,仍是能夠經過
虛引用監控到該對象的,只是這種狀況是否可讓對象從新激活爲強引用我就不敢說了。由於虛引用在引用對象的過程不會去使得這個對象由Dead復活,並且這種對象是能夠在回收週期進行回收的。
】