前段时间写了一个java socket相关的程序,大概意思就是client和server是采用socket长连接方式,之间通信都是通过通过ObjectOutputStream和OjbectInputStream来进行写和读操作。
其实以前就很多次的用到过ObjectOutputStream,不过没有详细的琢磨过,这次就想着琢磨一下,主要也是因为我发现程序中存在内存泄漏的问题,通过Jprobe跟踪,排除了别的泄漏因素,最后定位在是在socket这里发生了泄漏,具体情况下面进行分析。
先来说说ObjectOutputStream,通过ObjectOutputStream来进行socket写入,那么会在流中加入Object的信息,也就是说如果想要是跨平台的socket通信那么可能会带来一些问题,因为数据流中加入的是java特有的信息,class类型以及成员变量的类型信息,同样通过ObjectInputStream来读取也有相应的规则来进行解析。
下面我们来看一下写入的信息,首先有一个简单的类
我们通过ObjectOutputStream来把该类写入文件中,查看一下写入的内容
我们可以看到写入的内容中包含了写入类的类型以及成员变量信息,当然关于插入的内容,我们可以覆盖ObjectOutputStream类的writeStreamHeader()方法来实现插入我们自定义的内容,当然如果这样做的话,我们就必须对ObjectInputStream类进行重写
上面是一些题外话,下面回到正题,关于我标题中提到的有内存泄漏的问题。为了更清晰直观的说明该问题,我又写了一个很简单的测试,代码如下:
我在这里循环了20次,那么大家可以猜想一下文件中会是什么内容,可能会有人认为是我刚才在上面贴出的内容重复20遍,起初我也是这么认为的,但事实不是这么回事。
我现在更改一下程序,不再循环,只写入一次,那么写入的内容是
循环10次
循环20次
这样的测试我们还是不够清晰,下面我们让他每次循环写入的对象的属性都不相同,我们这样修改一下代码
那么这个时候文件里的内容是什么呢,我们再来看一下
这样就显而易见了,我们虽然写入了10次,但是不会每次写入都会插入写入对象和成员变量类型的信心,而是在第一次写入的时候插入一些头信息,以后再写就不会再插入了。这实际是java做的优化,通过该优化从而减少socket传输的开销。
那么会有人问了,你说的内存泄漏的问题呢,写到这里,我想应该有人已经看出问题来了,它之所以可以这么做优化,前提是持有MyObject的引用,也就是说,不会释放掉MyObject的引用。现在明白了吧,如果你是长连接的方式,ObjectOutputStream会一直持有你以前发送过的对象的引用,从而导致jvm在进行垃圾回收的时候不能回收之前发送的对象的实例,经过漫长时间的运行,最终导致内存溢出了。这一点从我通过Jprobe跟踪也得到了印证。
下面我们来谈谈如何避免该问题,说着这里我们就得提到ObjectOutputStream的reset方法了,JDK文档中是这么解释该方法的:
“重置将丢弃已写入流中的所有对象的状态。重新设置状态,使其与新的 ObjectOutputStream 相同。将流中的当前点标记为 reset,相应的 ObjectInputStream 也将在这一点重置。以前写入流中的对象不再被视为正位于流中。它们会再次被写入流。”
就是说调用reset那么就丢弃所持有对象的状态(也就是释放掉了对对象的应用),同时会在流中设置reset标识。
还是之前那个例子,我们来修改一下代码,在每次写入后都调用一下reset方法
我们再来看一下写入文件内容
这次跟之前不同的,每一次写入都加入了头信息且每一次末尾都加入了y,我想这个标识应该就是reset标识,至于具体是什么,我们没必要深究了。
通过上面一系列的测试,我们大概对Object流有了一定了解,那么具体到我们日常编码中到底该不该调用reset呢,这个我想不能一概而论了,我们通过测试也看到了,在不调用reset的方式下,java的优化对于减轻socket开销还是很可观的,当然代价是有的,那就是直到你调用reset或者是关闭输出流之前,对于发送过的对象的实例是不会释放的。
结论:当然只是我自己的片面之词。如果你的程序需要很长时间的运行,我建议你还是调用reset避免最后内存溢出程序崩溃,但是如果你又要长时间运行,且发送的消息量又很大,那么调用reset无疑会增加开销,那么这个时候最好的做法我觉得是你自己实现一套机制,定时的调用reset或者是定量,比如查看到内存已经涨到一个水平后调用一下,这样既可以避免内存无限的增长下去,又可以减少不少socket通信的开销
anson在这里感谢大家花了这么长时间阅读该文章,希望能给大家带了一些帮助,另外上面的分析都是我个人的理解,肯定存在一定的局限性,大家有什么更深刻的认识,还请大家指出来,我们一起交流,共同进步
其实以前就很多次的用到过ObjectOutputStream,不过没有详细的琢磨过,这次就想着琢磨一下,主要也是因为我发现程序中存在内存泄漏的问题,通过Jprobe跟踪,排除了别的泄漏因素,最后定位在是在socket这里发生了泄漏,具体情况下面进行分析。
先来说说ObjectOutputStream,通过ObjectOutputStream来进行socket写入,那么会在流中加入Object的信息,也就是说如果想要是跨平台的socket通信那么可能会带来一些问题,因为数据流中加入的是java特有的信息,class类型以及成员变量的类型信息,同样通过ObjectInputStream来读取也有相应的规则来进行解析。
下面我们来看一下写入的信息,首先有一个简单的类
class MyObject implements Serializable { private static final long serialVersionUID = -9163423175612080544L; String str1; String str2; }
我们通过ObjectOutputStream来把该类写入文件中,查看一下写入的内容
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~
我们可以看到写入的内容中包含了写入类的类型以及成员变量信息,当然关于插入的内容,我们可以覆盖ObjectOutputStream类的writeStreamHeader()方法来实现插入我们自定义的内容,当然如果这样做的话,我们就必须对ObjectInputStream类进行重写
上面是一些题外话,下面回到正题,关于我标题中提到的有内存泄漏的问题。为了更清晰直观的说明该问题,我又写了一个很简单的测试,代码如下:
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 < 20; i++) { oos.writeObject(myObj); oos.writeObject(myObj); } fos.close();
我在这里循环了20次,那么大家可以猜想一下文件中会是什么内容,可能会有人认为是我刚才在上面贴出的内容重复20遍,起初我也是这么认为的,但事实不是这么回事。
我现在更改一下程序,不再循环,只写入一次,那么写入的内容是
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~
循环10次
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~
循环20次
sr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test1t test2q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~ q ~
这样的测试我们还是不够清晰,下面我们让他每次循环写入的对象的属性都不相同,我们这样修改一下代码
FileOutputStream fos = new FileOutputStream("c:\\test.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); for (int i = 0; i < 10; i++) { MyObject myObj = new MyObject(); myObj.str1 = "test1" + i; myObj.str2 = "test2" + i; oos.writeObject(myObj); oos.writeObject(myObj); } fos.close();
那么这个时候文件里的内容是什么呢,我们再来看一下
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 ~ sq ~ t test15t test25q ~ sq ~ t test16t test26q ~ sq ~ t test17t test27q ~ sq ~ t test18t test28q ~ sq ~ t test19t test29q ~
这样就显而易见了,我们虽然写入了10次,但是不会每次写入都会插入写入对象和成员变量类型的信心,而是在第一次写入的时候插入一些头信息,以后再写就不会再插入了。这实际是java做的优化,通过该优化从而减少socket传输的开销。
那么会有人问了,你说的内存泄漏的问题呢,写到这里,我想应该有人已经看出问题来了,它之所以可以这么做优化,前提是持有MyObject的引用,也就是说,不会释放掉MyObject的引用。现在明白了吧,如果你是长连接的方式,ObjectOutputStream会一直持有你以前发送过的对象的引用,从而导致jvm在进行垃圾回收的时候不能回收之前发送的对象的实例,经过漫长时间的运行,最终导致内存溢出了。这一点从我通过Jprobe跟踪也得到了印证。
下面我们来谈谈如何避免该问题,说着这里我们就得提到ObjectOutputStream的reset方法了,JDK文档中是这么解释该方法的:
“重置将丢弃已写入流中的所有对象的状态。重新设置状态,使其与新的 ObjectOutputStream 相同。将流中的当前点标记为 reset,相应的 ObjectInputStream 也将在这一点重置。以前写入流中的对象不再被视为正位于流中。它们会再次被写入流。”
就是说调用reset那么就丢弃所持有对象的状态(也就是释放掉了对对象的应用),同时会在流中设置reset标识。
还是之前那个例子,我们来修改一下代码,在每次写入后都调用一下reset方法
FileOutputStream fos = new FileOutputStream("c:\\test.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); for (int i = 0; i < 10; i++) { MyObject myObj = new MyObject(); myObj.str1 = "test1" + i; myObj.str2 = "test2" + i; oos.writeObject(myObj); oos.writeObject(myObj); oos.reset(); } fos.close();
我们再来看一下写入文件内容
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 ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test15t test25q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test16t test26q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test17t test27q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test18t test28q ~ ysr com.travelsky.test.MyObject€喳+?^` L str1t Ljava/lang/String;L str2q ~ xpt test19t test29q ~ y
这次跟之前不同的,每一次写入都加入了头信息且每一次末尾都加入了y,我想这个标识应该就是reset标识,至于具体是什么,我们没必要深究了。
通过上面一系列的测试,我们大概对Object流有了一定了解,那么具体到我们日常编码中到底该不该调用reset呢,这个我想不能一概而论了,我们通过测试也看到了,在不调用reset的方式下,java的优化对于减轻socket开销还是很可观的,当然代价是有的,那就是直到你调用reset或者是关闭输出流之前,对于发送过的对象的实例是不会释放的。
结论:当然只是我自己的片面之词。如果你的程序需要很长时间的运行,我建议你还是调用reset避免最后内存溢出程序崩溃,但是如果你又要长时间运行,且发送的消息量又很大,那么调用reset无疑会增加开销,那么这个时候最好的做法我觉得是你自己实现一套机制,定时的调用reset或者是定量,比如查看到内存已经涨到一个水平后调用一下,这样既可以避免内存无限的增长下去,又可以减少不少socket通信的开销
anson在这里感谢大家花了这么长时间阅读该文章,希望能给大家带了一些帮助,另外上面的分析都是我个人的理解,肯定存在一定的局限性,大家有什么更深刻的认识,还请大家指出来,我们一起交流,共同进步