Java学习--论对象置空的重要性

探讨原因

我工作两年多一点点,Java基础一般,也没有太深入学习过JVM的原理。幸运的是在工作的第二年找到了一份目前比较流行的大数据工作,也算是借着工作的机会学习,算是一个不错的选择。在工作中我主要使用Hadoop Yarn进行图像处理,因此对于服务器资源的消耗自然要高于对日志的分析(笔者对Hadoop的入门就是从日志分析开始学习的)。但由于工作经验不足,过分依赖JVM对内存的回收,导致这段时间反复的遇到java.lang.OutOfMemoryError: Java heap space的错误。起初我只想到调整map和reduce的JVM参数和物理内存,但毕竟服务器的资源是有限的,不可能一直使用这种蠢方法来解决内存溢出的问题。于是我开始上网查找有关Java内存方面的资料,临时抱佛脚。我找到了几个资料,地址如下:
http://www.cnblogs.com/xwdreamer/archive/2012/04/01/2428857.html
http://www.cnblogs.com/gw811/archive/2012/10/18/2730117.html
http://www.cnblogs.com/gw811/archive/2012/10/19/2730258.html
其中后两个是《深入理解java虚拟机》这本书中的内容。不管怎样,我对JVM有了一个初步的印象。下面我简单介绍一下出问题的代码。

代码示例

代码主要目标是对MongoDB中处理好的图片导出成为zip包存到HDFS中,这里只使用了一个map生成zip包。
注:这个功能可能使用Hadoop来处理非常多余,但由于我们业务需要处理一组图片从存储-处理-导出整理流程,因此使用Hadoop进行zip导出是有必要的,这里不过多探讨业务,只是举个例子。
main函数主要进行简单的配置和作业提交到Hadoop集群中;map用来获取MongoDB中的图片输出到zip包中。map代码如下(已简化):

public class MongoZipMapper extends Mapper {

    private FileSystem fileSystem;
    private ArchiveOutputStream out;

    public void setup(Context context) {
        try {
            Configuration conf = context.getConfiguration();
            fileSystem = new Path(conf.get(FileOutputFormat.OUTDIR)).getFileSystem(conf);
            String archiveName = conf.get(Constants.ARCHIVENAME);
            Path outZip = new Path(archiveName + ".zip");
            //指定压缩文件路径
            FSDataOutputStream outputStream = fileSystem.create(outZip);
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
            out = new ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.ZIP, bufferedOutputStream);
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    public void map(Object key, BSONObject value, Context context) throws IOException, InterruptedException {
        String fileName = getFileName(value); // 获取zip中条目的文件名
        byte[] content = (byte[])value.get("binary"); // 读取MongoDB中的文件内容字段
        ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(fileName);
        zipArchiveEntry.setSize(content.length);
        out.putArchiveEntry(zipArchiveEntry);  
        out.write(content);   
        out.flush();
        out.closeArchiveEntry();
        context.write(new Text(fileName), new Text("OK"));
    }

    private String getFileName(BSONObject value) {
        // ...
    }

    public void cleanup(Context context) {
        try {
            if(out!=null) {
                out.finish();  
                out.close();
            }
            if(fileSystem!=null) {
                fileSystem.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

乍一看上面的代码,只是导出数据,貌似没有问题。只有一个map进行zip包的输出,在准备阶段(setup)初始化所需对象,在清理阶段(cleanup)对各种资源进行清理。当我导出少于16M(存入MongoDB中的数据限制大小)的文件时,这段程序并没有出现过问题。但由于我们处理的是图片,有些图片要求分辨率很高,因此很多图片都是大于16M的,通常在40M左右。我测试了导出500张40M左右的数据,这个程序就出现了OOM的问题。后来在仔细看代码,在map方法中增加了几个语句,如下:

public void map(Object key, BSONObject value, Context context) throws IOException, InterruptedException {
    String fileName = getFileName(value); // 获取zip中条目的文件名
    byte[] content = (byte[])value.get("binary"); // 读取MongoDB中的文件内容字段
    ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(fileName);
    zipArchiveEntry.setSize(content.length);
    out.putArchiveEntry(zipArchiveEntry);  
    out.write(content);   
    out.flush();
    out.closeArchiveEntry();
    /*-----start-----*/
    value.put("binary", new byte[0]); // 清空value中的"binary"
    value = null; // 清空value
    content = null; // 清空content      
    zipArchiveEntry = null; // 清空zipArchiveEntry
    /*------end------*/
    context.write(new Text(fileName), new Text("OK"));
}

第二段代码比第一段代码仅仅多了注释的四句话,全部都是置空的语句。再进行上次的500张图片导出zip包的测试,没有再发生OOM的报错。同时,我将分配的JVM参数和物理内存分别调整比原来小一半,仍然没有发生报错。

简单分析

在上文的代码中:

value.put("binary", new byte[0]);
value = null;

第一句是将value中的binary字段赋值为空byte数组,第二句将value置空。如果只对value值置空,该代码仍然会报OOM的错误,必须对value中的binary也置空才会起作用。这里置空的意思实际是告诉JVM这两个“强引用”变成“弱引用”,可以进行内存的收回。由于我没有对JVM中的一些术语和资料进行学习,这里可能用词不当。不过通过上述代码的实践得出结论:对“大对象”的及时置空是很有必要的,特别是处理图片、视频等单个数据较大时更是必要的。何况是在Hadoop中处理单个数据较大的情况,如果内存释放的及时,可以节省很多资源,明显提高程序的运行效率。好工具可以让我们写的代码运行更加高效稳定,而好的代码是从根本上增加处理效率、杜绝异常的原因。

总结

引用文中第一篇资料的话:数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。
因此在开发Java程序时,如果能够像C、C++程序员那样有意识的释放内存,不依赖JVM,可以让写出的代码更高效稳定,也减少未来出现OOM问题的可能。
文中可能有不当之处,望指正。

你可能感兴趣的:(java)