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,但分辨率类似。
问题分析与解决方案
核心问题分析
- 重复加载图片导致内存翻倍:JTextPane已经加载过一次图片,外部调用代码又通过
ImageIO.read重新下载加载,同一张图片在内存中存在两份实例,直接增加内存占用。 - 实时缩放的临时内存开销:错误栈显示溢出发生在图片缩放时创建临时
BufferedImage的过程。JDK7的D3D渲染管线处理大尺寸图片缩放时,会创建与目标尺寸一致的临时缓冲,叠加原始图片内存后容易触发堆溢出。 - 多窗口渲染冲突:主窗口未隐藏时的闪烁,是两个顶层窗口的渲染管线冲突导致重绘频繁,进一步加剧内存消耗。
具体修复步骤
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
相关产品推荐
相关产品推荐

