You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AWT/Swing中出现Java堆内存溢出问题该如何解决?

Java堆内存溢出问题排查与解决(全屏图片覆盖层场景)

我开发了一个包含3张不同图片的全屏覆盖层,将BufferedImage实例存储在ArrayList中,通过监听KeyEvent、使用CardLayout实现图片切换(按方向键触发)。单独测试该覆盖层时运行正常,但将其作为应用的一部分启动时,出现Java堆内存溢出错误:

Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: Java heap space
        at java.awt.image.DataBufferInt.<init>(DataBufferInt.java:75)
        at java.awt.image.Raster.createPackedRaster(Raster.java:467)
        at java.awt.image.DirectColorModel.createCompatibleWritableRaster(DirectColorModel.java:1032)
        at java.awt.image.BufferedImage.<init>(BufferedImage.java:331)
        at sun.java2d.pipe.DrawImage.makeBufferedImage(DrawImage.java:324)
        at sun.java2d.pipe.DrawImage.renderImageXform(DrawImage.java:392)
        at sun.java2d.d3d.D3DDrawImage.renderImageXform(D3DDrawImage.java:77)
        at sun.java2d.pipe.DrawImage.transformImage(DrawImage.java:264)
        at sun.java2d.pipe.DrawImage.scaleImage(DrawImage.java:124)
        at sun.java2d.pipe.DrawImage.scaleImage(DrawImage.java:1046)
        at sun.java2d.pipe.ValidatePipe.scaleImage(ValidatePipe.java:207)
        at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3127)
        at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3067)
        at xmessenger.Overlay$ImageViewer.paintComponent(Overlay.java:143)
        at javax.swing.JComponent.paint(JComponent.java:1055)
        at javax.swing.JComponent.paintChildren(JComponent.java:888)
        at javax.swing.JComponent.paint(JComponent.java:1064)
        at javax.swing.JComponent.paintChildren(JComponent.java:888)
        at javax.swing.JComponent.paint(JComponent.java:1064)
        at javax.swing.JComponent.paintChildren(JComponent.java:888)
        at javax.swing.JComponent.paint(JComponent.java:1064)
        at javax.swing.JComponent.paintToOffscreen(JComponent.java:5232)
        at javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:295)
        at javax.swing.RepaintManager.paint(RepaintManager.java:1249)
        at javax.swing.JComponent._paintImmediately(JComponent.java:5180)
        at javax.swing.JComponent.paintImmediately(JComponent.java:4991)
        at javax.swing.RepaintManager$3.run(RepaintManager.java:808)
        at javax.swing.RepaintManager$3.run(RepaintManager.java:796)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
        at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:796)
        at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:769)

Overlay类代码

public class Overlay extends JFrame {

        public Overlay(ArrayList<BufferedImage> imgs) {
            super();
            this.setUndecorated(true);
            getRootPane().setOpaque(true);
            pane = new JPanel();
            pane.setOpaque(false);
            add(pane);
            pane.setLayout(new CardLayout());

            Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
            imgWidth = (int) (dim.width * 0.7 + 0.4);
            imgHeight = (int) (imgWidth / 16 * 9 + 0.4);

            pos = 0;

            images = imgs;

            for (BufferedImage image: images) {
                addFrame(image);
            }

            ((CardLayout)pane.getLayout()).first(pane);
            pane.invalidate();

            this.addKeyListener(new KeyListener() {

                @Override
                public void keyTyped(KeyEvent e) {}

                @Override
                public void keyPressed(KeyEvent e) {
                    if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                        setVisible(false);
                        dispose();
                    }
                    else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                        ((CardLayout)pane.getLayout()).previous(pane);
                        pos--;
                        if (pos < 0) pos = images.size() - 1;
                    }
                    else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
                        ((CardLayout)pane.getLayout()).next(pane);
                        pos++;
                        if (pos > images.size() - 1) pos = 0;
                    }
                }

                @Override
                public void keyReleased(KeyEvent e) {}
            });
        }

        public class ImageViewer extends JPanel {

            public ImageViewer(String src) {
                this.src = src;
                setOpaque(false);
                try {
                    img = ImageIO.read(new URL(src));
                    int imgHeight = img.getHeight(observer);
                    if (imgHeight > 0) {
                        w = (int) (h * ((double) img.getWidth(null) / imgHeight));
                    }
                } catch (IOException ex) {}
            }

            public ImageViewer(BufferedImage image, int width, int height) {
                if (image == null) return;
                setOpaque(false);
                setPreferredSize(new Dimension(width, height));
                setMinimumSize(new Dimension(width, height));
                setMaximumSize(new Dimension(width, height));
                System.out.println(width + "x" +  height);
                w = width;
                h = height;

                img = image;
                int imgHeight = img.getHeight(observer);
                if (imgHeight > 0) {
                    w = (int) (h * ((double) img.getWidth(null) / imgHeight));
                }
            }

            public ImageViewer(String src, int width, int height) {
                setOpaque(false);
                setPreferredSize(new Dimension(width, height));
                setMinimumSize(new Dimension(width, height));
                setMaximumSize(new Dimension(width, height));
                w = width;
                h = height;
                this.src = src;
                try {
                    img = ImageIO.read(new URL(src));
                    int imgHeight = img.getHeight(observer);
                    if (imgHeight > 0) {
                        w = (int) (h * ((double) img.getWidth(null) / imgHeight));
                    }
                } catch (IOException ex) {}
            }

            ImageObserver observer = new ImageObserver() {
                @Override
                public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
                    if (height == 0) return false;
                    w = (int) (h * ((double) width / height));
                    if (getParent() != null) getParent().repaint();
                    return true;
                }
            };

            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                if (img == null) return;
                Graphics2D g2d = (Graphics2D) g;
                int x = (int) (getWidth() - w) / 2;
                int y = (int) (getHeight() - h) / 2;
                System.out.println(x + ", " + y);
                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
                g2d.clearRect(x, y, w, h);
                g2d.drawImage(img, x, y, w, h, null);
            }

            private int w;
            private int h;

            private Image img;
            private String src = null;
        }

        public void addFrame(BufferedImage image) {
            panel = new JPanel();
            panel.setBackground(new Color(12,12,12));

            pane.add(panel);
            panel.setBorder(BorderFactory.createEmptyBorder(90, 90, 90, 90));

            ImageViewer viewer = new ImageViewer(image, imgWidth, imgHeight);
            panel.setLayout(new BorderLayout());
            panel.add(viewer);

            int number = pane.getComponentCount();
            JLabel status = new JLabel(number + " / " + images.size());
            status.setForeground(new Color(235, 235, 235));
            status.setFont(new Font("SegoeUI", Font.PLAIN, 17));
            status.setHorizontalAlignment(SwingConstants.CENTER);
            panel.add(status, BorderLayout.PAGE_END);
        }

        public void prevImage() {
            ((CardLayout)pane.getLayout()).previous(pane);
            pos--;
            if (pos < 0) pos = images.size() - 1;
        }

        public void nextImage() {
            ((CardLayout)pane.getLayout()).next(pane);
            pos++;
            if (pos > images.size() - 1) pos = 0;
        }

        private int imgWidth = 0;
        private int imgHeight = 0;

        private ArrayList<BufferedImage> images;
        private int pos = 0;

        public void showContent() {
            try {
                GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow(this);
                setVisible(true);
            } catch (Exception error) {}
        }

        public static void main(String[] args) {
            ArrayList<BufferedImage> images = new ArrayList<BufferedImage>();
            try {
                images.add(ImageIO.read(new File("D:/My Documents/NetBeansProjects/XMessenger/image.jpg")));
                images.add(ImageIO.read(new URL("http://popov654.pp.ru/copybox/photo.jpg")));
                images.add(ImageIO.read(new URL("http://popov654.pp.ru/copybox/DSCN2155.jpg")));
            } catch (IOException ex) {}
            Overlay o = new Overlay(images);
            o.showContent();
        }

        JPanel pane;
        JPanel panel;
    }

外部调用代码

private void addHyperlinkListener(JTextPane pane) {
        pane.addHyperlinkListener(new HyperlinkListener() {
            @Override
            public void hyperlinkUpdate(HyperlinkEvent e) {
                if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
                    String src = (String) e.getSourceElement().getAttributes().getAttribute(HTML.Attribute.SRC);
                    System.err.println("Image src: " + src);
                    HTMLDocument doc = (HTMLDocument) msgContent.getStyledDocument();
                    Element[] imgs = getElementsByTagName(HTML.Tag.IMG, doc);

                    ArrayList<BufferedImage> images = new ArrayList<BufferedImage>();
                    ArrayList<String> srcs = new ArrayList<String>();
                    
                    int index = -1;
                    for (Element img: imgs) {
                        String source = (String) img.getAttributes().getAttribute(HTML.Attribute.SRC);
                        srcs.add(source);
                        try {
                            images.add(ImageIO.read(new URL(source)));
                        } catch (IOException ex) {}
                        if (!source.equals(src) && index < 0) {
                            index++;
                        }
                    }
                    
                    Overlay overlay = new Overlay(images);
                    for (int i = 0; i < index; i++) {
                        overlay.nextImage();
                    }
                    XMessengerApp.getMainWindow().getFrame().setVisible(false);
                    overlay.showContent();
                    
                }
            }
        });
    }

附加问题

若不隐藏当前应用窗口,覆盖层部分区域会持续闪烁;即使隐藏主窗口,仍无法避免内存溢出错误,且出错后CardLayout中的面板完全不响应按键。应用基于JDesktop框架开发,使用JDK7,本身并不复杂,最大的组件是包含HTML内容的JTextPane,仅包含一段两行文本和3张图片。怀疑是JDK/JRE的Bug,相关案例中虽处理的是TIFF而非JPEG,但分辨率类似。


问题分析与解决方案

核心问题分析

  1. 重复加载图片导致内存翻倍:JTextPane已经加载过一次图片,外部调用代码又通过ImageIO.read重新下载加载,同一张图片在内存中存在两份实例,直接增加内存占用。
  2. 实时缩放的临时内存开销:错误栈显示溢出发生在图片缩放时创建临时BufferedImage的过程。JDK7的D3D渲染管线处理大尺寸图片缩放时,会创建与目标尺寸一致的临时缓冲,叠加原始图片内存后容易触发堆溢出。
  3. 多窗口渲染冲突:主窗口未隐藏时的闪烁,是两个顶层窗口的渲染管线冲突导致重绘频繁,进一步加剧内存消耗。

具体修复步骤

1. 复用JTextPane已加载的图片

修改外部调用代码,直接从HTMLDocument中提取已缓存的图片,避免重复下载:

for (Element img: imgs) {
    String source = (String) img.getAttributes().getAttribute(HTML.Attribute.SRC);
    srcs.add(source);
    // 直接复用文档中已加载的图片
    Object imgObj = img.getAttributes().getAttribute(HTML.Attribute.SRC);
    if (imgObj instanceof BufferedImage) {
        images.add((BufferedImage) imgObj);
    } else {
        // 兜底逻辑:仅当文档未缓存时重新加载
        try {
            images.add(ImageIO.read(new URL(source)));
        } catch (IOException ex) {}
    }
    if (!source.equals(src) && index < 0) {
        index++;
    }
}

2. 提前缩放图片,减少渲染时的临时内存

在Overlay初始化时提前将图片缩放到目标尺寸,避免每次paintComponent时重复缩放:

public Overlay(ArrayList<BufferedImage> imgs) {
    // ... 原有代码 ...
    images = new ArrayList<>();
    // 提前缩放所有图片
    for (BufferedImage image: imgs) {
        if (image == null) continue;
        // 计算等比例缩放后的尺寸
        double scale = Math.min((double)imgWidth / image.getWidth(), (double)imgHeight / image.getHeight());
        int scaledWidth = (int)(image.getWidth() * scale);
        int scaledHeight = (int)(image.getHeight() * scale);
        
        // 创建缩放后的图片
        BufferedImage scaledImg = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = scaledImg.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        g2d.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
        g2d.dispose();
        
        images.add(scaledImg);
        // 释放原始图片内存
        image.flush();
    }
    // ... 原有代码 ...
}

同时修改ImageViewer的paintComponent方法,直接绘制缩放后的图片:

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (img == null) return;
    Graphics2D g2d = (Graphics2D) g;
    int x = (getWidth() - img.getWidth()) / 2;
    int y = (getHeight() - img.getHeight()) / 2;
    g2d.drawImage(img, x, y, null);
}

3. 优化窗口显示逻辑,避免渲染冲突

显示覆盖层时将主窗口最小化,减少系统渲染压力,并在覆盖层关闭时恢复主窗口状态:

// 替换原有显示逻辑
XMessengerApp.getMainWindow().getFrame().setState(Frame.ICONIFIED);
overlay.showContent();

// 为Overlay添加窗口关闭监听,恢复主窗口
overlay.addWindowListener(new WindowAdapter() {
    @Override
    public void windowClosed(WindowEvent e) {
        XMessengerApp.getMainWindow().getFrame().setState(Frame.NORMAL);
        XMessengerApp.getMainWindow().getFrame().setVisible(true);
    }
});

4. 调整JVM堆内存参数(临时兜底)

如果以上优化后仍有问题,可在启动参数中增加堆内存:

-Xmx512m -Xms256m

额外优化建议

  • 移除ImageViewer中无用的构造函数,简化代码;
  • 在Overlay的dispose方法中手动清空所有BufferedImage并调用flush()释放内存;
  • 使用KeyBindings替代KeyListener,提升Swing组件的按键响应可靠性。

内容的提问来源于stack exchange,提问作者Alex654

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 23:40:29