前段時間寫了一個java socket相關的程序,大概意思就是client和server是采用socket長連接方式,之間通信都是通過通過ObjectOutputStream和OjbectInputStream來進行寫和讀操作。
其實以前就很多次的用到過ObjectOutputStream,不過沒有詳細的琢磨過,這次就想著琢磨一下,主要也是因為我發(fā)現(xiàn)程序中存在內存泄漏的問題,通過Jprobe跟蹤,排除了別的泄漏因素,最后定位在是在socket這里發(fā)生了泄漏,具體情況下面進行分析。
先來說說ObjectOutputStream,通過ObjectOutputStream來進行socket寫入,那么會在流中加入Object的信息,也就是說如果想要是跨平臺的socket通信那么可能會帶來一些問題,因為數(shù)據(jù)流中加入的是java特有的信息,class類型以及成員變量的類型信息,同樣通過ObjectInputStream來讀取也有相應的規(guī)則來進行解析。
下面我們來看一下寫入的信息,首先有一個簡單的類
我們通過ObjectOutputStream來把該類寫入文件中,查看一下寫入的內容
我們可以看到寫入的內容中包含了寫入類的類型以及成員變量信息,當然關于插入的內容,我們可以覆蓋ObjectOutputStream類的writeStreamHeader()方法來實現(xiàn)插入我們自定義的內容,當然如果這樣做的話,我們就必須對ObjectInputStream類進行重寫
上面是一些題外話,下面回到正題,關于我標題中提到的有內存泄漏的問題。為了更清晰直觀的說明該問題,我又寫了一個很簡單的測試,代碼如下:
我在這里循環(huán)了20次,那么大家可以猜想一下文件中會是什么內容,可能會有人認為是我剛才在上面貼出的內容重復20遍,起初我也是這么認為的,但事實不是這么回事。
我現(xiàn)在更改一下程序,不再循環(huán),只寫入一次,那么寫入的內容是
循環(huán)10次
循環(huán)20次
這樣的測試我們還是不夠清晰,下面我們讓他每次循環(huán)寫入的對象的屬性都不相同,我們這樣修改一下代碼
那么這個時候文件里的內容是什么呢,我們再來看一下
這樣就顯而易見了,我們雖然寫入了10次,但是不會每次寫入都會插入寫入對象和成員變量類型的信心,而是在第一次寫入的時候插入一些頭信息,以后再寫就不會再插入了。這實際是java做的優(yōu)化,通過該優(yōu)化從而減少socket傳輸?shù)拈_銷。
那么會有人問了,你說的內存泄漏的問題呢,寫到這里,我想應該有人已經看出問題來了,它之所以可以這么做優(yōu)化,前提是持有MyObject的引用,也就是說,不會釋放掉MyObject的引用。現(xiàn)在明白了吧,如果你是長連接的方式,ObjectOutputStream會一直持有你以前發(fā)送過的對象的引用,從而導致jvm在進行垃圾回收的時候不能回收之前發(fā)送的對象的實例,經過漫長時間的運行,最終導致內存溢出了。這一點從我通過Jprobe跟蹤也得到了印證。
下面我們來談談如何避免該問題,說著這里我們就得提到ObjectOutputStream的reset方法了,JDK文檔中是這么解釋該方法的:
“重置將丟棄已寫入流中的所有對象的狀態(tài)。重新設置狀態(tài),使其與新的 ObjectOutputStream 相同。將流中的當前點標記為 reset,相應的 ObjectInputStream 也將在這一點重置。以前寫入流中的對象不再被視為正位于流中。它們會再次被寫入流。”
就是說調用reset那么就丟棄所持有對象的狀態(tài)(也就是釋放掉了對對象的應用),同時會在流中設置reset標識。
還是之前那個例子,我們來修改一下代碼,在每次寫入后都調用一下reset方法
我們再來看一下寫入文件內容
這次跟之前不同的,每一次寫入都加入了頭信息且每一次末尾都加入了y,我想這個標識應該就是reset標識,至于具體是什么,我們沒必要深究了。
通過上面一系列的測試,我們大概對Object流有了一定了解,那么具體到我們日常編碼中到底該不該調用reset呢,這個我想不能一概而論了,我們通過測試也看到了,在不調用reset的方式下,java的優(yōu)化對于減輕socket開銷還是很可觀的,當然代價是有的,那就是直到你調用reset或者是關閉輸出流之前,對于發(fā)送過的對象的實例是不會釋放的。
結論:當然只是我自己的片面之詞。如果你的程序需要很長時間的運行,我建議你還是調用reset避免最后內存溢出程序崩潰,但是如果你又要長時間運行,且發(fā)送的消息量又很大,那么調用reset無疑會增加開銷,那么這個時候最好的做法我覺得是你自己實現(xiàn)一套機制,定時的調用reset或者是定量,比如查看到內存已經漲到一個水平后調用一下,這樣既可以避免內存無限的增長下去,又可以減少不少socket通信的開銷
anson在這里感謝大家花了這么長時間閱讀該文章,希望能給大家?guī)Я艘恍椭硗馍厦娴姆治龆际俏覀€人的理解,肯定存在一定的局限性,大家有什么更深刻的認識,還請大家指出來,我們一起交流,共同進步
其實以前就很多次的用到過ObjectOutputStream,不過沒有詳細的琢磨過,這次就想著琢磨一下,主要也是因為我發(fā)現(xiàn)程序中存在內存泄漏的問題,通過Jprobe跟蹤,排除了別的泄漏因素,最后定位在是在socket這里發(fā)生了泄漏,具體情況下面進行分析。
先來說說ObjectOutputStream,通過ObjectOutputStream來進行socket寫入,那么會在流中加入Object的信息,也就是說如果想要是跨平臺的socket通信那么可能會帶來一些問題,因為數(shù)據(jù)流中加入的是java特有的信息,class類型以及成員變量的類型信息,同樣通過ObjectInputStream來讀取也有相應的規(guī)則來進行解析。
下面我們來看一下寫入的信息,首先有一個簡單的類
class MyObject implements Serializable { private static final long serialVersionUID = -9163423175612080544L; String str1; String str2; }
我們通過ObjectOutputStream來把該類寫入文件中,查看一下寫入的內容
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~
我們可以看到寫入的內容中包含了寫入類的類型以及成員變量信息,當然關于插入的內容,我們可以覆蓋ObjectOutputStream類的writeStreamHeader()方法來實現(xiàn)插入我們自定義的內容,當然如果這樣做的話,我們就必須對ObjectInputStream類進行重寫
上面是一些題外話,下面回到正題,關于我標題中提到的有內存泄漏的問題。為了更清晰直觀的說明該問題,我又寫了一個很簡單的測試,代碼如下:
FileOutputStream fos = new FileOutputStream("c:\\test.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); MyObject myObj = new MyObject(); myObj.str1 = "test1"; myObj.str2 = "test2"; for (int i = 0; i < 20; i++) { oos.writeObject(myObj); oos.writeObject(myObj); } fos.close();
我在這里循環(huán)了20次,那么大家可以猜想一下文件中會是什么內容,可能會有人認為是我剛才在上面貼出的內容重復20遍,起初我也是這么認為的,但事實不是這么回事。
我現(xiàn)在更改一下程序,不再循環(huán),只寫入一次,那么寫入的內容是
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~
循環(huán)10次
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~
循環(huán)20次
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~
這樣的測試我們還是不夠清晰,下面我們讓他每次循環(huán)寫入的對象的屬性都不相同,我們這樣修改一下代碼
FileOutputStream fos = new FileOutputStream("c:\\test.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); for (int i = 0; i < 10; i++) { MyObject myObj = new MyObject(); myObj.str1 = "test1" + i; myObj.str2 = "test2" + i; oos.writeObject(myObj); oos.writeObject(myObj); } fos.close();
那么這個時候文件里的內容是什么呢,我們再來看一下
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test10t test20q ~ sq ~ t test11t test21q ~ sq ~ t test12t test22q ~ sq ~ t test13t test23q ~ sq ~ t test14t test24q ~ sq ~ t test15t test25q ~ sq ~ t test16t test26q ~ sq ~ t test17t test27q ~ sq ~ t test18t test28q ~ sq ~ t test19t test29q ~
這樣就顯而易見了,我們雖然寫入了10次,但是不會每次寫入都會插入寫入對象和成員變量類型的信心,而是在第一次寫入的時候插入一些頭信息,以后再寫就不會再插入了。這實際是java做的優(yōu)化,通過該優(yōu)化從而減少socket傳輸?shù)拈_銷。
那么會有人問了,你說的內存泄漏的問題呢,寫到這里,我想應該有人已經看出問題來了,它之所以可以這么做優(yōu)化,前提是持有MyObject的引用,也就是說,不會釋放掉MyObject的引用。現(xiàn)在明白了吧,如果你是長連接的方式,ObjectOutputStream會一直持有你以前發(fā)送過的對象的引用,從而導致jvm在進行垃圾回收的時候不能回收之前發(fā)送的對象的實例,經過漫長時間的運行,最終導致內存溢出了。這一點從我通過Jprobe跟蹤也得到了印證。
下面我們來談談如何避免該問題,說著這里我們就得提到ObjectOutputStream的reset方法了,JDK文檔中是這么解釋該方法的:
“重置將丟棄已寫入流中的所有對象的狀態(tài)。重新設置狀態(tài),使其與新的 ObjectOutputStream 相同。將流中的當前點標記為 reset,相應的 ObjectInputStream 也將在這一點重置。以前寫入流中的對象不再被視為正位于流中。它們會再次被寫入流。”
就是說調用reset那么就丟棄所持有對象的狀態(tài)(也就是釋放掉了對對象的應用),同時會在流中設置reset標識。
還是之前那個例子,我們來修改一下代碼,在每次寫入后都調用一下reset方法
FileOutputStream fos = new FileOutputStream("c:\\test.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); for (int i = 0; i < 10; i++) { MyObject myObj = new MyObject(); myObj.str1 = "test1" + i; myObj.str2 = "test2" + i; oos.writeObject(myObj); oos.writeObject(myObj); oos.reset(); } fos.close();
我們再來看一下寫入文件內容
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test10t test20q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test11t test21q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test12t test22q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test13t test23q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test14t test24q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test15t test25q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test16t test26q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test17t test27q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test18t test28q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test19t test29q ~ y
這次跟之前不同的,每一次寫入都加入了頭信息且每一次末尾都加入了y,我想這個標識應該就是reset標識,至于具體是什么,我們沒必要深究了。
通過上面一系列的測試,我們大概對Object流有了一定了解,那么具體到我們日常編碼中到底該不該調用reset呢,這個我想不能一概而論了,我們通過測試也看到了,在不調用reset的方式下,java的優(yōu)化對于減輕socket開銷還是很可觀的,當然代價是有的,那就是直到你調用reset或者是關閉輸出流之前,對于發(fā)送過的對象的實例是不會釋放的。
結論:當然只是我自己的片面之詞。如果你的程序需要很長時間的運行,我建議你還是調用reset避免最后內存溢出程序崩潰,但是如果你又要長時間運行,且發(fā)送的消息量又很大,那么調用reset無疑會增加開銷,那么這個時候最好的做法我覺得是你自己實現(xiàn)一套機制,定時的調用reset或者是定量,比如查看到內存已經漲到一個水平后調用一下,這樣既可以避免內存無限的增長下去,又可以減少不少socket通信的開銷

anson在這里感謝大家花了這么長時間閱讀該文章,希望能給大家?guī)Я艘恍椭硗馍厦娴姆治龆际俏覀€人的理解,肯定存在一定的局限性,大家有什么更深刻的認識,還請大家指出來,我們一起交流,共同進步
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
