MENU

【代码札记】排查ObjectOutputStream引起的内存泄漏

September 28, 2020 • 瞎折腾

今天在利用Java处理语料的时候发现随着时间的推移,Java进程需要的内存越来越多,虽然过程中有往下掉的环节,但是总体而言还是增长的趋势。由此我推断是发生了内存泄漏。本文将记述排查及解决的过程。

运行的程序本身代码很简单:

import ch.qos.logback.classic.Level
import info.skyblond.fiona.helper.HanLPHelper
import org.slf4j.LoggerFactory
import java.io.File
import java.io.ObjectOutputStream
import java.nio.charset.StandardCharsets

fun main(args: Array<String>) {
    val corpus = File("").bufferedReader(StandardCharsets.UTF_8)

    val structuralizedSentence = File("")
    val objectOutputStream = ObjectOutputStream(structuralizedSentence.outputStream())

    corpus.lineSequence().shuffled().map {
        // 分词
    }.map { sentence ->
        // 其他处理
    }.forEachIndexed { index, it ->
        objectOutputStream.writeObject(it)
        if (index % 1000 == 0) {
            objectOutputStream.flush()
            System.gc()
        }
    }
    objectOutputStream.close()
}

可以看出来基本上就是对语料库进行处理,处理的过程中并没有将循环内的变量存储到循环外部,内部变量也没有和外部长生命周期的变量交互的机会。而循环内部的变量与外部唯一交互的机会就是最后forEachIndexed中利用ObjectOutputStream写入文件的过程。

为了科学严谨,我注释了与ObjectOutputStream相关的语句,只在循环内部执行同样的操作,唯独不写入文件。利用Jconsole查看内存发现并没有内存泄漏。因此根据控制变量的原理,可以确定就是ObjectOutputStream导致了这次内存泄漏。确定了问题的位置,之后就好办了。原本为了方便存储处理结果,Json显得十分冗余,而使用其他编码方式又相当麻烦,于是我直接用ObjectOutputStream存入文件,十分优雅。可结果没想到这东西不仅没优雅成,反倒还给我弄了个内存泄漏出来,但即便如此,Java仍然比Python优秀十万倍

经过网上的一番搜索(关键字ObjectOutputStream memory leak),总结起来是这样的:

ObjectOutputStream在工作的时候会对已经发送(调用writeObject(obj)obj)对象记录在一个表中(引用形式),为了节省带宽和内存,如果之后发送过同样的数据,就可以直接告诉接收端使用之前发送过的内容,从而避免发送重复的数据。在搜索的过程中还看到有人提到writeUnshared函数,通过说明来看应该是无论之前是否发送过同样的数据,经由这个函数发送的数据总是会被传输。看起来是解决了内存泄漏的问题,但是在发送的时候还是会记录被发送对象的引用,从而导致GC认为这不是带回收的对象,引起内存泄漏。

这个问题真正的解法在于经常调用reset方法,这个方法会将对应的ObjectOutputStream重置,效果就是和新new出来的对象一样。这样之前持有的发送过的对象的引用就会被清零,同时也会在流上输出一个reset标志,告诉(未来的)接收者对方也要清空这个引用表。这样之前没能被GC收集并清理的对象就可以成功的为后来者腾出内存了。而关于writeUnshared,我认为不必要,出于两方面考虑:一方面是默认的机制可以节省带宽,本文中我要写入文件,这样可以避免在文件中写入相同的数据,能够减小文件大小;另一方面考虑如果要常规调用reset(),那么调用这个还是writeObject()我觉得可能区别不大。因此我选择看上去更优雅的writeObject()。于是对forEachIndexed块修改如下:

.forEachIndexed { index, it ->
    objectOutputStream.writeObject(it)
    if (index % 10000 == 0) {
        objectOutputStream.flush()
        objectOutputStream.reset()
        System.gc()
    }
}

修改过代码之后重新跑,终于看到了正常的内存曲线,针不戳。

微信图片_20200928204413.png

-全文完-


知识共享许可协议
【代码札记】排查ObjectOutputStream引起的内存泄漏天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment

3 Comments
  1. 新闻头条 新闻头条

    文章不错支持一下吧

  2. 惠州注册公司 惠州注册公司

    又发现一个好站,收藏了~以后会经常光顾的 (。•ˇ‸ˇ•。)

  3. 茵荟养生资讯 茵荟养生资讯

    赞!前排混个脸熟,博客真好看@(太开心)