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

如何在JFileChooser中显示大图缩略图且避免Java堆空间错误

解决JFileChooser缩略图内存溢出问题

问题根源

你的代码通过ImageIO.read(f)加载完整尺寸的原图,即便后续缩成128x128的图标,原图占用的内存也不会释放。如果文件夹里有大量高分辨率图片,很快就会耗尽堆内存。另外,滚动文件列表时可能重复触发SwingWorker加载同一张图,进一步加剧内存消耗。

修复方案

以下是优化后的代码,核心改进点在于减少内存占用、避免重复加载:

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class ThumbnailFileChooser extends JFileChooser {

    private static final long serialVersionUID = 1L;

    public ThumbnailFileChooser(File file) {
        super(file);
        setFileView(new ImageFileView());
    }

    class ImageFileView extends FileView {
        private static final int ICON_SIZE = 128;
        // 缓存图标,用占位符标记正在加载的文件
        private final Map<File, Icon> iconCache = Collections.synchronizedMap(new LinkedHashMap<File, Icon>(100, 0.75f, true) {
            private static final long serialVersionUID = 1L;

            @Override
            protected boolean removeEldestEntry(Map.Entry<File, Icon> eldest) {
                return size() > 100;
            }
        });
        // 占位符:标记文件正在加载中
        private final Icon LOADING_PLACEHOLDER = new ImageIcon();

        @Override
        public String getTypeDescription(File f) {
            String extension = getExtension(f);
            if (extension != null && (extension.equals("jpeg") || extension.equals("jpg") || extension.equals("png"))) {
                return "图片文件";
            }
            return null;
        }

        @Override
        public Icon getIcon(File f) {
            String extension = getExtension(f);
            if (extension == null || !(extension.equals("jpeg") || extension.equals("jpg") || extension.equals("png"))) {
                return null;
            }

            Icon icon = iconCache.get(f);
            // 缓存无记录且未在加载时,启动加载线程
            if (icon == null) {
                synchronized (iconCache) {
                    // 二次检查避免多线程重复创建任务
                    if (!iconCache.containsKey(f)) {
                        iconCache.put(f, LOADING_PLACEHOLDER);
                        new SwingWorker<Icon, Void>() {
                            @Override
                            protected Icon doInBackground() throws Exception {
                                return createScaledIcon(f, ICON_SIZE, ICON_SIZE);
                            }

                            @Override
                            protected void done() {
                                try {
                                    Icon scaledIcon = get();
                                    synchronized (iconCache) {
                                        iconCache.put(f, scaledIcon);
                                    }
                                    ThumbnailFileChooser.this.repaint();
                                } catch (Exception ex) {
                                    ex.printStackTrace();
                                    // 加载失败移除占位符
                                    synchronized (iconCache) {
                                        iconCache.remove(f);
                                    }
                                }
                            }
                        }.execute();
                    }
                }
                return LOADING_PLACEHOLDER;
            }
            // 占位符返回null(也可替换为自定义加载中图标)
            return icon == LOADING_PLACEHOLDER ? null : icon;
        }

        // 直接按目标尺寸读取缩略图,跳过全图加载
        private Icon createScaledIcon(File file, int targetWidth, int targetHeight) throws IOException {
            try (ImageInputStream stream = ImageIO.createImageInputStream(file)) {
                Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
                if (!readers.hasNext()) {
                    return null;
                }
                ImageReader reader = readers.next();
                try {
                    reader.setInput(stream);
                    // 计算缩放比例,保持原图宽高比
                    int originalWidth = reader.getWidth(0);
                    int originalHeight = reader.getHeight(0);
                    float scale = Math.min((float) targetWidth / originalWidth, (float) targetHeight / originalHeight);
                    int scaledWidth = Math.round(originalWidth * scale);
                    int scaledHeight = Math.round(originalHeight * scale);
                    // 指定读取参数,直接生成缩略图
                    reader.setThumbnailParameter(0, reader.getDefaultReadParam().setSourceSubsampling(
                            Math.max(1, originalWidth / scaledWidth),
                            Math.max(1, originalHeight / scaledHeight),
                            0, 0
                    ));
                    return new ImageIcon(reader.readThumbnail(0, scaledWidth, scaledHeight));
                } finally {
                    reader.dispose();
                }
            }
        }

        private String getExtension(File f) {
            String ext = null;
            String s = f.getName();
            int i = s.lastIndexOf('.');
            if (i > 0 && i < s.length() - 1) {
                ext = s.substring(i + 1).toLowerCase();
            }
            return ext;
        }
    }
}

关键优化说明

  1. 按需读取缩略图:用ImageReader的readThumbnail方法直接生成目标尺寸的图片,无需加载完整原图,内存占用大幅降低。
  2. 避免重复加载:通过占位符标记正在加载的文件,防止滚动列表时多次触发同一张图的加载任务。
  3. 缓存策略优化:使用访问顺序的LinkedHashMap,最近访问的图标会留在缓存中,更贴合用户操作习惯。
  4. 资源自动释放:用try-with-resources关闭ImageInputStream,调用reader.dispose()释放资源,避免内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 04:35:35