如何在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; } } }
关键优化说明
- 按需读取缩略图:用
ImageReader的readThumbnail方法直接生成目标尺寸的图片,无需加载完整原图,内存占用大幅降低。 - 避免重复加载:通过占位符标记正在加载的文件,防止滚动列表时多次触发同一张图的加载任务。
- 缓存策略优化:使用访问顺序的LinkedHashMap,最近访问的图标会留在缓存中,更贴合用户操作习惯。
- 资源自动释放:用try-with-resources关闭
ImageInputStream,调用reader.dispose()释放资源,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Nicolas Baumann
相关产品推荐
相关产品推荐

