有趣的Java對(duì)象序列化緩存問(wèn)題
【51CTO特稿】在這里我們將通過(guò)幾個(gè)有趣的例子,來(lái)演示Java對(duì)象序列化緩存問(wèn)題。下面這個(gè)程序非常神奇,用了不到4秒的時(shí)間就向我的硬盤(pán)上輸出了1000TB的數(shù)據(jù)。不要懷疑你看錯(cuò)了,確實(shí)是不到4秒時(shí)間就輸出1000TB的數(shù)據(jù),不相信你也可以在你的電腦上運(yùn)行一下這個(gè)程序。如果你的硬盤(pán)不夠大也不用擔(dān)心,Java完全可以自己解決硬盤(pán)容量問(wèn)題。這個(gè)例子對(duì)你的電腦***的要求就是必須有256M以上的內(nèi)存,并且要設(shè)置執(zhí)行參數(shù)為-Xmx256m。相信現(xiàn)在沒(méi)有誰(shuí)的電腦內(nèi)存是不夠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);
- }
- }
編譯之后,我們就可以執(zhí)行這個(gè)程序了。
java -Xmx256m SuperFastWriter
可以看到類(lèi)似以下的輸出
Wrote 1000 TB
time = 3710
你一定會(huì)非常奇怪,我用的到底是什么電腦。不僅輸出的速度那么快,并且輸出的內(nèi)容完全超出了硬盤(pán)容量。每秒鐘250 TB,簡(jiǎn)直是不可思議的事情。
如果到硬盤(pán)上看一下輸出的文件,會(huì)發(fā)現(xiàn)文件只有大概150M。這是因?yàn)楫?dāng)我們通過(guò)ObjectOutputStream輸出一個(gè)對(duì)象的時(shí)候,ObjectOutputStream會(huì)將該對(duì)象保存到一個(gè)哈希表中,以后在輸出相同的對(duì)象,都會(huì)只輸出指針,不輸出內(nèi)容。同樣的事情也發(fā)生在讀取對(duì)象的時(shí)候。Java通過(guò)該機(jī)制達(dá)到最小化數(shù)據(jù)輸入和輸出的目的。下面的例子就演示了讀取的過(guò)程。
- 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);
- }
- }
在這個(gè)例子中,我們?nèi)プx取剛才輸出的文件。雖然文件只有150M左右,但是實(shí)際讀取的時(shí)候,數(shù)據(jù)量應(yīng)該是和寫(xiě)出的一樣。程序執(zhí)行時(shí)間只需要幾秒時(shí)間。類(lèi)似執(zhí)行結(jié)果是:
Read 1000 TB
time = 2033
前面的例子我們反復(fù)的將同一個(gè)數(shù)組寫(xiě)出到文件中,但是并沒(méi)有修改數(shù)組的內(nèi)容。下面的例子我們將每次寫(xiě)出內(nèi)容不同的數(shù)組。因?yàn)锳rrays.fill()的執(zhí)行效率比較低。所以我們只寫(xiě)出256個(gè)大數(shù)組。
- 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();
- }
- }
接下來(lái),我們把寫(xiě)出的內(nèi)容在從文件中讀出看看。
- 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();
- }
- }
觀察會(huì)發(fā)現(xiàn),讀出的內(nèi)容并沒(méi)有-128, -127, -126等數(shù)字,只有-128。這是因?yàn)殡m然每次我們寫(xiě)出之前都修改了數(shù)據(jù)的內(nèi)容,但是依然是原來(lái)的數(shù)組。Java序列化機(jī)制除了***次寫(xiě)出數(shù)組內(nèi)容以外,以后每次只寫(xiě)出一個(gè)指針。在讀的時(shí)候,也就只***次讀取到內(nèi)容為-128的數(shù)組,以后每次都根據(jù)讀取到的指針?lè)磸?fù)在本地哈希表中讀取了。也就是說(shuō)序列化機(jī)制只關(guān)心對(duì)象是否變化,而不關(guān)心內(nèi)容是否變化。
通過(guò)這些提點(diǎn),我們可以看出序列化的原則是:如果需要重復(fù)序列化一個(gè)對(duì)象,并且兩次序列化之間對(duì)象的內(nèi)容會(huì)發(fā)生改變,那么就要復(fù)位輸出流?;蛘呙看屋敵銮岸贾匦聞?chuàng)建一個(gè)對(duì)象。
下面我們看一下每次都創(chuàng)建新對(duì)象的結(jié)果:
- 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();
- }
- }
當(dāng)程序運(yùn)行一會(huì)之后,將會(huì)提示OutOfMemoryError。這是因?yàn)槊看螌?duì)象寫(xiě)出的時(shí)候,都會(huì)在哈希表中保留一個(gè)指針,所以雖然對(duì)象已經(jīng)不再使用了,Java的垃圾回收機(jī)制也不會(huì)對(duì)對(duì)象進(jìn)行回收,要一直等到輸出流復(fù)位為止。當(dāng)循環(huán)多次執(zhí)行的時(shí)候,創(chuàng)建的對(duì)象越來(lái)越多,并且沒(méi)有被及時(shí)回收,就會(huì)出現(xiàn)OutOfMemoryError問(wèn)題了。通過(guò)觀察可以發(fā)現(xiàn),在出現(xiàn)錯(cuò)誤之前所產(chǎn)生的文件基本接近于為JVM所分配的內(nèi)存大小。如果每次輸出之后,都復(fù)位輸出,就可以避免這個(gè)問(wèn)題了。
- 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();
- }
- }
不幸的是,復(fù)位輸出為導(dǎo)致所有的對(duì)象都被清理,即使是需要重復(fù)輸出的對(duì)象。
對(duì)ObjectOutputStream和ObjectInputStream進(jìn)行優(yōu)化設(shè)計(jì)很大程度上降低了重復(fù)數(shù)據(jù)的輸入輸出工作,比如字符串。不幸的是,如果不恰當(dāng)?shù)氖褂脮?huì)經(jīng)常導(dǎo)致OutOfMemoryError錯(cuò)誤或者輸出數(shù)據(jù)不完整。
【編輯推薦】