IO流為什么必須手動(dòng)關(guān)閉,不能像其他的對(duì)象坐等GC回收?
一、問題回溯
在項(xiàng)目的開發(fā)過程中,當(dāng)我們對(duì)文件進(jìn)行讀寫操作時(shí),不知道大家有沒有碰到這樣的問題。
有的同學(xué)在做一個(gè)讀取臨時(shí)文件數(shù)據(jù)的工作,當(dāng)讀完文件內(nèi)容,準(zhǔn)備將其刪除的時(shí)候,有時(shí)候會(huì)正常,但有時(shí)候會(huì)提示:操作無法完成,因?yàn)槲募言?Java? Platform SE binary 中打開,編譯器也會(huì)提示:Resource leak: 'xxxx' is never closed。
樣例代碼如下:
File file = new File("xxx.txt");
// 實(shí)例化輸入流
FileReader reader = new FileReader(file);
// 緩沖區(qū)
char[] buffer = new char[1024];
// 分次讀取數(shù)據(jù),每次最多讀取1024個(gè)字符,將數(shù)據(jù)讀取到緩沖區(qū)之中,同時(shí)返回讀取的字節(jié)個(gè)數(shù)
int len;
while ((len = reader.read(buffer)) > -1) {
// 字符轉(zhuǎn)為字符串
String msg = new String(buffer, 0, len);
System.out.println(msg);
}
// 刪除文件
file.delete();
經(jīng)過排查,發(fā)現(xiàn)出現(xiàn)該問題的原因是:讀取文件的 IO 流沒有正常的關(guān)閉,導(dǎo)致文件一直被流持有,刪除文件不成功!
那這么解決這個(gè)問題呢?答案其實(shí)也很簡(jiǎn)單,當(dāng)讀完 IO 流的數(shù)據(jù)或者寫完數(shù)據(jù),手動(dòng)調(diào)用一下關(guān)閉流的方法,最后再進(jìn)行刪除文件。
// 刪除文件之前,先將 IO 流關(guān)閉
reader.close();
// 刪除文件
file.delete();
可能有的同學(xué)會(huì)發(fā)出疑問,為什么 IO 流必須手動(dòng)關(guān)閉,不能像其他的方法一樣坐等 GC 回收?
今天我們就一起來聊聊這個(gè)話題,以及如何正確的關(guān)閉 IO 流操作。
二、為什么 IO 流需要手動(dòng)關(guān)閉?
熟悉編程語(yǔ)言的同學(xué),可能知道,無論是 C 語(yǔ)言還是 C++,都需要手動(dòng)釋放內(nèi)存,但是 Java 不需要。
這主要得益于 Java 的虛擬機(jī)垃圾回收機(jī)制,它可以幫助開發(fā)者自動(dòng)回收內(nèi)存中的對(duì)象,不需要手動(dòng)釋放內(nèi)存,但是有些東西它是無法回收的,例如端口、顯存、文件等,超出了虛擬機(jī)能夠釋放資源的界限。
如果對(duì)未關(guān)閉流的文件進(jìn)行讀寫操作,可能就會(huì)報(bào)錯(cuò),告訴你這個(gè)文件被某個(gè)進(jìn)程占用。如果不手動(dòng)釋放資源,隨著資源占有量逐漸增多,垃圾會(huì)越來越多,最終可能導(dǎo)致系統(tǒng)無法存儲(chǔ)其他的資源,甚至?xí)霈F(xiàn)系統(tǒng)崩潰。
一般來說,只要存在 IO 流讀寫操作,無論使用到的是網(wǎng)絡(luò) IO 或者文件 IO,都是需要和計(jì)算機(jī)內(nèi)的資源打交道的,清理計(jì)算機(jī)上面的垃圾,Java 的虛擬機(jī)垃圾回收機(jī)制沒有這個(gè)能力。
熟悉 Java 虛擬機(jī)垃圾回收機(jī)制的同學(xué),可能知道 gc 有兩個(gè)顯著的特點(diǎn):
- gc 只能釋放內(nèi)存資源,而不能釋放與內(nèi)存無關(guān)的資源
- gc 回收具有不確定性,也就是說你根本不知道它什么時(shí)候會(huì)回收
所以進(jìn)行流的操作時(shí),凡是跨出虛擬機(jī)邊界的資源都要求程序員自己手動(dòng)關(guān)閉資源。
可能有的同學(xué)又發(fā)出疑問,我平時(shí)本地測(cè)試的時(shí)候沒有發(fā)現(xiàn)這個(gè)問題,為什么部署到線上就出這個(gè)提示的呢?
以讀取文件的FileInputStream流為例,其實(shí)里面隱含了一個(gè)finalize方法,當(dāng)虛擬機(jī)進(jìn)行垃圾回收之前,會(huì)調(diào)用這個(gè)方法。
打開源碼,你會(huì)發(fā)現(xiàn)底層調(diào)用的其實(shí)是close釋放資源的方法,可以看到 JDK 間接的幫助開發(fā)者進(jìn)行最后一次的兜底。
/**
* Ensures that the <code>close</code> method of this file input stream is
* called when there are no more references to it.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FileInputStream#close()
*/
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
這就解釋了,為什么只是時(shí)不時(shí)的會(huì)出現(xiàn)提示,并不是總是。這個(gè)方法什么時(shí)候被調(diào)用,這取決于虛擬機(jī)的垃圾回收頻次。
但是在實(shí)際的開發(fā)過程中,開發(fā)者不能完全依賴虛擬機(jī)幫你回收這些系統(tǒng)資源,只要涉及到流的操作,強(qiáng)烈建議大家一定要手動(dòng)關(guān)閉釋放資源,避免出現(xiàn)一些不必要的bug。
具體如何手動(dòng)釋放資源資源呢,我們接著看!
三、正確的關(guān)閉流姿勢(shì)介紹
我們深知在操作 Java 流對(duì)象后要將流進(jìn)行關(guān)閉,但是現(xiàn)實(shí)的情況卻往往不盡人意,原因是每個(gè)開發(fā)者的寫法可能不盡相同,不同的寫法導(dǎo)致出現(xiàn)各種千奇百怪的問題,下面我們一起來看看幾種關(guān)閉流的代碼案例!
寫法 1:在 try 中關(guān)流,而沒在 finally 中關(guān)流
try {
OutputStream out = new FileOutputStream("file");
// ...操作流代碼
out.close();
} catch (Exception e) {
e.printStackTrace();
}
當(dāng)操作流代碼報(bào)錯(cuò)的時(shí)候,這種寫法會(huì)導(dǎo)致流無法正常的關(guān)閉,因此不推薦采用!
正確的操作方式,應(yīng)該在finally里面完成,實(shí)例代碼如下:
OutputStream out = null;
try {
out = new FileOutputStream("file");
// ...操作流代碼
} catch (Exception e) {
e.printStackTrace();
} finally {
// 在 finally 中進(jìn)行關(guān)閉,確保一定能被執(zhí)行
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
寫法 2:在關(guān)閉多個(gè)流時(shí),將其放在一個(gè) try 中
在關(guān)閉多個(gè)流時(shí),有的同學(xué)嫌棄麻煩,將其放在一個(gè) try 中完成,實(shí)例代碼如下:
OutputStream out1 = null;
OutputStream out2 = null;
try {
out1 = new FileOutputStream("file");
out2 = new FileOutputStream("file");
// ...操作流代碼
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out1 != null) {
// 如果此處出現(xiàn)異常,則out2流沒有被關(guān)閉
out1.close();
}
if (out2 != null) {
out2.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
這種寫法下,當(dāng)out1.close出異常的時(shí)候,out2.close是不會(huì)被正常關(guān)閉的,因此不推薦采用!
正確的操作方式,應(yīng)該是一個(gè)一個(gè)的close,別偷懶,實(shí)例代碼如下:
OutputStream out1 = null;
OutputStream out2 = null;
try {
out1 = new FileOutputStream("file");
out2 = new FileOutputStream("file");
// ...操作流代碼
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out1 != null) {
out1.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (out2 != null) {
out2.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
寫法 3:在循環(huán)中創(chuàng)建流,在循環(huán)外關(guān)閉
有的同學(xué)在循環(huán)操作多個(gè)文件時(shí),在循環(huán)外關(guān)閉文件流,實(shí)例代碼如下:
OutputStream out = null;
try {
for (int i = 0; i < 10; i++) {
out = new FileOutputStream("file");
// ...操作流代碼
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
表面看上去好像沒有問題,但是實(shí)際上創(chuàng)建了 10 個(gè) IO 流,try 里面的邏輯執(zhí)行完成之后,只是把最后的一個(gè) IO 流對(duì)象賦予給了out參數(shù)。也就是當(dāng)程序執(zhí)行完畢之后,只關(guān)閉了最后一個(gè) IO 流,其它 9 個(gè) IO 流沒用被手動(dòng)關(guān)閉,因此不推薦采用!
正確的操作方式,應(yīng)該是在循環(huán)體內(nèi)close,別偷懶,實(shí)例代碼如下:
for (int i = 0; i < 10; i++) {
OutputStream out = null;
try {
out = new FileOutputStream("file");
// ...操作流代碼
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
寫法 4:關(guān)閉多個(gè)流時(shí),沒用遵循后定義先釋放原則
有的同學(xué)在操作多個(gè)文件流時(shí),操作完成之后,依照先后次序進(jìn)行關(guān)閉文件流,實(shí)例代碼如下:
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
fos = new FileOutputStream("file");
bos = new BufferedOutputStream(fos);
// ...操作流代碼
} catch (Exception e){
} finally {
// 依次關(guān)閉流
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
// 此處會(huì)報(bào) java.io.IOException: Stream Closed 錯(cuò)誤
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
按照先后順序關(guān)閉文件流,這種寫法下,有可能會(huì)報(bào)java.io.IOException: Stream Closed錯(cuò)誤。
原因是BufferedOutputStream依賴于FileOutputStream,如果直接關(guān)閉FileOutputStream流,再次關(guān)閉BufferedOutputStream,會(huì)提示源頭已經(jīng)被關(guān)閉,緩存區(qū)數(shù)據(jù)無法輸出。
正確的操作方式,應(yīng)該遵循后定義先釋放的原則,實(shí)例代碼如下:
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
fos = new FileOutputStream("file");
bos = new BufferedOutputStream(fos);
// ...操作流代碼
} catch (Exception e){
} finally {
// 后定義先釋放
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
寫法 5:jdk7 及以上版本,推薦采用 try-with-resources 寫法
try-with-resources是 JDK 7 中引入的一個(gè)新的異常處理機(jī)制,它能讓開發(fā)人員不用顯式的釋放try-catch語(yǔ)句塊中使用的資源。
以上文為例,可以改成如下寫法:
try (FileOutputStream fos = new FileOutputStream("file");
BufferedOutputStream bos = new BufferedOutputStream(fos)){
// ...操作流代碼
} catch (Exception e){
e.printStackTrace();
}
try-with-resources釋放資源的操作,也是遵循的后定義先釋放的原則!
寫法 6:使用包裝流時(shí),只需要關(guān)閉最后面的包裝流即可
包裝流是指通過裝飾設(shè)計(jì)模式實(shí)現(xiàn)的 IO 流類,其目的是對(duì)底層流的功能進(jìn)行擴(kuò)展,在實(shí)際數(shù)據(jù)傳輸?shù)臅r(shí)候,還是使用底層流進(jìn)行傳輸。比如緩存字節(jié)輸出流BufferedOutputStream就是一個(gè)包裝流,目的是對(duì)字節(jié)輸出流提供一個(gè)緩存區(qū)功能,讓數(shù)據(jù)輸出效率更高。
在使用到包裝流的時(shí)候,我們只需要關(guān)閉最后面的包裝流即可。
以上文為例,改寫的實(shí)例代碼如下:
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
try {
is = new FileInputStream("file");
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
// ...操作流代碼
} catch (Exception e){
e.printStackTrace();
} finally {
// 關(guān)閉包裝流,也會(huì)自動(dòng)關(guān)閉 InputStream 流
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
這是因?yàn)?,包裝流關(guān)閉時(shí)會(huì)調(diào)用原生流的關(guān)閉方法,請(qǐng)看源碼!
public void close() throws IOException {
synchronized (lock) {
if (in == null)
return;
try {
// 這里的in 指的是 InputStreamReader,最后會(huì)原生流的close方法
in.close();
} finally {
in = null;
cb = null;
}
}
}
四、內(nèi)存流是否需要關(guān)閉?
在上文中,我們提到只要是 IO 流都建議大家手機(jī)關(guān)閉資源,但是在 Java 中有一種流,它是不需要手動(dòng)關(guān)閉的,比如內(nèi)存讀寫流:ByteArrayInputStream、ByteArrayOutputStream。
不同于指向硬盤的流,ByteArrayInputStream和ByteArrayOutputStream其實(shí)是偽裝成流的字節(jié)數(shù)組存儲(chǔ)在內(nèi)存中(把它們當(dāng)成字節(jié)數(shù)據(jù)來看就好了),他們不會(huì)鎖定任何文件句柄和端口,如果不再被使用,字節(jié)數(shù)組會(huì)被垃圾回收掉,所以不需要關(guān)閉。
當(dāng) IO 流是指向存儲(chǔ)卡 / 硬盤 / 網(wǎng)絡(luò)等外部資源的流,是一定要手動(dòng)關(guān)閉的。
五、小結(jié)
本位主要圍繞【為什么 IO 流必須手動(dòng)關(guān)閉,不能像其他的方法坐等 GC 處理】這個(gè)話題進(jìn)行一次內(nèi)容的整合和總結(jié),同時(shí)也給出了推薦的正確關(guān)閉 IO 流的寫法。
在實(shí)際的開發(fā)過程中,建議大家正確的使用 IO 流,以免出現(xiàn)各種 bug !
內(nèi)容難免有所遺漏,歡迎網(wǎng)友留言指出。
六、參考
1、csdn - 演員12138 - IO流為什么必須手動(dòng)關(guān)閉,不能像其他的方法坐等GC處理
2、csdn - 思想永無止境 - Java之關(guān)閉流