Memory leak from Java?

Help
2013-06-13
2014-02-12
  • Tod Shannon

    Tod Shannon - 2013-06-13

    We're trying to use NITRO to write NITfs from Java. While we can do this, we are seeing huge memory leaks (we are writing thousands of images in our runs), regardless of what the Java heap size is set to, indicating to me a problem in the JNI side of things. By querying the NITFResourceManager it looks like tons of references get left around forever.

    Can anyone help what we are doing wrong?

    I'm seeing this behavior with the TestWriter2 example from the source tree - this is on r1157. I've slightly modified the source so it can run multiple times and also run multithreaded (although I have the number of threads set to 1 currently to keep it simple).

    Here's the modified TestWriter2 which leaks like crazy (NITRO r1157, Windows 7, java 1.6.35 x64)

    Thanks,
    -Tod

    package edu.mit.ll.util;

    / =========================================================================
    * This file is part of NITRO
    * =========================================================================

    * (C) Copyright 2004 - 2010, General Dynamics - Advanced Information Systems

    * NITRO is free software; you can redistribute it and/or modify
    * it under the terms of the GNU Lesser General Public License as published by
    * the Free Software Foundation; either version 3 of the License, or
    * (at your option) any later version.

    * This program is distributed in the hope that it will be useful,
    * but WITHOUT ANY WARRANTY; without even the implied warranty of
    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    * GNU Lesser General Public License for more details.

    * You should have received a copy of the GNU Lesser General Public
    * License along with this program; if not, If not,
    * see http://www.gnu.org/licenses/.

    */

    import java.awt.image.BufferedImage;
    import java.awt.image.DataBuffer;
    import java.awt.image.DataBufferByte;
    import java.awt.image.SampleModel;
    import java.io.File;
    import java.io.IOException;
    import java.text.SimpleDateFormat;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.List;
    import java.util.concurrent.ScheduledThreadPoolExecutor;

    import javax.imageio.ImageIO;

    import nitf.*;

    /*
    * This test writes a test image out to a file
    /
    public class TestWriter2 implements Runnable
    {

    public static final String FILENAME = "test_file.src";
    
    public static final String OUTPUT_NAME = "test_file.ntf";
    
    private  String _outputName = OUTPUT_NAME;
    
    private  int _width;
    
    private  int _height;
    
    private  int _bands;
    private String inputFile;
    
    public static void main(String[] args) throws nitf.NITFException,
                                                  IOException
    {
    
        final List list = Arrays.asList(args);
        String inputFile = null;
        for (int i = 0; i < list.size(); i++)
        {
            String arg = ((String) list.get(i)).trim();
            if (arg.equals("-o") && i < list.size() - 1)
            {
    

    // _outputName = (String) list.get(++i);
    }
    else if (arg.equals("--help") || arg.equals("--?"))
    {
    System.out.println("usage: java " + TestWriter2.class.getName()
    + " [-b bands] [-o name] [--help | --?] image");
    System.out.println(" Where:");
    System.out
    .println("-------------------------------------------------------");
    System.out
    .println("name ............ sets the output file name");
    System.out.println("--help and --? print this help message");
    System.exit(0);
    }
    else
    inputFile = arg;
    }

        ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(1);
    
        for (int i=0; i < 3000; i++) {
            TestWriter2 tw = new TestWriter2();
            tw.inputFile = inputFile;
            tw._outputName = OUTPUT_NAME + i;
            threadPool.execute(tw);
        }
    
        long completedTasks = 0;
        while (true)
        {
            try {
                Thread.sleep(1000);
            } catch (Exception e)
            {
                e.printStackTrace();
            }
    
            long newCompletedTasks = threadPool.getCompletedTaskCount();
            if (newCompletedTasks > completedTasks)
            {
                completedTasks = newCompletedTasks;
                System.out.println("executed tasks = " + completedTasks);
            }
        }
    }
    
    public TestWriter2() {}
    public void run()
    {
        try {
            /* write the test image out */
            // writeTestImage();
            /* create bandsources from the image */
            // final ImageSource imageSource = makeImageSource();
            final ImageSource imageSource = makeImageSource(inputFile);
    
            writeNITFImage(imageSource);
        } catch (Exception e)
        {
            e.printStackTrace();
        } finally {
    
            // delete the test_file.src file
            new File(FILENAME).delete();
    
            NITFResourceManager nitfr = NITFResourceManager.getInstance();
            nitfr.getClass(); // break here in debugger and look at tracked object list size
        }
    }
    
    private  void writeNITFImage(ImageSource imageSource)
            throws NITFException
    {
        Record record = new Record(Version.NITF_21);
    
        // fill the header
        fillHeader(record);
    
        // fill the image
        fillImageSubheader(record);
    
        IOHandle outHandle = new IOHandle(_outputName,
                                          IOHandle.NITF_ACCESS_WRITEONLY, IOHandle.NITF_CREATE);
        Writer writer = new Writer();
    
        writer.prepare(record, outHandle);
        final ImageWriter imageWriter = writer.getNewImageWriter(0);
        imageWriter.attachSource(imageSource);
    
        final boolean status = writer.write();
        System.out.println("Write Image Status = " + status);
        outHandle.close();
    }
    
    private  void fillImageSubheader(Record record) throws NITFException
    {
        final ImageSegment imageSegment = record.newImageSegment();
        final ImageSubheader subheader = imageSegment.getSubheader();
    
        subheader.getImageCoordinateSystem().setData(" ");
    
        subheader.getFilePartType().setData("IM");
        // imageID
        subheader.getImageId().setData(
                String.valueOf(System.currentTimeMillis()));
    
        // title
        subheader.getImageTitle().setData("Test Image");
    
        // target ID
        // subheader.getTargetId().setData("");
    
        // source
        subheader.getImageSource().setData("Artificial");
    
        // pixelType
        subheader.getPixelValueType().setData("INT");
    
        // pixelJust
        subheader.getPixelJustification().setData("R");
    
        // compression
        subheader.getImageCompression().setData("NC");
    
        // displayLevel
        subheader.getImageDisplayLevel().setData("1");
    
        // attachmentLevel
        subheader.getImageAttachmentLevel().setData("0");
    
        // imageCategory
        subheader.getImageCategory().setData("VIS");
    
        // imageRep
        switch (_bands)
        {
            case 1:
                subheader.getImageRepresentation().setData("MONO");
                break;
            case 3:
                subheader.getImageRepresentation().setData("RGB");
                break;
            default:
                subheader.getImageRepresentation().setData("MULTI");
                break;
        }
    
        // imageMode
        subheader.getImageMode().setData("B");
    
        // magnification
        subheader.getImageMagnification().setData("1.0");
    
        // imageLocation
        subheader.getImageLocation().setData("0000000000");
    
        // syncCode
        subheader.getImageSyncCode().setData("0");
    
        // integer fields
        subheader.getNumCols().setData(String.valueOf(_width));
        subheader.getNumRows().setData(String.valueOf(_height));
        subheader.getNumBitsPerPixel().setData(String.valueOf(8));
        subheader.getActualBitsPerPixel().setData(String.valueOf(8));
        subheader.getNumBlocksPerCol().setData(String.valueOf(1));
        subheader.getNumBlocksPerRow().setData(String.valueOf(1));
        subheader.getNumPixelsPerHorizBlock().setData(String.valueOf(_width));
        subheader.getNumPixelsPerVertBlock().setData(String.valueOf(_height));
    
        subheader.insertImageComment("This is a test image", 0);
        subheader.insertImageComment(
                "It is comprised of artificial pixel data", 1);
    
        subheader.removeImageComment(0);
    
        final String irep = "RGB";
    
        /* create new bands */
        subheader.createBands(_bands);
        final BandInfo[] bandInfos = subheader.getBandInfo();
        for (int i = 0; i < bandInfos.length; i++)
        {
            BandInfo bandInfo = bandInfos[i];
            bandInfo.getNumLUTs().setData("0");
            if (_bands == 3)
            {
                bandInfo.getRepresentation().setData("" + irep.charAt(i));
            }
            // bandInfo.getSubcategory().setData("0");
            bandInfo.getImageFilterCondition().setData("N");
        }
    
        // hard-coded values for now
        subheader.getImageSecurityClass().setData("U");
        subheader.getEncrypted().setData("0");
    }
    
    private  void fillHeader(Record record) throws NITFException
    {
        final FileHeader header = record.getHeader();
        header.getFileHeader().setData("NITF");
        header.getFileVersion().setData("02.10");
    
        header.getSystemType().setData("    ");
        header.getBackgroundColor().setData("   ");
    
        // set the date
        header.getFileDateTime().setData(
                new SimpleDateFormat("ddHHmmss'Z'MMMyy").format(new Date())
                        .toUpperCase());
    
        header.getOriginStationID().setData("Test Image - Ypsilanti");
    
        header.getOriginatorName().setData("GD-AIS");
    
        header.getOriginatorPhone().setData("734-480-xxxx");
    
        header.getComplianceLevel().setData("03");
    
        // these are all defaults hard-coded to the values given
        header.getClassification().setData("U");
        header.getEncrypted().setData("0");
        header.getMessageCopyNum().setData("0");
        header.getMessageNumCopies().setData("0");
    }
    
    private  ImageSource makeImageSource(String input)
            throws NITFException, IOException
    {
        if (input == null)
            throw new IOException("no input image provided");
    
        File file = new File(input);
        if (!file.exists())
            throw new IOException("input image does not exist");
    
        BufferedImage bufImage = ImageIO.read(file);
        _bands = bufImage.getData().getNumBands();
        _height = bufImage.getHeight();
        _width = bufImage.getWidth();
        int type = bufImage.getType();
    
        ImageSource imageSource = new ImageSource();
    
        DataBuffer dataBuffer = bufImage.getData().getDataBuffer();
    
        System.out.println(type);
        if (type == BufferedImage.TYPE_BYTE_GRAY)
        {
            DataBufferByte dbb = (DataBufferByte) dataBuffer;
            MemorySource source = new MemorySource(dbb.getData(0), _height
                                                                   * _width, 0, 1, 0);
            imageSource.addBand(source);
        }
        else if (type == BufferedImage.TYPE_3BYTE_BGR)
        {
            SampleModel sampleModel = bufImage.getSampleModel();
            for (int i = 0; i < _bands; ++i)
            {
                int[] data = sampleModel.getSamples(0, 0, _width, _height, i,
                                                    (int[]) null, dataBuffer);
                byte[] buf = new byte[data.length];
                for (int j = 0; j < data.length; ++j)
                {
                    buf[j] = (byte) data[j];
                }
                MemorySource source = new MemorySource(buf, _height * _width,
                                                       0, 1, 0);
                imageSource.addBand(source);
            }
        }
        else
            throw new NITFException("Unsupported image type");
    
        return imageSource;
    }
    

    }

     
    • Tod Shannon

      Tod Shannon - 2013-06-13

      It looks as if there are many unpaired _ManageObject(.., JNI_FALSE) calls which increment the native refs to an object. Since there are no corresponding _ManageObject(..., JNI_TRUE) calls, the number of native references stays positive, the NITFResourceManager keeps a reference to the objects through the TrackedObject map, and the objects are never gc'd or finalized and hence their underlying native memory is never released.

       
  • Adam Sylvester

    Adam Sylvester - 2013-06-14

    Tod,

    We've been gradually working on getting some leaks cleaned up in the C and C++ layers but the Java layer hasn't been getting as much love. What you're saying above makes sense... obviously we want to get this fixed up so if you get to the point where you can provide a patch we'd be happy to integrate it in. Otherwise we'll try to get to some of these over the coming weeks.

    -Adam

     
  • Tod Shannon

    Tod Shannon - 2013-06-14

    Hi Adam, I'm not sure I have/will have enough grasp of the architecture to make a clean patch. What approach do you think you guys would take on this? Relying on Java finalization seems unpredictable - I'd probably lean towards explicit release call(s) even though it puts the onus on the client application code.

     
  • Tod Shannon

    Tod Shannon - 2013-06-14

    I'm not going to be able to spend enough time on this for a pretty fix. Assuming I am understanding the code, it looks like there is confused ownership of objects between the underlying C layer and the Java/JNI layer. Once DestructibleObjects (ie ImageWriter) are created with a reference to some underlying C-side memory, they seem to expect to manage it, including destruction/freeing at some point. Once you do a nitf_write, however, the C code goes through and frees the all the WriteHandler objects directly, leaving dangling references from the ImageWriter DestructibleObject base class and in the NITFResourceManager. Seems like anything that is getting freed by the underlying C layer without regard for the Java/JNI side needs to not be a DestructibleObject.

    This seems to be true for multiple classes. I don't see a quick fix for this given my limited experience with the code base, so I doubt I'll be able to come up with a patch in the time I have available.

     
  • Dave Sousa

    Dave Sousa - 2014-02-11

    I am trying to convert thousands of JPEG images to NITF using NITRO 2.7. Java heap size is increasing and decreasing ok. However, the private working set memory displayed using Task Manager increases until the OS runs out of memory. Was this memory leak ever fixed, or does anybody have a workaround? Thanks.

     
  • Adam Sylvester

    Adam Sylvester - 2014-02-12

    Dave,

    What Tod states above is likely the culprit; this is on our radar but unfortunately there hasn't been time to fix this yet.

    -Adam

     

Log in to post a comment.