引用:http://www.shangxueba.com/jingyan/86432.html
使用 ObjectOutputStream 来进行对象序列化
相信大多数程序员在使用 Java 进行日常开发工作中,都曾经遇到需要把数据进行序列化的情况,比如写入文件或者写入 socket 流。Java 的类库也提供了丰富工具类供我们使用,这其中就包括 ObjectOutputStream。此类允许我们将 Java 对象的基本数据类型和图形写入 OutputStream,在需要将 Java 对象进行序列化操作时,使用该类可以极大为我们提供便利。但是使用不当也会引起一些麻烦。
需要说明的是,在使用 ObjectOutputStream 向 OutputStream 流中写入的时候会在流中添加写入 Object 的信息。这样的话在我们使用 ObjectOutputStream 编写跨语言的 Socket 通信时会遇到问题。因为数据流中加入的是 Java 特有的信息,如 Class 类型以及成员变量的类型信息,只有使用 ObjectInputStream 才能解析这些特定的信息。
好了,通过上面的说明,我们现在对 ObjectOutputStream 有了一个大体的了解,接下来我们使用具体的代码来看一下如果使用 ObjectOutputStream 来把对象序列化到对象中。
把 Java 对象序列化到文件中
首先我们有一个简单的 Java 对象类:
清单 1. 要序列化的 java 对象
class MyObject implements Serializable {
private static final long serialVersionUID = -9163423175612080544L;
String str1;
String str2;
}
接下来我们使用 ObjectOutputStream 把 MyObject 对象写入文件:
清单 2. 写入文件
FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream os = new ObjectOutputStream(fos);
MyObject myObj = new MyObject();
myObj.str1 = "test1";
myObj.str2 = "test2";
oos.writeObject(myObj);
fos.close();
我们来看一下写入文件的内容:
清单 3. 写入文件内容
#sr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~
#xpt #test1t #test2
我们可以看到写入的内容中包含了写入类的类型以及成员变量信息,当然关于插入的内容,我们可以覆盖 ObjectOutputStream 类的 writeStreamHeader() 方法来实现插入我们自定义的内容。当然如果这样做的话,就必须对 ObjectInputStream 类进行重写。
上面是一些题外话,下面回到正题,关于标题中提到的有内存泄漏的问题。为了更清晰直观的说明该问题,我又写了一个很简单的测试,代码如下:
清单 4. 多次写入
FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream os = new ObjectOutputStream(fos);
MyObject myObj = new MyObject();
myObj.str1 = "test1";
myObj.str2 = "test2";
for (int i = 0; i < 5; i++) {
oos.writeObject(myObj);
}
fos.close();
我们再来看一下写入的内容:
清单 5. 写入 5 次
#sr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~ #xpt
#test1t #test2q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #
我们可以看到多次写入同一个类型的对象,那么对象的类型信息是不会重复写入的。那么有人会说了,那是因为你写入的对象每次的内容都是一样的,接下来为了得到更清晰的测试结果,我们来让每次写入的内容不同。
清单 6. 多次写入不同内容
FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream os = new ObjectOutputStream(fos);
for (int i = 0; i < 5; i++) {
MyObject myObj = new MyObject();
myObj.str1 = "test1" + i;
myObj.str2 = "test2" + i;
oos.writeObject(myObj);
}
fos.close();
我们再来看一下写入的结果:
清单 7. 写入 5 次不同内容的对象
#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 ~ #
测试小结
通过上面的测试就很容易的发现,我们虽然写入了 5 次,但是不会每次写入都会插入写入对象和成员变量类型的信息,而是在第一次写入的时候插入一些头信息,以后再写就不会再插入了。这实际是 Java 做的优化,通过该优化从而减少 socket 传输的开销。
我想现在应该有人已经看出问题来了,它之所以可以这么做优化,前提是持有 MyObject 的引用,也就是说,不会释放掉 MyObject 的引用。如果你是长连接的方式(socket 中很常用),ObjectOutputStream 会一直持有你以前发送过的对象的引用,从而导致 JVM 在进行垃圾回收的时候不能回收之前发送的对象的实例,经过漫长时间的运行,最终导致内存溢出。这一点从我通过 Jprobe 跟踪也得到了印证。
避免长连接的情况下出现内存溢出
下面我们来谈谈如何避免该问题,说到这里我们就得提到 ObjectOutputStream 的 reset 方法了,JDK 文档中是这么解释该方法的:
“重置将丢弃已写入流中的所有对象的状态。重新设置状态,使其与新的 ObjectOutputStream 相同。将流中的当前点标记为 reset,相应的 ObjectInputStream 也将在这一点重置。以前写入流中的对象不再被视为正位于流中。它们会再次被写入流。”
就是说调用 reset 那么就丢弃所持有对象的状态(也就是释放掉了对对象的引用),同时会在流中设置 reset 标识。
我们来把之前的代码稍作修改,在进行一下测试来看看有什么不同:
清单 8. 重置的方式多次写入
FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream os = new ObjectOutputStream(fos);
for (int i = 0; i < 5; i++) {
MyObject myObj = new MyObject();
myObj.str1 = "test1" + i;
myObj.str2 = "test2" + i;
oos.writeObject(myObj);
oos.reset();
}
fos.close();
我们来看一下加入 reset 后写入文件的内容:
清单 9. 重置的方式写入的内容
#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 ~ #y
这次跟之前不同的,每一次写入都加入了头信息且每一次末尾都加入了 y,我想这个标识应该就是 reset 标识,至于具体是什么,我们没必要深究了。
结论及一些建议
通过上面一系列的测试,我们大概对 Object 流有了一定了解,那么具体到我们日常编码中到底该不该调用 reset 呢,这个我想不能一概而论了。我们通过测试也看到了,在不调用 reset 的方式下,Java 的优化对于减轻 socket 开销还是很可观的,当然代价是有的,那就是直到你调用 reset 或者是关闭输出流之前,对于发送过的对象的实例是不会释放的。
如果你的程序需要很长时间的运行,建议你还是调用 reset 避免最后内存溢出程序崩溃,但是如果你又要长时间运行,且发送的消息量又很大,那么调用 reset 无疑会增加开销,那么这个时候最好的做法我觉得是自己实现一套机制,定时的调用 reset 或者是定量,比如查看到内存已经涨到一个水平后调用一下,这样既可以避免内存无限的增长下去,又可以减少不少 socket 通信的开销。
分享到:
相关推荐
Java是一种广泛使用的面向对象的编程语言,其基础知识涵盖了多个方面,包括语法、面向对象特性、异常处理、多线程...以上只是Java基础知识的一个概述,每个话题都值得深入探讨和实践,不断学习和总结是提升技能的关键。
Java 基础知识大全 本资源摘要信息是 Java 基础知识大全的总结,涵盖了 Java 语言的基本概念、特点、历史发展等方面的知识点。以下是本资源摘要信息的详细内容: 一、 Java 语言的特点 * 面向对象:Java 语言是...
java基础知识,帮助初学者更快更好地掌握java。ppt内容具体易懂,希望对刚接触java的初学者有所帮助。
### Java基础知识精炼 #### 一、Java概述与发展历程 Java是一种高级编程语言,由Sun Microsystems公司的James Gosling等人于1991年开始研发,原名为Oak,旨在控制嵌入式设备如有线电视交换盒和PDA。1994年正式更名...
Java是一种广泛使用的面向对象的编程语言,由Sun Microsystems(现为Oracle公司的一部分)的James Gosling等人于1991年...无论是初学者还是经验丰富的开发者,对Java基础知识的深入理解和实践都是提升编程技能的关键。
java基础知识的培训ppt,对于java初学者来说可以有一些作用。
java基础知识
JAVA基础知识总结 JAVA基础知识总结 JAVA基础知识总结
Java基础知识总结涵盖了Java程序设计语言的核心概念和常用知识点。在详细学习和总结这些知识点之前,首先需要对Java有一个整体的认识。Java是一种面向对象的编程语言,它具有跨平台的特性,即“一次编写,到处运行”...
Java 基础知识总结是 Java 程序员不可或缺的一部分,本总结将涵盖 Java 的基础知识,包括 Java 概述、Java 语法基础、变量、数据类型、运算符、控制语句、方法、数组、继承、多态、接口、异常、IO 流等。 一、Java ...
【Java基础知识概述】 Java是一种广泛使用的高级编程语言,由Sun Microsystems公司(现已被Oracle公司收购)于1991年发起的“绿色项目”孕育而生,最初名为Oak。尽管最初的项目目标并未实现,但Java作为一种面向...
### Java基础知识总结(绝对经典) #### 一、Java概述 - **起源与发展**:Java语言始于1991年Sun公司的James Gosling等人所开发的Oak语言,原计划用于控制嵌入式设备如有线电视交换盒和个人数字助理(PDA)。1994年...
java基础知识点总结及面试问题java基础知识点总结及面试问题java基础知识点总结及面试java基础知识点总结及面试问题
Java基础知识学习:包括JVM虚拟机、对象模型等Java基础知识代码案例Java基础知识学习:包括JVM虚拟机、对象模型等Java基础知识代码案例Java基础知识学习:包括JVM虚拟机、对象模型等Java基础知识代码案例Java基础...
java基础知识点,面试宝典,最适合你的java面试知识点,所学java知识点总结
Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案Java基础知识点和答案...
Java基础知识.pdf