有趣的Java对象序列化缓存问题

开发 后端
Java的序列化机制对相同的对象进行了优化。向流中输出不同对象的时候,如果没有进行复位,将导致内存泄露。对同一个对象,如果输出多次只会有一次被保留。如果复位,则失去了优化所带来的好处。

【51CTO特稿】在这里我们将通过几个有趣的例子,来演示Java对象序列化缓存问题。下面这个程序非常神奇,用了不到4秒的时间就向我的硬盘上输出了1000TB的数据。不要怀疑你看错了,确实是不到4秒时间就输出1000TB的数据,不相信你也可以在你的电脑上运行一下这个程序。如果你的硬盘不够大也不用担心,Java完全可以自己解决硬盘容量问题。这个例子对你的电脑***的要求就是必须有256M以上的内存,并且要设置执行参数为-Xmx256m。相信现在没有谁的电脑内存是不够256M的。
 

  1. import java.io.*;  
  2.  
  3. public class SuperFastWriter {  
  4.     private static final long TERA_BYTE = 1024L * 1024 * 1024 * 1024;  
  5.     public static void main(String[] args) throws IOException {  
  6.         long bytesWritten = 0;  
  7.         byte[] data = new byte[100 * 1024 * 1024];  
  8.         ObjectOutputStream out = new ObjectOutputStream(  
  9.             new BufferedOutputStream(  
  10.                 new FileOutputStream("bigdata.bin")  
  11.             )  
  12.         );  
  13.         long time = System.currentTimeMillis();  
  14.         for (int i = 0; i < 10 * 1024 * 1024; i++) {  
  15.             out.writeObject(data);  
  16.             bytesWritten += data.length;  
  17.         }  
  18.         out.writeObject(null);  
  19.         out.close();  
  20.         time = System.currentTimeMillis() - time;  
  21.         System.out.printf("Wrote %d TB%n", bytesWritten / TERA_BYTE);  
  22.         System.out.println("time = " + time);  
  23.     }  
  24. }  

编译之后,我们就可以执行这个程序了。

java -Xmx256m SuperFastWriter

可以看到类似以下的输出

Wrote 1000 TB

time = 3710

你一定会非常奇怪,我用的到底是什么电脑。不仅输出的速度那么快,并且输出的内容完全超出了硬盘容量。每秒钟250 TB,简直是不可思议的事情。

如果到硬盘上看一下输出的文件,会发现文件只有大概150M。这是因为当我们通过ObjectOutputStream输出一个对象的时候,ObjectOutputStream会将该对象保存到一个哈希表中,以后在输出相同的对象,都会只输出指针,不输出内容。同样的事情也发生在读取对象的时候。Java通过该机制达到最小化数据输入和输出的目的。下面的例子就演示了读取的过程。

  1. import java.io.*;  
  2.  
  3. public class SuperFastReader {  
  4.     private static final long TERA_BYTE = 1024L * 1024 * 1024 * 1024;  
  5.     public static void main(String[] args) throws Exception {  
  6.         long bytesRead = 0;  
  7.         ObjectInputStream in = new ObjectInputStream(  
  8.             new BufferedInputStream(  
  9.                 new FileInputStream("bigdata.bin")  
  10.             )  
  11.         );  
  12.         long time = System.currentTimeMillis();  
  13.         byte[] data;  
  14.         while ((data = (byte[]) in.readObject()) != null) {  
  15.             bytesRead += data.length;  
  16.         }  
  17.         in.close();  
  18.         time = System.currentTimeMillis() - time;  
  19.         System.out.printf("Read %d TB%n", bytesRead / TERA_BYTE);  
  20.         System.out.println("time = " + time);  
  21.     }  
  22. }  

在这个例子中,我们去读取刚才输出的文件。虽然文件只有150M左右,但是实际读取的时候,数据量应该是和写出的一样。程序执行时间只需要几秒时间。类似执行结果是:

Read 1000 TB

time = 2033

前面的例子我们反复的将同一个数组写出到文件中,但是并没有修改数组的内容。下面的例子我们将每次写出内容不同的数组。因为Arrays.fill()的执行效率比较低。所以我们只写出256个大数组。
 

  1. import java.io.*;  
  2. import java.util.Arrays;  
  3.  
  4. public class ModifiedObjectWriter {  
  5.     public static void main(String[] args) throws IOException {  
  6.         byte[] data = new byte[10 * 1024 * 1024];  
  7.         ObjectOutputStream out = new ObjectOutputStream(  
  8.             new BufferedOutputStream(  
  9.                 new FileOutputStream("smalldata.bin")  
  10.             )  
  11.         );  
  12.         for (int i = -128; i < 128; i++) {  
  13.             Arrays.fill(data, (byte) i);  
  14.             out.writeObject(data);  
  15.         }  
  16.         out.writeObject(null);  
  17.         out.close();  
  18.     }  
  19. }  

 接下来,我们把写出的内容在从文件中读出看看。 

  1. import java.io.*;  
  2.  
  3. public class ModifiedObjectReader {  
  4.     public static void main(String[] args) throws Exception {  
  5.         ObjectInputStream in = new ObjectInputStream(  
  6.             new BufferedInputStream(  
  7.                 new FileInputStream("smalldata.bin")  
  8.             )  
  9.         );  
  10.         byte[] data;  
  11.         while ((data = (byte[]) in.readObject()) != null) {  
  12.         System.out.println(data[0]);  
  13.         }  
  14.         in.close();  
  15.     }  
  16. }  

观察会发现,读出的内容并没有-128, -127, -126等数字,只有-128。这是因为虽然每次我们写出之前都修改了数据的内容,但是依然是原来的数组。Java序列化机制除了***次写出数组内容以外,以后每次只写出一个指针。在读的时候,也就只***次读取到内容为-128的数组,以后每次都根据读取到的指针反复在本地哈希表中读取了。也就是说序列化机制只关心对象是否变化,而不关心内容是否变化。

通过这些提点,我们可以看出序列化的原则是:如果需要重复序列化一个对象,并且两次序列化之间对象的内容会发生改变,那么就要复位输出流。或者每次输出前都重新创建一个对象。

下面我们看一下每次都创建新对象的结果:

  1. public class ModifiedObjectWriter2 {  
  2.     public static void main(String[] args) throws IOException {  
  3.         ObjectOutputStream out = new ObjectOutputStream(  
  4.             new BufferedOutputStream(  
  5.                 new FileOutputStream("verylargedata.bin")  
  6.             )  
  7.         );  
  8.         for (int i = -128; i < 128; i++) {  
  9.             byte[] data = new byte[10 * 1024 * 1024];  
  10.             Arrays.fill(data, (byte) i);  
  11.             out.writeObject(data);  
  12.         }  
  13.         out.writeObject(null);  
  14.         out.close();  
  15.     }  
  16. }  

当程序运行一会之后,将会提示OutOfMemoryError。这是因为每次对象写出的时候,都会在哈希表中保留一个指针,所以虽然对象已经不再使用了,Java的垃圾回收机制也不会对对象进行回收,要一直等到输出流复位为止。当循环多次执行的时候,创建的对象越来越多,并且没有被及时回收,就会出现OutOfMemoryError问题了。通过观察可以发现,在出现错误之前所产生的文件基本接近于为JVM所分配的内存大小。如果每次输出之后,都复位输出,就可以避免这个问题了。

 

  1. import java.io.*;  
  2. import java.util.Arrays;  
  3.  
  4. public class ModifiedObjectWriter3 {  
  5.     public static void main(String[] args) throws IOException {  
  6.         ObjectOutputStream out = new ObjectOutputStream(  
  7.             new BufferedOutputStream(  
  8.                 new FileOutputStream("verylargedata.bin")  
  9.             )  
  10.         );  
  11.         byte[] data = new byte[10 * 1024 * 1024];  
  12.         for (int i = -128; i < 128; i++) {  
  13.             Arrays.fill(data, (byte) i);  
  14.             out.writeObject(data);  
  15.             out.reset();  
  16.         }  
  17.         out.writeObject(null);  
  18.         out.close();  
  19.     }  

不幸的是,复位输出为导致所有的对象都被清理,即使是需要重复输出的对象。
对ObjectOutputStream和ObjectInputStream进行优化设计很大程度上降低了重复数据的输入输出工作,比如字符串。不幸的是,如果不恰当的使用会经常导致OutOfMemoryError错误或者输出数据不完整。

【编辑推荐】

  1. Java序列化的机制和原理
  2. Java Socket通信的序列化和反序列化代码介绍
  3. Java输入数据流详解
  4. Java语言深入 文件和流
  5. Java对象序列化
责任编辑:佚名 来源: 51CTO
相关推荐

2018-03-19 10:20:23

Java序列化反序列化

2009-06-14 22:01:27

Java对象序列化反序列化

2011-06-01 15:05:02

序列化反序列化

2009-03-10 13:38:01

Java序列化字节流

2012-04-13 10:45:59

XML

2009-09-09 15:47:27

XML序列化和反序列化

2009-09-09 14:45:41

XML序列化和反序列化

2012-02-14 10:29:02

Java

2019-11-20 10:07:23

web安全PHP序列化反序列化

2021-08-30 12:25:12

Python序列化函数

2022-08-06 08:41:18

序列化反序列化Hessian

2010-03-19 15:54:21

Java Socket

2013-03-11 13:55:03

JavaJSON

2016-12-05 18:32:08

序列化androidjava

2009-08-25 15:15:08

C#对象序列化应用

2024-03-05 12:49:30

序列化反序列化C#

2011-04-02 09:04:49

Java序列化

2013-02-28 10:00:43

JIDJava序列化

2013-02-27 09:58:32

JavaJID

2009-08-24 17:14:08

C#序列化
点赞
收藏

51CTO技术栈公众号