23

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

性能超级慢,因为 File.list() 返回一个数组而不是一个可迭代的。Java 开始收集文件夹中的所有名称,并在返回之前将其打包到一个数组中。

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

几个问题:

  1. 有人有解决这个性能瓶颈的方法吗?
  2. 我在努力实现不可能吗?即使它只是迭代目录,性能仍然会很差吗?
  3. 我可以使用具有此功能的 beta JDK7 构建,而无需在其上构建我的整个项目吗?
4

10 回答 10

9

虽然它不是很漂亮,但我通过在启动我的应用程序之前将 dir/ls 的输出管道传输到文件并传入文件名来解决此类问题。

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

您询问。第一种形式将非常快,第二种形式也应该很快。

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

编辑:

只需 30 分钟即可获得目录列表,哇。

令我震惊的是,如果您使用 exec(),您可以将其标准输出重定向到管道中,而不是将其写入文件。

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

交互实际上可能会减慢速度,但也许不会——你可以试一试。

哇,我刚刚为您找到了 .exec 命令的语法并遇到了这个,可能正是您想要的(它使用 exec 和“ls”列出了一个目录并将结果通过管道传输到您的程序中进行处理): 好链接在回程中(Jörg 在评论中提供了替换Oracle 打破的 Sun 的这个

无论如何,这个想法很简单,但正确编写代码很烦人。我会从互联网上窃取一些代码并破解它们--brb

/**
 * 注意:仅将此作为最后的手段!它特定于 Windows 甚至
 * 那不是一个好的解决方案,但它应该很快。
 *
 * 要使用它,扩展 FileProcessor 并使用列表调用 processFiles("...")
 * 选项,如果您希望它们像 /s... 我强烈推荐 /b
 *
 * 覆盖 processFile 并且它将为每一行输出调用一次。
 */
导入java.io.*;

公共抽象类 FileProcessor
{
   公共无效进程文件(字符串 dirOptions)
   {
      处理 theProcess = null;
      BufferedReader inStream = null;

      // 调用 Hello 类
      尝试
      {
          theProcess = Runtime.getRuntime().exec("cmd /c dir " + dirOptions);
      }
      捕获(IOException e)
      {
         System.err.println("exec() 方法出错");
         e.printStackTrace();  
      }

      // 从被调用程序的标准输出流中读取
      尝试
      {
         inStream = 新的 BufferedReader(
                                新的 InputStreamReader( theProcess.getInputStream() ));  
         processFile(inStream.readLine());
      }
      捕获(IOException e)
      {
         System.err.println("inStream.readLine() 出错");
         e.printStackTrace();  
      }

   } // 结束方法
   /** 覆盖这个方法——每个文件都会调用一次 */
   公共抽象无效进程文件(字符串文件名);


} // 结束类

感谢IBM的代码捐赠者

于 2008-12-10T00:57:17.247 回答
8

如何使用 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));
    }  
}
于 2013-10-22T14:20:45.590 回答
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 文件信息获取到您的客户端计算机(我不知道该信息有多少),而您只需要文件名以供以后处理.

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

试试看。

于 2008-12-10T00:36:42.407 回答
3

一个不可移植的解决方案是对操作系统进行本地调用并流式传输结果。

对于 Linux

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

对于 Windows

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

于 2008-12-10T00:22:43.370 回答
3

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

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

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

于 2008-12-10T08:49:35.593 回答
1

如果您最终需要处理所有文件,那么在 String[] 上使用 Iterable 不会给您带来任何优势,因为您仍然必须去获取整个文件列表。

于 2008-12-10T12:26:09.167 回答
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 在幕后做了什么,但如果您需要文件路径以外的其他文件属性(例如大小、mod 日期等),我发现以下 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 倍。

于 2013-01-27T17:30:10.217 回答
0

我想知道为什么一个目录中有 10k 个文件。一些文件系统不能很好地处理这么多文件。文件系统有特定的限制,例如每个目录的最大文件数量和子目录的最大级别数量。

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

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

我尝试了 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();
    }

}
于 2008-12-10T12:18:16.267 回答
0

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

于 2009-03-01T07:56:31.117 回答
0

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

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

我不知道 SMB 的来龙去脉,但我强烈怀疑列表中的每个文件都需要往返 - 这不会很快,尤其是在具有中等延迟的网络上。

在一个数组中有 10k 个字符串听起来也不应该对现代 Java VM 征税太多。

于 2009-03-01T08:06:41.153 回答