【问题标题】:JVM throws java.lang.OutOfMemoryError: heap space (File processing)JVM 抛出 java.lang.OutOfMemoryError: heap space (文件处理)
【发布时间】:2018-07-24 11:22:41
【问题描述】:

我编写了一个文件复制处理器,它获取每个文件的 MD5 哈希,将其添加到哈希映射中,然后将所有具有相同哈希的文件添加到名为 dupeList 的哈希映射中。但是在运行大目录进行扫描时,例如 C:\Program Files\ 会抛出以下错误

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.nio.file.Files.read(Unknown Source)
at java.nio.file.Files.readAllBytes(Unknown Source)
at com.embah.FileDupe.Utils.FileUtils.getMD5Hash(FileUtils.java:14)
at com.embah.FileDupe.FileDupe.getDuplicateFiles(FileDupe.java:43)
at com.embah.FileDupe.FileDupe.getDuplicateFiles(FileDupe.java:68)
at ImgHandler.main(ImgHandler.java:14)

我确定它是因为它处理了这么多文件,但我不确定有更好的方法来处理它。我试图让这项工作正常进行,这样我就可以筛选所有孩子的婴儿照片并删除重复项,然后再将它们放在我的外部硬盘驱动器上进行长期存储。感谢大家的帮助!

我的代码

public class FileUtils {
public static String getMD5Hash(String path){
    try {
        byte[] bytes = Files.readAllBytes(Paths.get(path)); //LINE STACK THROWS ERROR
        byte[] hash = MessageDigest.getInstance("MD5").digest(bytes);
        bytes = null;
        String hexHash = DatatypeConverter.printHexBinary(hash);
        hash = null;
        return hexHash;
    } catch(Exception e){
        System.out.println("Having problem with file: " + path);
        return null;
    }
}

public class FileDupe {
public static Map<String, List<String>> getDuplicateFiles(String dirs){
    Map<String, List<String>> allEntrys = new HashMap<>(); //<hash, file loc>
    Map<String, List<String>> dupeEntrys = new HashMap<>();
    File fileDir = new File(dirs);
    if(fileDir.isDirectory()){
        ArrayList<File> nestedFiles = getNestedFiles(fileDir.listFiles());
        File[] fileList = new File[nestedFiles.size()];
        fileList = nestedFiles.toArray(fileList);

        for(File file:fileList){
            String path = file.getAbsolutePath();
            String hash = "";
            if((hash = FileUtils.getMD5Hash(path)) == null)
                continue;
            if(!allEntrys.containsValue(path))
                put(allEntrys, hash, path);
        }
        fileList = null;
    }
    allEntrys.forEach((hash, locs) -> {
        if(locs.size() > 1){
            dupeEntrys.put(hash, locs);
        }
    });
    allEntrys = null;
    return dupeEntrys;
}

public static Map<String, List<String>> getDuplicateFiles(String... dirs){
    ArrayList<Map<String, List<String>>> maps = new ArrayList<Map<String, List<String>>>();
    Map<String, List<String>> dupeMap = new HashMap<>();
    for(String dir : dirs){ //Get all dupe files
        maps.add(getDuplicateFiles(dir));
    }
    for(Map<String, List<String>> map : maps){ //iterate thru each map, and add all items not in the dupemap to it
        dupeMap.putAll(map);
    }
    return dupeMap;
}

protected static ArrayList<File> getNestedFiles(File[] fileDir){
    ArrayList<File> files = new ArrayList<File>();
    return getNestedFiles(fileDir, files);
}

protected static ArrayList<File> getNestedFiles(File[] fileDir, ArrayList<File> allFiles){
    for(File file:fileDir){
        if(file.isDirectory()){
            getNestedFiles(file.listFiles(), allFiles);
        } else {
            allFiles.add(file);
        }
    }
    return allFiles;
}

protected static <KEY, VALUE> void put(Map<KEY, List<VALUE>> map, KEY key, VALUE value) {
    map.compute(key, (s, strings) -> strings == null ? new ArrayList<>() : strings).add(value);
}


public class ImgHandler {
private static Scanner s = new Scanner(System.in);

public static void main(String[] args){
    System.out.print("Please enter locations to scan for dupelicates\nSeperate Location via semi-colon(;)\nLocations: ");
    String[] locList = s.nextLine().split(";");
    Map<String, List<String>> dupes = FileDupe.getDuplicateFiles(locList);
    System.out.println(dupes.size() + " dupes detected!");
    dupes.forEach((hash, locs) -> {
        System.out.println("Hash: " + hash);
        locs.forEach((loc) -> System.out.println("\tLocation: " + loc));
    });
}

【问题讨论】:

  • a) 为什么不增加heap 设置? b)您的数据结构似乎有点复杂 - 地图列表的地图
  • 为什么不读取每个文件,一次一次,计算文件的哈希值,然后移动到下一个文件。
  • 你的 md5hash 方法性能很差。无需将整个文件读入内存(如果文件很大,则会导致内存不足)。您可以一次读取 8192 字节的块并调用摘要对象的更新方法。
  • 我确实尝试增加我的堆,但我仍然在很久以后收到错误。这就是我正在做的事情,我计算每个文件的哈希值,而不是将文件添加到地图中。地图只包含哈希,而不是文件与该哈希关联的位置数组。

标签: java jvm heap-memory


【解决方案1】:

将整个文件读入字节数组不仅需要足够的堆空间,而且文件大小也仅限于Integer.MAX_VALUE原则上(HotSpot JVM 的实际限制甚至是小几个字节)。

最好的解决方案是根本不将数据加载到堆内存中:

public static String getMD5Hash(String path) {
    MessageDigest md;
    try { md = MessageDigest.getInstance("MD5"); }
    catch(NoSuchAlgorithmException ex) {
        System.out.println("FileUtils.getMD5Hash(): "+ex);
        return null;// TODO better error handling
    }
    try(FileChannel fch = FileChannel.open(Paths.get(path), StandardOpenOption.READ)) {
        for(long pos = 0, rem = fch.size(), chunk; rem>pos; pos+=chunk) {
            chunk = Math.min(Integer.MAX_VALUE, rem-pos);
            md.update(fch.map(FileChannel.MapMode.READ_ONLY, pos, chunk));
        }
    } catch(IOException e){
        System.out.println("Having problem with file: " + path);
        return null;// TODO better error handling
    }
    return String.format("%032X", new BigInteger(1, md.digest()));
}

如果底层 MessageDigest 实现是纯 Java 实现,它会将数据从直接缓冲区传输到堆,但这不在您的责任范围内(这将是消耗的堆内存和性能之间的合理权衡)。

上述方法可以毫无问题地处理超过 2GiB 大小的文件。

【讨论】:

  • 需要 api >= 26
【解决方案2】:

考虑使用番石榴:

    private final static HashFunction HASH_FUNCTION = Hashing.goodFastHash(32);

   //somewhere later

   final HashCode hash = Files.asByteSource(file).hash(HASH_FUNCTION);

Guava 会为你缓冲文件的读取。

【讨论】:

【解决方案3】:

你有很多解决方案:

  1. 不要一次读取所有字节,尝试使用BufferedInputStream,每次读取大量字节。但不是所有文件。

    try (BufferedInputStream fileInputStream = new BufferedInputStream( 
            Files.newInputStream(Paths.get("your_file_here"), StandardOpenOption.READ))) {
    
        byte[] buf = new byte[2048];
        int len = 0;
        while((len = fileInputStream.read(buf)) == 2048) {
            // Add this to your calculation
            doSomethingWithBytes(buf);
        }
        doSomethingWithBytes(buf, len); // Do only with the bytes
                                        // read from the file
    
    
    } catch(IOException ex) {
        ex.printStackTrace();
    }
    
  2. 使用 C/C++ 做这样的事情,(嗯,这是不安全的,因为你将自己处理内存)

【讨论】:

  • 可能只是将您的第一个选项与表查找功能一起使用,我只需要进行一些字节操作以使其保持快速。
  • 当您同时拥有自己的(足够大的)缓冲区数组时,使用BufferedInputStream 是没有意义的。你所做的只是强制从BufferedInputStream 的数组到你的数组进行不必要的复制。当您拥有足够大小的数组时,只需直接从源代码InputStream 中读取即可。
【解决方案4】:

FileUtils 的任何实现都试图读取整个文件以计算哈希。这不是必需的:通过读取较小块的内容可以进行计算。事实上,要求这个是一种糟糕的设计,而不是简单地读取需要的块(64 字节?)。所以也许你需要使用更好的库。

【讨论】:

    猜你喜欢
    • 2016-04-15
    • 1970-01-01
    • 2010-09-07
    • 2020-11-30
    • 2021-01-15
    相关资源
    最近更新 更多