简体   繁体   中英

Graphics.drawImage() consumes a lot of memory drawing an int[] image

I have an ImageViewComponent that extends JComponent. This component is added to a JPanel that is added to a JFrame. From another class I regularly update the int[] image field of ImageViewComponent from another class. The problem is that this process eats up a lot of memory. It even consumes so much memory (+/- 130 MB after a few seconds according to JProfiler, and eventually it surpasses 1GB) that the entire program undergoes a 'lag spike' during garbage collection (the lag in the program happens at the same time that the memory is cleared).

This is the code of ImageViewComponent:

public class ImageViewComponent extends JComponent {

    private int image_width, image_height, updateInterval, updateCounter;
    private int[] imageArray;
    private BufferedImage currentDisplayedImage;
    private Image scaledDisplayedImage;

    /**
     * @param width          The width of this component
     * @param height         The height of this component
     * @param ui             The higher, the less frequent the image will be updated
     */
    public ImageViewComponent(int width, int height, int ui) {
        setPreferredSize(new Dimension(width, height));
        this.updateInterval = ui;
        this.updateCounter = 0;
        this.currentDisplayedImage = null;
        this.scaledDisplayedImage = null;
    }

    public void setImage(int[] image, int width, int height) {
        this.imageArray = image;
        this.image_width = width;
        this.image_height = height;
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);

        if (image_width == 0 || image_height == 0)
            return;
        else if (updateCounter != updateInterval && currentDisplayedImage != null) {
            g.drawImage(scaledDisplayedImage, 0, 0, this);
            updateCounter++;
            return;
        }

        this.currentDisplayedImage = new BufferedImage(image_width, image_height, BufferedImage.TYPE_INT_RGB);
        this.currentDisplayedImage.setRGB(0, 0, image_width, image_height, this.imageArray, 0, image_width);

        this.scaledDisplayedImage = this.currentDisplayedImage.getScaledInstance(this.getPreferredSize().width,
                this.getPreferredSize().height, BufferedImage.SCALE_DEFAULT);

        g.drawImage(scaledDisplayedImage, 0, 0, this);

        // reset update counter
        updateCounter = 0;
    }

}

JProfiler states that 70% of the program its active memory is allocated in this class, 50% is in Graphics.drawImage while 20% is in BufferedImage initialization.

I have tried fixing it by putting the line this.currentDisplayedImage = new BufferedImage(image_width, image_height, BufferedImage.TYPE_INT_RGB) in `setImage' and have it only set it once with a boolean flag but this makes the drawn image turn completely black for short amounts of time once in a while, nor does it fix the memory problem. I also tried this suggestion, which didn't work either.

How can I fix this memory issue?

There are several issues with the code. Some refer to performance, others to style or best practices, and others (at least potentially) refer to memory consumption.

  • Performance: The getScaledInstance method is distressingly slow. See https://stackoverflow.com/a/32278737/3182664 and others for better alternatives
  • Style: It's imageWidth , not image_width
  • Best practices: For a JComponent , you usually, you override paintComponent and not paint
  • Memory consumption: That's the main point...:

As MadProgrammer already pointed out: Do things as rarely as possible. The role and purpose of this updateCounter is not entirely clear. I think that the responsibility for updating the image less frequently should be in the class that uses your component - particularly, in the class that calls updateImage (which should simply be done less often). Maintaining this in the paint method is not very reliable.

In your current code, it seems like the currentDisplayedImage is (despite its name) neither displayed nor used in any other way. It may, however, be a good idea to keep it: It will be needed to be filled with the int[] data, and as a source for the scaled image that might have to be created.

One possible implementation of your class might look as follows:

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;

import javax.swing.JComponent;

public class ImageViewComponent extends JComponent {

    private int updateInterval, updateCounter;
    private BufferedImage fullImage;
    private BufferedImage displayedImage;

    /**
     * @param width          The width of this component
     * @param height         The height of this component
     * @param ui             The higher, the less frequent the image will be updated
     */
    public ImageViewComponent(int width, int height, int ui) {
        setPreferredSize(new Dimension(width, height));
        this.updateInterval = ui;
        this.updateCounter = 0;
        this.fullImage = null;
        this.displayedImage = 
            new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    }

    public void setImage(int[] image, int width, int height) {

        // Note: The updateInvervall/updateCounter stuff COULD
        // probably also be done here...
        if (fullImage == null ||
            fullImage.getWidth() != width ||
            fullImage.getHeight() != height)
        {
            fullImage = new BufferedImage(
                width, height, BufferedImage.TYPE_INT_RGB);
        }
        fullImage.setRGB(0, 0, width, height, image, 0, width);
        scaleImage(fullImage, displayedImage);
        repaint();
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(displayedImage, 0, 0, this);
    }


    private static BufferedImage scaleImage(
        BufferedImage input, BufferedImage output)
    {
        double scaleX = (double) output.getWidth() / input.getWidth();
        double scaleY = (double) output.getHeight() / input.getHeight();
        AffineTransform affineTransform = 
            AffineTransform.getScaleInstance(scaleX, scaleY);
        AffineTransformOp affineTransformOp = 
            new AffineTransformOp(affineTransform, null);
        return affineTransformOp.filter(input, output);
    }    

}

but note that this does not do this "updateInterval" handling, for the reason mentioned above.

And a side note: Maybe you don't even have to scale the image. If your intention is to have the image always being displayed at the size of the component , then you can simply do

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);

    // Draw the FULL image, which, regardless of its size (!) 
    // is here painted to just fill this component:
    g.drawImage(fullImage, 0, 0, getWidth(), getHeight(), null);
}

Usually, drawing a scaled image like this is pretty fast. But depending on many factors, separating the step of scaling and painting the image, like you did, may also be a reasonable option.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM