是否有一种解决方法可以解决Java在行走大型目录时表现不佳的问题?

时间:2008-12-10 00:14:35

标签: java performance directory-walk

我正在尝试一次处理一个通过网络存储的文件。由于缓冲不是问题,因此读取文件很快。我遇到的问题只是列出文件夹中的目录。在多个文件夹中,每个文件夹至少有10k个文件。

由于File.list()返回一个数组而不是一个可迭代的数据,因此性能非常慢。 Java关闭并收集文件夹中的所有名称,并在返回之前将其打包成数组。

此错误条目为http://bugs.sun.com/view_bug.do;jsessionid=db7fcf25bcce13541c4289edeb4?bug_id=4285834,但没有解决方法。他们只是说这已经为JDK7修复了。

几个问题:

  1. 有没有人解决这个性能瓶颈?
  2. 我是否想要实现不可能的目标?即使只是遍历目录,性能仍然会变差吗?
  3. 我是否可以使用具有此功能的beta JDK7版本而无需在其上构建我的整个项目?

10 个答案:

答案 0 :(得分:7)

虽然它不漂亮,但我在启动应用程序之前将dir / ls的输出传递给文件并传入文件名,解决了这种问题。

如果您需要在应用程序中执行此操作,您可以使用system.exec(),但这会产生一些肮脏。

你问道。第一种形式将非常快,第二种形式也应该非常快。

确保每行执行一项(裸露,无装饰,无图形),完整路径并递归所选命令的选项。

编辑:

30分钟才获得目录列表,哇。

让我感到震惊的是,如果使用exec(),你可以将它的stdout重定向到管道而不是将其写入文件。

如果你这样做,你应该立即开始获取文件,并能够在命令完成之前开始处理。

这种互动实际上可能会让事情变慢,但也许不会 - 你可能会尝试一下。

哇,我刚刚为你找到.exec命令的语法并遇到了这个,可能正是你想要的(它使用exec和“ls”列出一个目录,并将结果通过管道传输到你的程序中) :good link in wayback(Jörg在评论中提供了将this one替换为Oracle破坏的sun)

无论如何,这个想法很简单,但正确的代码是令人讨厌的。我将从互联网中窃取一些代码并将其破解 - brb


/**
 * Note: Only use this as a last resort!  It's specific to windows and even
 * at that it's not a good solution, but it should be fast.
 * 
 * to use it, extend FileProcessor and call processFiles("...") with a list
 * of options if you want them like /s... I highly recommend /b
 * 
 * override processFile and it will be called once for each line of output.
 */
import java.io.*;

public abstract class FileProcessor
{
   public void processFiles(String dirOptions)
   {
      Process theProcess = null;
      BufferedReader inStream = null;

      // call the Hello class
      try
      {
          theProcess = Runtime.getRuntime().exec("cmd /c dir " + dirOptions);
      }
      catch(IOException e)
      {
         System.err.println("Error on exec() method");
         e.printStackTrace();  
      }

      // read from the called program's standard output stream
      try
      {
         inStream = new BufferedReader(
                                new InputStreamReader( theProcess.getInputStream() ));  
         processFile(inStream.readLine());
      }
      catch(IOException e)
      {
         System.err.println("Error on inStream.readLine()");
         e.printStackTrace();  
      }

   } // end method
   /** Override this method--it will be called once for each file */
   public abstract void processFile(String filename);


} // end class

谢谢代码捐赠者IBM

答案 1 :(得分:5)

如何使用File.list(FilenameFilter filter)方法并实现FilenameFilter.accept(File dir,String name)来处理每个文件并返回false。

我在Linux vm上运行了这个包含10K +文件的目录,花了不到10秒。

import java.io.File;  
import java.io.FilenameFilter;

public class Temp {
    private static void processFile(File dir, String name) {
        File file = new File(dir, name);
        System.out.println("processing file " + file.getName());
    }

    private static void forEachFile(File dir) {
        String [] ignore = dir.list(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                processFile(dir, name);
                return false;
            }
        });
    }

    public static void main(String[] args) {
        long before, after;
        File dot = new File(".");
        before = System.currentTimeMillis();
        forEachFile(dot);
        after = System.currentTimeMillis();
        System.out.println("after call, delta is " + (after - before));
    }  
}

答案 2 :(得分:4)

另一种方法是让文件通过不同的协议提供服务。据我所知你正在使用SMB而java只是试图将它们列为常规文件。

这里的问题可能不是单独的java(当你使用Microsoft Explorer x:\ shared打开该目录时它的行为如何)根据我的经验,它也需要相当长的时间。

您可以将协议更改为HTTP,仅用于获取文件名。这样你就可以通过http检索文件列表(10k行不应该太多),让服务器处理文件列表。这将非常快,因为它将使用本地资源(服务器中的那些资源)运行

然后当你有了这个列表时,你可以按照你现在的方式处理它们。

关键点是在节点的另一侧有一个辅助机制。

这可行吗?

今天:

File [] content = new File("X:\\remote\\dir").listFiles();

for ( File f : content ) {
    process( f );
}

建议:

String [] content = fetchViaHttpTheListNameOf("x:\\remote\\dir");

for ( String fileName : content ) {
    process( new File( fileName ) );
}

http服务器可能是一个非常小的简单文件。

如果这就是你现在的方式,那么当你只需要文件名时,你所做的就是将所有10k文件信息提取到你的客户端机器上(我不知道有多少信息)供以后处理。

如果现在处理速度非常快,可能会慢一点。这是因为预取的信息不再可用。

试一试。

答案 3 :(得分:3)

我怀疑问题与您引用的错误报告有关。 问题是“仅”内存使用,但不一定是速度。 如果您有足够的内存,则该错误与您的问题无关。

您应该衡量您的问题是否与内存有关。打开垃圾收集器日志并使用例如gcviewer来分析您的内存使用情况。

我怀疑它与导致问题的SMB协议有关。 您可以尝试用另一种语言编写测试并查看它是否更快,或者您可以尝试通过其他方法获取文件名列表,如此处另一篇文章中所述。

答案 4 :(得分:2)

非便携式解决方案是对操作系统进行本机调用并传输结果。

适用于Linux

您可以查看readdir之类的内容。您可以像链接列表一样遍历目录结构,并分批或单独返回结果。

适用于Windows

在Windows中,使用FindFirstFileFindNextFile apis的行为非常相似。

答案 5 :(得分:1)

如果您使用的是Java 1.5或1.6,那么在Windows上解析“dir”命令并解析标准输出流是一种完全可以接受的方法。我过去曾使用这种方法处理网络驱动器,它通常比等待本机java.io.File listFiles()方法返回要快得多。

当然,JNI调用应该比发出“dir”命令更快,更安全。以下JNI代码可用于使用Windows API检索文件/目录列表。此函数可以轻松地重构为新类,因此调用者可以递增地检索文件路径(即一次获取一个路径)。例如,您可以重构代码,以便在构造函数中调用FindFirstFileW并使用单独的方法来调用FindNextFileW。

JNIEXPORT jstring JNICALL Java_javaxt_io_File_GetFiles(JNIEnv *env, jclass, jstring directory)
{
    HANDLE hFind;
    try {

      //Convert jstring to wstring
        const jchar *_directory = env->GetStringChars(directory, 0);
        jsize x = env->GetStringLength(directory);
        wstring path;  //L"C:\\temp\\*";
        path.assign(_directory, _directory + x);
        env->ReleaseStringChars(directory, _directory);

        if (x<2){
            jclass exceptionClass = env->FindClass("java/lang/Exception");
            env->ThrowNew(exceptionClass, "Invalid path, less than 2 characters long.");
        }

        wstringstream ss;
        BOOL bContinue = TRUE;
        WIN32_FIND_DATAW data;
        hFind = FindFirstFileW(path.c_str(), &data);
        if (INVALID_HANDLE_VALUE == hFind){
            jclass exceptionClass = env->FindClass("java/lang/Exception");
            env->ThrowNew(exceptionClass, "FindFirstFileW returned invalid handle.");
        }


        //HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
        //DWORD dwBytesWritten;


        // If we have no error, loop thru the files in this dir
        while (hFind && bContinue){

          /*
          //Debug Print Statment. DO NOT DELETE! cout and wcout do not print unicode correctly.
            WriteConsole(hStdOut, data.cFileName, (DWORD)_tcslen(data.cFileName), &dwBytesWritten, NULL);
            WriteConsole(hStdOut, L"\n", 1, &dwBytesWritten, NULL);
            */

          //Check if this entry is a directory
            if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY){
                // Make sure this dir is not . or ..
                if (wstring(data.cFileName) != L"." &&
                    wstring(data.cFileName) != L"..")
                {   
                    ss << wstring(data.cFileName) << L"\\" << L"\n";
                }
            }
            else{
                ss << wstring(data.cFileName) << L"\n";
            }
            bContinue = FindNextFileW(hFind, &data);
        }   
        FindClose(hFind); // Free the dir structure



        wstring cstr = ss.str();
        int len = cstr.size();
        //WriteConsole(hStdOut, cstr.c_str(), len, &dwBytesWritten, NULL);
        //WriteConsole(hStdOut, L"\n", 1, &dwBytesWritten, NULL);
        jchar* raw = new jchar[len];
        memcpy(raw, cstr.c_str(), len*sizeof(wchar_t));
        jstring result = env->NewString(raw, len);
        delete[] raw;
        return result;
    }
    catch(...){
        FindClose(hFind);
        jclass exceptionClass = env->FindClass("java/lang/Exception");
        env->ThrowNew(exceptionClass, "Exception occured.");
    }

    return NULL;
}

信用:  https://sites.google.com/site/jozsefbekes/Home/windows-programming/miscellaneous-functions

即使采用这种方法,仍然可以获得效率。如果序列化java.io.File的路径,则会产生巨大的性能损失 - 特别是如果路径代表网络驱动器上的文件。我不知道Sun / Oracle在幕后做了什么,但如果你需要除文件路径之外的其他文件属性(例如大小,模式日期等),我发现以下JNI函数比实例化java要快得多.io.File对象在网络上的路径。

JNIEXPORT jlongArray JNICALL Java_javaxt_io_File_GetFileAttributesEx(JNIEnv *env, jclass, jstring filename)
{   

  //Convert jstring to wstring
    const jchar *_filename = env->GetStringChars(filename, 0);
    jsize len = env->GetStringLength(filename);
    wstring path;
    path.assign(_filename, _filename + len);
    env->ReleaseStringChars(filename, _filename);


  //Get attributes
    WIN32_FILE_ATTRIBUTE_DATA fileAttrs;
    BOOL result = GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, &fileAttrs);
    if (!result) {
        jclass exceptionClass = env->FindClass("java/lang/Exception");
        env->ThrowNew(exceptionClass, "Exception Occurred");
    }

  //Create an array to store the WIN32_FILE_ATTRIBUTE_DATA
    jlong buffer[6];
    buffer[0] = fileAttrs.dwFileAttributes;
    buffer[1] = date2int(fileAttrs.ftCreationTime);
    buffer[2] = date2int(fileAttrs.ftLastAccessTime);
    buffer[3] = date2int(fileAttrs.ftLastWriteTime);
    buffer[4] = fileAttrs.nFileSizeHigh;
    buffer[5] = fileAttrs.nFileSizeLow;

    jlongArray jLongArray = env->NewLongArray(6);
    env->SetLongArrayRegion(jLongArray, 0, 6, buffer);
    return jLongArray;
}

您可以在javaxt-core库中找到这个基于JNI的方法的完整工作示例。在我使用Java 1.6.0_38和Windows主机命中Windows共享的测试中,我发现这个JNI方法比调用java.io.File listFiles()或炮轰“dir”命令快大约10倍。

答案 6 :(得分:1)

如果你需要最终处理所有文件,那么通过String []进行Iterable将不会给你带来任何好处,因为你仍然需要去获取整个文件列表。

答案 7 :(得分:0)

我想知道为什么目录中有10k个文件。某些文件系统无法与这么多文件一起使用。文件系统存在特定限制,例如每个目录的最大文件数量和子目录的最大级别数。

我用迭代器解决方案解决了类似的问题。

我需要递归地遍历巨大的directorys和几级目录树。

我尝试Apache commons io的FileUtils.iterateFiles()。但它通过添加List中的所有文件然后返回List.iterator()来实现迭代器。这对记忆非常不利。

所以我更喜欢写这样的东西:

private static class SequentialIterator implements Iterator<File> {
    private DirectoryStack dir = null;
    private File current = null;
    private long limit;
    private FileFilter filter = null;

    public SequentialIterator(String path, long limit, FileFilter ff) {
        current = new File(path);
        this.limit = limit;
        filter = ff;
        dir = DirectoryStack.getNewStack(current);
    }

    public boolean hasNext() {
        while(walkOver());
        return isMore && (limit > count || limit < 0) && dir.getCurrent() != null;
    }

    private long count = 0;

    public File next() {
        File aux = dir.getCurrent();
        dir.advancePostition();
        count++;
        return aux;
    }

    private boolean walkOver() {
        if (dir.isOutOfDirListRange()) {
            if (dir.isCantGoParent()) {
                isMore = false;
                return false;
            } else {
                dir.goToParent();
                dir.advancePostition();
                return true;
            }
        } else {
            if (dir.isCurrentDirectory()) {
                if (dir.isDirectoryEmpty()) {
                    dir.advancePostition();
                } else {
                    dir.goIntoDir();
                }
                return true;
            } else {
                if (filter.accept(dir.getCurrent())) {
                    return false;
                } else {
                    dir.advancePostition();
                    return true;
                }
            }
        }
    }

    private boolean isMore = true;

    public void remove() {
        throw new UnsupportedOperationException();
    }

}

请注意,迭代器会停止迭代一定数量的文件,并且它还有一个FileFilter。

而DirectoryStack是:

public class DirectoryStack {
    private class Element{
        private File files[] = null;
        private int currentPointer;
        public Element(File current) {
            currentPointer = 0;
            if (current.exists()) {
                if(current.isDirectory()){
                    files = current.listFiles();
                    Set<File> set = new TreeSet<File>();
                    for (int i = 0; i < files.length; i++) {
                        File file = files[i];
                        set.add(file);
                    }
                    set.toArray(files);
                }else{
                    throw new IllegalArgumentException("File current must be directory");
                }
            } else {
                throw new IllegalArgumentException("File current not exist");
            }

        }
        public String toString(){
            return "current="+getCurrent().toString();
        }
        public int getCurrentPointer() {
            return currentPointer;
        }
        public void setCurrentPointer(int currentPointer) {
            this.currentPointer = currentPointer;
        }
        public File[] getFiles() {
            return files;
        }
        public File getCurrent(){
            File ret = null;
            try{
                ret = getFiles()[getCurrentPointer()];
            }catch (Exception e){
            }
            return ret;
        }
        public boolean isDirectoryEmpty(){
            return !(getFiles().length>0);
        }
        public Element advancePointer(){
            setCurrentPointer(getCurrentPointer()+1);
            return this;
        }
    }
    private DirectoryStack(File first){
        getStack().push(new Element(first));
    }
    public static DirectoryStack getNewStack(File first){
        return new DirectoryStack(first);
    }
    public String toString(){
        String ret = "stack:\n";
        int i = 0;
        for (Element elem : stack) {
            ret += "nivel " + i++ + elem.toString()+"\n";
        }
        return ret;
    }
    private Stack<Element> stack=null;
    private Stack<Element> getStack(){
        if(stack==null){
            stack = new Stack<Element>();
        }
        return stack;
    }
    public File getCurrent(){
        return getStack().peek().getCurrent();
    }
    public boolean isDirectoryEmpty(){
        return getStack().peek().isDirectoryEmpty();
    }
    public DirectoryStack downLevel(){
        getStack().pop();
        return this;
    }
    public DirectoryStack goToParent(){
        return downLevel();
    }
    public DirectoryStack goIntoDir(){
        return upLevel();
    }
    public DirectoryStack upLevel(){
        if(isCurrentNotNull())
            getStack().push(new Element(getCurrent()));
        return this;
    }
    public DirectoryStack advancePostition(){
        getStack().peek().advancePointer();
        return this;
    }
    public File[] peekDirectory(){
        return getStack().peek().getFiles();
    }
    public boolean isLastFileOfDirectory(){
        return getStack().peek().getFiles().length <= getStack().peek().getCurrentPointer();
    }
    public boolean gotMoreLevels() {
        return getStack().size()>0;
    }
    public boolean gotMoreInCurrentLevel() {
        return getStack().peek().getFiles().length > getStack().peek().getCurrentPointer()+1;
    }
    public boolean isRoot() {
        return !(getStack().size()>1);
    }
    public boolean isCurrentNotNull() {
        if(!getStack().isEmpty()){
            int currentPointer = getStack().peek().getCurrentPointer();
            int maxFiles = getStack().peek().getFiles().length;
            return currentPointer < maxFiles;
        }else{
            return false;
        }
    }
    public boolean isCurrentDirectory() {
        return getStack().peek().getCurrent().isDirectory();
    }
    public boolean isLastFromDirList() {
        return getStack().peek().getCurrentPointer() == (getStack().peek().getFiles().length-1);
    }
    public boolean isCantGoParent() {
        return !(getStack().size()>1);
    }
    public boolean isOutOfDirListRange() {
        return getStack().peek().getFiles().length <= getStack().peek().getCurrentPointer();
    }

}

答案 8 :(得分:0)

使用Iterable并不意味着文件将流式传输给您。实际上它通常是相反的。因此,数组通常比Iterable快。

答案 9 :(得分:0)

您确定这是由于Java,而不仅仅是在一个目录中有10k条目的一般问题,特别是在网络上?

您是否尝试使用win32 findfirst / findnext函数编写概念验证程序以在C中执行相同操作以查看它是否更快?

我不知道SMB的来龙去脉,但我强烈怀疑它需要对列表中的每个文件进行往返 - 这不会很快,特别是在中等延迟的网络上。

在一个数组中有10k个字符串听起来像是不应该过多地对现代Java VM征税的东西。