`
bj_dzj
  • 浏览: 16410 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

剖析使用 ObjectOutputStream 可能引起的内存泄漏

 
阅读更多

使用 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 oos = 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 oos = 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 oos = 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 oos = 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 理论与实践: 用弱引用堵住内存泄漏”(developerWorks,2006 年 1 月):虽然用 Java 语言编写的程序在理论上是不会出现“内存泄漏”的,但是有时对象在不再作为程序的逻辑状态的一部分之后仍然不被垃圾收集。本文探讨了无意识的对象保留的常见原因,并展示了如何用弱引用堵住泄漏。
分享到:
评论

相关推荐

    JAVA内存溢出详解.doc

    - **内存泄漏检测**:使用内存分析工具,如VisualVM、MAT(Memory Analyzer Tool)等,找出程序中的内存泄漏问题。 4. **垃圾收集机制** Java的垃圾收集器负责自动回收不再使用的对象所占用的内存。了解并优化...

    websphere性能分析

    - **ObjectOutputStream**:如果序列化处理不当,可能导致内存消耗过大。 5. **解决方案**: - **生成dump文件**:使用wsadmin脚本客户端的generateHeapDump操作。 - **并发读取配置文件**:确保CAS并发读取配置...

    java学习笔记(java 反射机制 流 内存管理)

    内存泄露是常见的问题,当不再使用的对象仍然被引用,无法被垃圾收集器回收。此外,栈内存用于存储方法局部变量,随着方法的调用和返回自动管理。 四、基础Java学习 学习Java的基础涉及语法、控制结构、异常处理、...

    SimpleJava.pdf

    - **文档**:提供详尽的使用指南和API文档,帮助用户快速集成和使用。 #### 3. Java类何时以及如何被加载和初始化? - **类加载机制**:Java虚拟机采用双亲委派模型来加载类,确保了核心类库的稳定性和安全性。 - ...

    JavaEE技术问题汇总.docx

    内存泄露是程序未释放不再使用的内存,导致可用内存逐渐减少。 以上仅是部分知识点,更多如静态代理、动态代理、CGLIB与JDK代理、静态关键字、CAS单点登录原理等未展开详述。这些知识涵盖JavaEE开发的多个方面,...

    大数据常见面试题(2019版)

    - Spark提供了一种更高效的数据处理方式,它使用内存计算,比Hadoop更快。 - Spark的主要组件包括Spark Core、Spark SQL、Spark Streaming和MLlib,了解它们的应用场景和API是基础。 7. **Java在大数据中的应用**...

    Java语言安全编程规范

    8. 对象的生命周期管理:避免创建过期对象并保持其引用,防止内存泄漏和内存安全问题。使用弱引用或软引用管理不再使用的对象。 9. 数据加密:使用Java的Cipher类进行数据加密和解密,确保敏感信息在存储和传输过程...

    X友NC6.5未授权文件上传漏洞分析1

    然后,使用ByteArrayOutputStream和ObjectOutputStream来生成POST内容,并将其写入到1.cer文件中。 三、漏洞利用 为了利用该漏洞,攻击者可以使用POSTMAN或其他软件将生成的1.cer文件内容作为POST请求的主体,提交...

    一些不常见的java面试题.docx

    - **防止内存泄漏:** 通过自动识别不再使用的对象并回收它们占用的内存,有效地避免了内存泄漏。 - **提高编程效率:** 开发者无需关注内存分配和释放,降低了出错概率,提高了开发效率。 - **优化内存使用:** ...

    整理齐全的java面试题大全

    张孝祥老师整理的Java就业面试题大全文档,很可能会涵盖以上所有知识点,并可能包含具体的题目实例和解答分析,是准备Java面试的重要参考资料。通过深入理解和实践这些知识点,将大大提高面试成功的几率。

    125条常见的java面试笔试题大汇总(5)

    例如,如果`i`和`i2`两个变量被分配了大量的内存空间,但在计算完成后并没有被适当释放(虽然这种情况更倾向于内存溢出而非泄漏),这可能会导致内存使用不当。正确的内存泄漏例子应该是对象不再使用但因为某些原因...

    J2ee面试题目

    不推荐使用`stop()`和`suspend()`方法,因为它们可能导致不安全的状态(例如资源解锁)和死锁。推荐使用`wait()`和`notify()`来控制线程协作。 3. **Java流类型**: Java的流分为字节流(Byte Stream)和字符流...

    java序列化

    但是,当涉及到循环引用时,需要特别小心,因为这可能导致无限循环或内存泄漏。 `struts2 in action.xmind`文件名可能指的是Struts2框架的学习或参考材料,Struts2是一个基于MVC设计模式的Web应用框架,它结合了...

    Java-IO流基础例题 & 例题源码 & PPT教学文档(黑马程序员详细版).rar

    例如,你可能会看到如何使用FileInputStream和FileOutputStream读写文件,或者使用DataInputStream和DataOutputStream处理结构化数据。 总结,Java IO流是Java编程中的基石,掌握了流的操作,意味着你能够有效地...

    java版qq聊天软件

    此外,理解如何释放资源(如关闭输入输出流)是防止内存泄漏的重要技巧。 通过这个项目,初学者不仅可以学习到Java语言本身,还能接触到网络编程、GUI设计等实际应用领域,从而提升编程能力和问题解决能力。不过,...

    Java面试题以及答案

    - **垃圾回收机制**:自动管理内存的一种机制,Java虚拟机会定期清理不再使用的对象占用的内存空间,以防止内存泄漏。 #### 5. 在JAVA中,如何跳出当前的多重嵌套循环? 可以使用标签化的break语句,即在最外层...

    Java序列化和反序列化

    4. **关闭输出流**:序列化完成后,必须关闭`ObjectOutputStream`和`FileOutputStream`,释放资源,避免内存泄漏。 #### 序列化示例详解 以下是一个具体的序列化与反序列化示例,展示如何将`Box`类的对象序列化至...

    Java面试题资料超全.rar

    - 调优:JVM参数设置,内存泄漏和性能优化。 10. **Spring框架** - Spring核心:依赖注入(DI),AOP(面向切面编程)。 - Spring Boot:快速开发,自动配置,起步依赖。 - Spring MVC:控制器,模型-视图-控制...

    1000 Java Tips

    7. **JVM内存管理**:理解Java虚拟机(JVM)的工作原理,特别是垃圾收集和内存区域,如堆内存、栈内存、方法区和本地方法栈,有助于优化程序性能和避免内存泄漏。 8. **反射机制**:Java的反射机制允许程序在运行时...

    java面试题

    - **垃圾回收机制**:自动管理内存的一种方式,主要负责回收不再使用的对象占用的内存空间,以避免内存泄漏。JVM通过标记-清除算法、复制算法等技术来实现。 #### 5. 在JAVA中,如何跳出当前的多重嵌套循环? 可以...

Global site tag (gtag.js) - Google Analytics