有趣的Java對象序列化緩存問題
【51CTO特稿】在這里我們將通過幾個有趣的例子,來演示Java對象序列化緩存問題。下面這個程序非常神奇,用了不到4秒的時間就向我的硬盤上輸出了1000TB的數據。不要懷疑你看錯了,確實是不到4秒時間就輸出1000TB的數據,不相信你也可以在你的電腦上運行一下這個程序。如果你的硬盤不夠大也不用擔心,Java完全可以自己解決硬盤容量問題。這個例子對你的電腦***的要求就是必須有256M以上的內存,并且要設置執行參數為-Xmx256m。相信現在沒有誰的電腦內存是不夠256M的。
- import java.io.*;
- public class SuperFastWriter {
- private static final long TERA_BYTE = 1024L * 1024 * 1024 * 1024;
- public static void main(String[] args) throws IOException {
- long bytesWritten = 0;
- byte[] data = new byte[100 * 1024 * 1024];
- ObjectOutputStream out = new ObjectOutputStream(
- new BufferedOutputStream(
- new FileOutputStream("bigdata.bin")
- )
- );
- long time = System.currentTimeMillis();
- for (int i = 0; i < 10 * 1024 * 1024; i++) {
- out.writeObject(data);
- bytesWritten += data.length;
- }
- out.writeObject(null);
- out.close();
- time = System.currentTimeMillis() - time;
- System.out.printf("Wrote %d TB%n", bytesWritten / TERA_BYTE);
- System.out.println("time = " + time);
- }
- }
編譯之后,我們就可以執行這個程序了。
java -Xmx256m SuperFastWriter
可以看到類似以下的輸出
Wrote 1000 TB
time = 3710
你一定會非常奇怪,我用的到底是什么電腦。不僅輸出的速度那么快,并且輸出的內容完全超出了硬盤容量。每秒鐘250 TB,簡直是不可思議的事情。
如果到硬盤上看一下輸出的文件,會發現文件只有大概150M。這是因為當我們通過ObjectOutputStream輸出一個對象的時候,ObjectOutputStream會將該對象保存到一個哈希表中,以后在輸出相同的對象,都會只輸出指針,不輸出內容。同樣的事情也發生在讀取對象的時候。Java通過該機制達到最小化數據輸入和輸出的目的。下面的例子就演示了讀取的過程。
- import java.io.*;
- public class SuperFastReader {
- private static final long TERA_BYTE = 1024L * 1024 * 1024 * 1024;
- public static void main(String[] args) throws Exception {
- long bytesRead = 0;
- ObjectInputStream in = new ObjectInputStream(
- new BufferedInputStream(
- new FileInputStream("bigdata.bin")
- )
- );
- long time = System.currentTimeMillis();
- byte[] data;
- while ((data = (byte[]) in.readObject()) != null) {
- bytesRead += data.length;
- }
- in.close();
- time = System.currentTimeMillis() - time;
- System.out.printf("Read %d TB%n", bytesRead / TERA_BYTE);
- System.out.println("time = " + time);
- }
- }
在這個例子中,我們去讀取剛才輸出的文件。雖然文件只有150M左右,但是實際讀取的時候,數據量應該是和寫出的一樣。程序執行時間只需要幾秒時間。類似執行結果是:
Read 1000 TB
time = 2033
前面的例子我們反復的將同一個數組寫出到文件中,但是并沒有修改數組的內容。下面的例子我們將每次寫出內容不同的數組。因為Arrays.fill()的執行效率比較低。所以我們只寫出256個大數組。
- import java.io.*;
- import java.util.Arrays;
- public class ModifiedObjectWriter {
- public static void main(String[] args) throws IOException {
- byte[] data = new byte[10 * 1024 * 1024];
- ObjectOutputStream out = new ObjectOutputStream(
- new BufferedOutputStream(
- new FileOutputStream("smalldata.bin")
- )
- );
- for (int i = -128; i < 128; i++) {
- Arrays.fill(data, (byte) i);
- out.writeObject(data);
- }
- out.writeObject(null);
- out.close();
- }
- }
接下來,我們把寫出的內容在從文件中讀出看看。
- import java.io.*;
- public class ModifiedObjectReader {
- public static void main(String[] args) throws Exception {
- ObjectInputStream in = new ObjectInputStream(
- new BufferedInputStream(
- new FileInputStream("smalldata.bin")
- )
- );
- byte[] data;
- while ((data = (byte[]) in.readObject()) != null) {
- System.out.println(data[0]);
- }
- in.close();
- }
- }
觀察會發現,讀出的內容并沒有-128, -127, -126等數字,只有-128。這是因為雖然每次我們寫出之前都修改了數據的內容,但是依然是原來的數組。Java序列化機制除了***次寫出數組內容以外,以后每次只寫出一個指針。在讀的時候,也就只***次讀取到內容為-128的數組,以后每次都根據讀取到的指針反復在本地哈希表中讀取了。也就是說序列化機制只關心對象是否變化,而不關心內容是否變化。
通過這些提點,我們可以看出序列化的原則是:如果需要重復序列化一個對象,并且兩次序列化之間對象的內容會發生改變,那么就要復位輸出流?;蛘呙看屋敵銮岸贾匦聞摻ㄒ粋€對象。
下面我們看一下每次都創建新對象的結果:
- public class ModifiedObjectWriter2 {
- public static void main(String[] args) throws IOException {
- ObjectOutputStream out = new ObjectOutputStream(
- new BufferedOutputStream(
- new FileOutputStream("verylargedata.bin")
- )
- );
- for (int i = -128; i < 128; i++) {
- byte[] data = new byte[10 * 1024 * 1024];
- Arrays.fill(data, (byte) i);
- out.writeObject(data);
- }
- out.writeObject(null);
- out.close();
- }
- }
當程序運行一會之后,將會提示OutOfMemoryError。這是因為每次對象寫出的時候,都會在哈希表中保留一個指針,所以雖然對象已經不再使用了,Java的垃圾回收機制也不會對對象進行回收,要一直等到輸出流復位為止。當循環多次執行的時候,創建的對象越來越多,并且沒有被及時回收,就會出現OutOfMemoryError問題了。通過觀察可以發現,在出現錯誤之前所產生的文件基本接近于為JVM所分配的內存大小。如果每次輸出之后,都復位輸出,就可以避免這個問題了。
- import java.io.*;
- import java.util.Arrays;
- public class ModifiedObjectWriter3 {
- public static void main(String[] args) throws IOException {
- ObjectOutputStream out = new ObjectOutputStream(
- new BufferedOutputStream(
- new FileOutputStream("verylargedata.bin")
- )
- );
- byte[] data = new byte[10 * 1024 * 1024];
- for (int i = -128; i < 128; i++) {
- Arrays.fill(data, (byte) i);
- out.writeObject(data);
- out.reset();
- }
- out.writeObject(null);
- out.close();
- }
- }
不幸的是,復位輸出為導致所有的對象都被清理,即使是需要重復輸出的對象。
對ObjectOutputStream和ObjectInputStream進行優化設計很大程度上降低了重復數據的輸入輸出工作,比如字符串。不幸的是,如果不恰當的使用會經常導致OutOfMemoryError錯誤或者輸出數據不完整。
【編輯推薦】