# Große Dateibestände löschen - Speicherproblem



## Timoo (7. Dez 2007)

Hallo zusammen,

bin neu hier im Forum und hoffe auf eure fachkundige Hilfe 

Ich will mit einem Java Programm aufgrund unterschiedlicher Kriterien Dateien Löschen. Das Problem ist, dass sich dabei nicht um ein paar wenige Dateien handelt, sondern um viele Millionen (ca. 30 Millionen). Der Löschvorgang ist auch keine Einmalaktion, sondern wird kontinuierlich benötigt.

Ich habe dazu auch schon eine deleteTree() Methode geschrieben, die soweit funktioniert. Wenn in einem Ordner allerdings z.B. 500.000 Dateien liegen, bricht das Programm mit der Fehlermeldung: "java.lang.outOfMemoryError" ab. Den JVM Speicher habe ich auf dem Standard Wert belassen. Nun könnte ich diesen zwar erhöhen, jedoch weiß ich nicht wieviele Dateien sich maximal in einem Ordner befinden können. Daher ist das spekulativ.

Der Methode gebe ich einen Root-Pfad mit. Dieser wird dann rekursiv durchsucht (auch Unterverzeichnisse). Leere Verzeichnisse werden gelöscht. Um die Abbruchbedingung für die Schleife zu definieren sage ich, dass er abbrechen soll wenn keine Dateien mehr im Verzeichnis sind: path.listFiles(). So bis 100.000 und vielleicht auch mehr ist das kein Problem. Aber bei 500.000 gibt den Abbruch.

In der Methode werden Dateien die älter als ein definiertes Datum sind gelöscht.

Hat zu dem Thema vielleicht jemand schon Erfahrungen oder Anregungen? Freu mich über jeden Hinweis!!

Hier der Code der deleteTree() Methode:




```
//-----------------------------------------------------------------------------------
//Methode zum Löschen
	public void deleteTree(File path) {
		
		Calendar myCal = new GregorianCalendar();  
		myCal.add(Calendar.DAY_OF_MONTH, daysBefore);
		Date delBeforeDate = myCal.getTime();
		Date fileDate = new GregorianCalendar().getTime();
		    	
		for ( File file : path.listFiles() ) 
	    { 
	      fileDate.setTime(file.lastModified());
	      
	      if ( file.isDirectory() ){
	    	  deleteTree(file);  		   
	      }
	      else if (fileDate.before(delBeforeDate)){
	    	  
	    	  if (file.delete() == true){
	    		  file.delete();
	    		  filesDeleted++;
	    		  delLogger.info("Datei gelöscht: " + file.getPath());
	    		  errInfoLogger.info("Freier JVM Speicher: " + rt.freeMemory());
	    	  }
	    	  else{
	    		  corruptDelLogger.info("Fehler beim Löschen: " + file.getPath());
	    		  errInfoLogger.info("Freier JVM Speicher: " + rt.freeMemory());
	    	  }
	      }
	      else {
	    	  notDelLogger.info("Datei jünger als vorgegebenes Datum: " + file.getPath());
	    	  errInfoLogger.info("Freier JVM Speicher: " + rt.freeMemory());
	      }
	    }//Ende for Schleife 
	    
	    //Leere Ordner löschen
	    if(path.list().length == 0){
	    	if (path.delete() == true){
	    		path.delete();  
	    		foldersDeleted++;
	    		delFolderLogger.info("Ordner gelöscht: " + path.getPath());
	    		errInfoLogger.info("Freier JVM Speicher: " + rt.freeMemory());
	    	}
	    	else{
	    		corruptDelLogger.info("Fehler beim Löschen: " + path.getPath());
	    		errInfoLogger.info("Freier JVM Speicher: " + rt.freeMemory());
	    	}
	    }
	    else{    	
	    	notDelLogger.info("Ordner ist nicht leer: " + path.getPath());
	    	errInfoLogger.info("Freier JVM Speicher: " + rt.freeMemory());
	    }
	    
	}//Ende DeleteTree()
	//-----------------------------------------------------------------------------------
```


Ich hoff ihr könnt mir helfen.

Viele Grüße
Timo


----------



## maki (7. Dez 2007)

Meine Tipps:

1. Lerne die JVM Parameter Xms, Xmx, PermSize, MaxPermSize kennen
2. Benutze jconsole (ab Java 5) um den wirklichen Speicherbedarf deines Programmes zu ermitteln
3. Verwende Iteration statt Rekursion um zu verhindern, dass dir die Stackframes ausgehen
4. Imho wäre ein Script besser geeignet als eine OO Sprache, aber das ist geschmackssache


----------



## Timoo (7. Dez 2007)

Hi Maki,

und vielen Dank für Tipps, werd mich mal in die Themen einlesen.

Ich hätte auch eine Skriptsprache genommen, ist aber aufgrund der gegebenen Infrastruktur nicht möglich.


----------



## ARadauer (7. Dez 2007)

wie maki schon erwähnt hat ist eine rekursion nicht immer klug, da für jeden methoden aufruf speicher am stack benutzt wird. was sehr schnell zu einem java.lang.outOfMemoryError führt.

also sowas hier muss raus



> if ( file.isDirectory() ){
> deleteTree(file);
> }



vielleicht die die verzeichnisse in eine array list geben und nach durchlauf des aktuellen verzeichnises, die Inhalte der array list abarbeiten....


----------



## maki (7. Dez 2007)

Ein Stack wäre die "natürliche" Datenstruktur für so eine Aufgabe


----------



## Timoo (7. Dez 2007)

Vielen Dank für die Antworten.

Also nachdem ich jetzt mal etwas weiter getestet hab bin ich zur folgenden Erkenntnis gekommen:

1) Das Problem für den Abbruch liegt nicht in der Rekursion, da der GC nach dem rekusivem Auruf den Speicher leert.

2) Der Knackpunkt ist die Ermittlung der Anzahl der Dateien im Verzeichnis über file.listFiles().length bzw. das befüllen des Arrays mit den Inhalten eines Verzeichnisses über file.listFiles()

Ich hab nur mal den Befehl auf ein mit 500.000 Dateien gefülltes Dir ausgeführt. Über JConsole konnt ich dann zusehen wie der Speicher immer an angestiegen ist bis zum Abbruch. 

file.listFiles() liefert mir ja ein Array mit den Inhalten des Verzeichnisses. D.h. es würde doch nichts bringen wenn ich die einzelnen Verzeichnisinhalte selbst in ein Array schreibe oder? Der Speicher würde so oder so voll laufen? Oder sehe ich das falsch. 



> nach durchlauf des aktuellen verzeichnises


Wie erkenne ich denn das Ende eines Verzeichnisses wenn ich mir nicht vorher die Inhalte geben lasse über file.listFiles()? Oder steh ich irgendwie auf dem Schlauch?

Muss auch dazu sagen, dass ich Java Anfänger bin und diese ganze Speicherthematik und Datenstrukturen für mich Neuland sind.


----------



## maki (7. Dez 2007)

Es ist keine Schande den Heap zu erhöhen 

Java hat einen Standard Heap von 64MB, das war vor 10 Jahren gut genug, heute nicht mehr.

Ich meine, 30 Mio Dateien sind kein Pappenstiel


----------



## NTB (7. Dez 2007)

Aus reiner Neugierde: Wo hat man so viele Dateien und wie werden die erzeugt?


----------



## lhein (7. Dez 2007)

Mir sind folgende Sachen aufgefallen:

1. Du legst in jeder Rekursionsstufe einen GregorianCalendar an, wozu? Einmal am Anfang tät auch reichen.
2. Du addierst in jeder Rekursionsstufe Kalendertage
3. Du legst in jeder Rekursionsstufe 2 neue Date Objekte an.
4. Du führst path.listFiles() n-Mal aus, nämlich bei jedem Schleifendurchlauf
5. ermittel dir einmalig dein Vergleichsdatum als long Wert und vergleich den direkt mit file.getLastModified()
6. if (file.delete() == true){ file.delete(); ... bedeuted nichts weiter wie "Wurde das File gelöscht? Wenn ja, dann lösche das File nochmal". Selbiges gilt für if (path.delete() == true){ path.delete(); ... 

Alternativ könnte man auch die Apache commons.io hernehmen und mal den Code ansehen, wie die das so machen. Evtl. kannst Du da ein paar Rückschlüsse ziehen.

lr


----------



## Timoo (7. Dez 2007)

> Aus reiner Neugierde: Wo hat man so viele Dateien und wie werden die erzeugt?



Die Files entstehen als Temp Files von Briefanlagen eines DMS. Eigentlich sollten die auch gelöscht werden (nach Bearbeitung eines Briefes), ist aber nicht so. Es dürfen auch nicht alle einfach gelöscht werden, da manche noch aktiv sind. Welche weiß man allerdings nicht. Tja und so sammeln sich enorme Datenmengen :-(

Also 500.000 Dateien packt das Programm mit max. 90MB.

Danke LR, deine Punkte sind natürlich richtig, ich hab mich bisher nur an path.listFiles() aufgehangen, die allgemeine Performance ist dabei zu kurz gekommen. Da gibts noch viel zu tun.


----------



## lhein (7. Dez 2007)

Timo, anbei mal ein Verbesserungsvorschlag meinerseits:


```
import java.io.File;

public class FolderDelete
{
    private long compareDate;
    private int filesDeleted = 0;
    private int foldersDeleted = 0;
    
    /**
     * deletes all files in path which are older than n days
     * 
     * @param pathToDelete  the path to delete in
     * @param ageInDays     the age in days a file can have before it will be deleted
     */
    public FolderDelete(final File pathToDelete, final int ageInDays)
        {
        // calculate the compare date
        compareDate = getCompareDate(ageInDays);
        
        // start the cleanup 
        deleteTree(pathToDelete);
        
        // write some statistics
        System.out.println(String.format("Delete resulted in %d deleted folders and %d deleted files.", foldersDeleted, filesDeleted));
        }

    /**
     * calculates the compare date
     * 
     * @param ageInDays the age in days a file can have before it will be deleted
     * @return          the compare date
     */
    private long getCompareDate(final int ageInDays)
        {
        long returnValue = System.currentTimeMillis();
        long duration = (long)ageInDays // days 
                      * 24L             // hours
                      * 60L             // minutes
                      * 60L             // seconds
                      * 1000L;          // millis
        
        return returnValue - duration;
        }
    
    /**
     * deletes all files older than the compareDate inside the given folder
     * 
     * @param path  the folder to delete in
     */
    private void deleteTree(final File path) 
        {
        if (!path.exists())
            {
            // strange, someone deleted the folder before
            return;
            }

        // grab all file objects in folder
        File[] files = path.listFiles();

        // loop them
        for (File file : files)
            {
            if (!file.exists())
                {
                // strange, someone deleted the file/folder before
                continue;
                }
            
            // divide between folders and files
            if (file.isDirectory())
                {
                // call this method again for the folder (recursion)
                deleteTree(file);           
                }
            else
                {
                // check if the file may be deleted
                if (file.lastModified() < compareDate)
                    {
                    if (file.delete())
                        {
                        // file deleted successfully
                        filesDeleted++;
                        }
                    else
                        {
                        // wasn't able to delete the file
                        }
                    }
                else
                    {
                    // file is not old enough to get deleted
                    }
                }
            }
        
        // check if the folder is empty now
        if(path.list().length == 0)
            {
            if (path.delete())
                {
                // empty folder was deleted
                foldersDeleted++;
                }
            else
                {
                // unable to delete the folder
                }
            }
        else
            {
            // the folder is still not empty
            }
        }
    
    public static void main(String[] args)
        {
        new FolderDelete(new File("/tmp/documents/"), 30);
        }
}
```

Nicht an der Einrückung stören, es geht nur ums Prinzip.

Gruß
lr


----------



## Timoo (10. Dez 2007)

Hi LR,

und VIELEN DANK für die Hilfestellung. Sieht an einigen Stellen natürlich wesentlich smarter aus dein Code. Werd mich gleich mal ran machen und meinen Code umschreiben.

Bin mal gespannt, wie sich das auf die Performance und den Speicherverbrauch auswirkt!

Viele Grüße
Timo


----------



## lhein (10. Dez 2007)

Fein, würde mich interessieren, ob sich da nun was geändert hat am Verhalten.

Gruß
lr


----------



## Timoo (10. Dez 2007)

So hab jetzt meine Anwendungsarchitektur umgebaut und deinen Vorschlag integriert.

Konnte allerdings erst auf einen kleinen Dateibestand (4000) testen. Da war die neue Variante rund 5 Sekunden schneller (10 zu 15). Nach dem Löschen hatte ich bei der neuen Variante 1.153.760 bytes freien Speicher, bei der Alten nur 765.856 bytes. Sieht also mal gar nicht schlecht aus. Interessant wirds, wenn das Programm in einer der nächsten Nächte auf einen 2.000.000 Testbestand losgelassen wird.

Auf jeden Fall schonmal vielen Dank LR, du hast mir schon viel weitergeholfen.  :applaus:


----------



## lhein (10. Dez 2007)

Super, freut mich.
Dann mal sehen, wie es bei den Millionen wird.

lr


----------



## Timoo (12. Dez 2007)

So, die letzte Nacht hab ich das Programm mal auf eine Dateistruktur mit 2.000.000 Dateien losgelassen. Der größte Ordner beinhaltete dabei 1.000.000 Dateien.

Das Programm lief problemlos durch ), allerdings ist die Laufzeit nicht zu verachten. Hier mal ein Auszug aus der Runtime Logdatei:

Speicher zu Beginn:
-----------------------
Gesamter JVM Speicher: 2.031.616
Freier JVM Speicher: 1.650.280
Maximaler JVM Speicher: 532.742.144
-----------------------

Delete if File is older than 365 days.

Delete result: 7 deleted folders and 1.774.500 deleted files.

Zeit in Millisekunden: 55.512.563
Zeit in Sekunden: 55.512
Zeit in Stunden: 15,42

Speicher nach Beendigung:
-----------------------
Gesamter JVM Speicher: 275.357.696
Freier JVM Speicher: 231.202.736
Maximaler JVM Speicher: 532.742.144
-----------------------


So wie es aussieht muss ich die maximale Programmlaufzeit konfigurierbar machen. Hab dabei an Threads gedacht. Sind  Threads in Java denn arg Resourcenfressend? Ich bräuchte dann 2 Threads nehm ich mal an. Einen in dem mein Löschprogramm läuft und eines in dem ein Timer läuft. Wenn der Timer einen bestimmten Wert erreicht hat wird der Lösch-Thread beendet. Ist das so vom Ansatz her korrekt? Bzw. muss was spezielles beachten?

Grüße
Timo


----------



## Guest (12. Dez 2007)

Wieso willste denn zwei Threads dafür machen? Du hast doch zu jeder Zeit


```
System.currentTimeMillis();
```

Die merkst du dir, übergibst deinem Programm die maximale Laufzeit und rechnest die auf die aktuelle Zeit. Ergebnis auch merken und bei jedem Schleifendurchlauf (oder halt eben passent zu deinem Code) prüfst du ob die maximale Laufzeit erreicht ist.

Ich denke das ist wesentlich einfacher als zu versuche von einem Thread aus einen anderen Thread zu beenden.


----------



## Guest (12. Dez 2007)

Anonymous hat gesagt.:
			
		

> Wieso willste denn zwei Threads dafür machen? Du hast doch zu jeder Zeit
> 
> 
> ```
> ...



OK der Satz ist doof.  :roll: 

Also Maximale Laufzeit übergeben, die auf die aktuelle Zeit rechnen und das Ergebnis merken und bei jedem Durchlauf gegen die aktuelle Zeit prüfen. (so hoffe jetzt passts    )


----------



## lhein (12. Dez 2007)

```
import java.io.File;

public class FolderDelete
{
    private long compareDate;
    private int filesDeleted = 0;
    private int foldersDeleted = 0;
    private long maxTime = 0L;
   
    /**
     * deletes all files in path which are older than n days
     *
     * @param pathToDelete  the path to delete in
     * @param ageInDays     the age in days a file can have before it will be deleted
     */
    public FolderDelete(final File pathToDelete, final int ageInDays, final long maxRunTime)
        {
        // calculate the compare date
        compareDate = getCompareDate(ageInDays);
       
        // calculate max run time
        maxTime = System.currentTimeMillis() + maxRunTime;

        // start the cleanup
        deleteTree(pathToDelete);
       
        // write some statistics
        System.out.println(String.format("Delete resulted in %d deleted folders and %d deleted files.", foldersDeleted, filesDeleted));
        }

    /**
     * calculates the compare date
     *
     * @param ageInDays the age in days a file can have before it will be deleted
     * @return          the compare date
     */
    private long getCompareDate(final int ageInDays)
        {
        long returnValue = System.currentTimeMillis();
        long duration = (long)ageInDays // days
                      * 24L             // hours
                      * 60L             // minutes
                      * 60L             // seconds
                      * 1000L;          // millis
       
        return returnValue - duration;
        }
   
    /**
     * deletes all files older than the compareDate inside the given folder
     *
     * @param path  the folder to delete in
     */
    private void deleteTree(final File path)
        {
        if (!path.exists())
            {
            // strange, someone deleted the folder before
            return;
            }

        // grab all file objects in folder
        File[] files = path.listFiles();

        // loop them
        for (File file : files)
            {
            if (System.currentTimeMillis() >= maxTime)
            {
                // entweder du return'st / break'st hier um das Programm noch zum Abschluss zu bringen
                // break;   // langsam
                // return;  // schneller
                // oder du verlässt das Programm sofort
                // System.exit(0);  // am schnellsten :)
            }

            if (!file.exists())
                {
                // strange, someone deleted the file/folder before
                continue;
                }
           
            // divide between folders and files
            if (file.isDirectory())
                {
                // call this method again for the folder (recursion)
                deleteTree(file);           
                }
            else
                {
                // check if the file may be deleted
                if (file.lastModified() < compareDate)
                    {
                    if (file.delete())
                        {
                        // file deleted successfully
                        filesDeleted++;
                        }
                    else
                        {
                        // wasn't able to delete the file
                        }
                    }
                else
                    {
                    // file is not old enough to get deleted
                    }
                }
            }
       
        // check if the folder is empty now
        if(path.list().length == 0)
            {
            if (path.delete())
                {
                // empty folder was deleted
                foldersDeleted++;
                }
            else
                {
                // unable to delete the folder
                }
            }
        else
            {
            // the folder is still not empty
            }
        }
   
    public static void main(String[] args)
        {
        new FolderDelete(new File("/tmp/documents/"), 30,  2L // Stunden
                                                                   * 60L // Minuten
                                                                   * 60L // Sekunden
                                                                   * 1000L // Millis);
        }
}
```

Bisl dreckig, aber Du sparst dir das Threading.
Ansonsten schön, daß es nun geklappt hat, wobei der Speicherverbrauch aber ganz ordentlich ist. 

lr


----------



## Timoo (12. Dez 2007)

Vielen Dank ihr 2, euer Weg ist wesentlich einfacher. Hab viel zu kompliziert gedacht.

Habs schon getestet und funktioniert mit return wunderbar, die zusätzliche if Abfrage braucht auch nicht wirklich erkennbar mehr Laufzeit.

Damit ist dann auch meine erste Anfrage  hier im Forum super gut beantwortet, also Danke an alle die was dazu beigetragen haben, bin echt begeistert vom Forum!!   :applaus:


----------



## lhein (12. Dez 2007)

Gern


----------

