2010-06-09 1 views
2

J'utilise une servlet pour faire un multi fileupload (en utilisant apache commons fileupload). Une partie de mon code est affichée ci-dessous. Mon problème est que si je télécharge plusieurs fichiers à la fois, la consommation de mémoire du serveur de l'application saute plutôt drastiquement. C'est probablement OK si ce n'était que jusqu'à ce que le téléchargement du fichier soit terminé, mais le serveur de l'application semble rester en mémoire et ne le renvoie jamais au système d'exploitation. Je crains que lorsque je mettrai cela en production, je finirai par avoir une exception de mémoire insuffisante sur le serveur. Des idées sur pourquoi cela se passe-t-il? Je pense que le serveur a peut-être démarré une session et redonnera de la mémoire après son expiration, mais je ne suis pas 100% positif.Consommation de fichier de téléchargement de fichier de servlet

if (ServletFileUpload.isMultipartContent(request)) { 
     ServletFileUpload upload = new ServletFileUpload(); 
     FileItemIterator iter = upload.getItemIterator(request); 
     while (iter.hasNext()) { 
      FileItemStream license = iter.next(); 
      if (license.getFieldName().equals("upload_button") || license.getName().equals("")) { 
       continue; 
      } 
      // DataInputStream stream = new DataInputStream(license.openStream()); 
      InputStream stream = license.openStream(); 
      List<Integer> byteArray = new ArrayList<Integer>(); 
      int tempByte; 
      do { 
       tempByte = stream.read(); 
       byteArray.add(tempByte); 
      } while (tempByte != -1); 
      stream.close(); 
      byteArray.remove(byteArray.size() - 1); 
      byte[] bytes = new byte[byteArray.size()]; 
      int i = 0; 
      for (Integer tByte : byteArray) { 
       bytes[i++] = tByte.byteValue(); 
      } 
     } 
    } 

Merci d'avance!

+0

J'ai mis à jour le code pour refléter les suggestions @skaffman et @ Bozho. Je crée maintenant l'objet ServletFileUpload en utilisant un DiskFileItemFactory, par lequel je passe des paramètres personnalisés à. Je gère également le InputStream de la bonne façon, en le fermant dans un bloc finally. Cependant, cela ne semble toujours pas avoir réglé le problème. Voici ce que je pense pourrait se produire. J'utilise une base de données derby, qui semble être ouverte dans le même thread que le serveur. Je stocke également les objets en tant que Blobs. Est-ce que la base de données elle-même est lue en mémoire et est maintenue? Merci pour tous les bons commentaires jusqu'à présent! Scott – Scott

+0

Avez-vous réussi à trouver la solution? J'ai le même problème! –

+0

Je n'ai pas vu autant de SO-ers célèbres dans une question :) – gkiko

Répondre

0

La bonne façon de gérer les flux en Java (au moins jusqu'à ce que Java 7) est:

InputStream is; 
try { 
    is = ... 
} catch (IOEXception ex) { 
    // report exception - print, or throw a wrapper 
} finally { 
    try { 
     is.close(); 
    } catch (IOException ex) {} 
} 

(exploitation forestière peut-être l'exception dans la deuxième capture ainsi)

Si vous ne fermez pas vos flux, la mémoire ne sera pas libérée par le garbage collector.

2

Lors de la construction d'un ServletFileUpload, vous devez transmettre un objet FileItemFactory (plus précisément un DiskFileItemFactory) que vous configurez vous-même, plutôt que de vous fier aux valeurs par défaut. Les valeurs par défaut peuvent ne pas convenir à vos besoins, en particulier dans les environnements de production à haut volume.

+0

Il utilise déjà le ["streaming mode"] (http://commons.apache.org/fileupload/streaming.html). Il n'en profite cependant pas en allouant déjà trop de mémoire lui-même. – BalusC

1

Ici

ArrayList<Integer> byteArray = new ArrayList<Integer>(); 
int tempByte; 
do { 
tempByte = stream.read(); 
byteArray.add(tempByte); 

vous écrivez chaque octet directement dans la mémoire dans un tableau d'entiers! Chaque entier consomme 4 octets de mémoire alors que vous n'en avez besoin que d'un octet pour chaque octet lu. Effectivement, vous devriez utiliser ArrayList<Byte> ou mieux byte[] à la place puisque chaque byte coûte seulement un octet de mémoire, mais que Saldo allouerait toujours autant de mémoire que le fichier est grand.

Et ici

byte[] bytes = new byte[byteArray.size()]; 

vous allouez après autant de mémoire que le fichier est grand. Par saldo vous êtes à la fois avec ArrayList<Integer> et byte[] allouer 5 fois plus de mémoire que le fichier est grand.

C'est une perte.

Vous devez l'écrire dans OutputStreamimmédiatement, par ex. FileOutputStream.

InputStream input = null; 
OutputStream output = null; 
try { 
    input = license.openStream(); 
    output = new FileOutputStream("/file.ext"); 
    byte[] buffer = new byte[1024]; 
    for (int length; (length = input.read(buffer)) > 0;) { 
     output.write(buffer, 0, length); 
    } 
} finally { 
    if (output != null) try { output.close(); } catch (IOException logOrIgnore) {} 
    if (input != null) try { input.close(); } catch (IOException logOrIgnore) {} 
} 

Cela coûte effectivement seulement 1KB de mémoire pour le tampon à la place de la longueur totale du fichier d'octets (ou 4 fois lors de l'utilisation de celui-ci des nombres entiers).

Ou si vous vraiment voulez avoir dans un byte[] puis juste sauter toute étape ArrayList<Integer>. Cela n'a aucun sens. Utilisez un ByteArrayOutputStream comme OutputStream.

InputStream input = null; 
ByteArrayOutputStream output = null; 
try { 
    input = license.openStream(); 
    output = new ByteArrayOutputStream(); 
    byte[] buffer = new byte[1024]; 
    for (int length; (length = input.read(buffer)) > 0;) { 
     output.write(buffer, 0, length); 
    } 
} finally { 
    if (output != null) try { output.close(); } catch (IOException logOrIgnore) {} 
    if (input != null) try { input.close(); } catch (IOException logOrIgnore) {} 
} 

byte[] bytes = output.toByteArray(); 

Cela coûte cependant encore autant de mémoire que le fichier est grand, il est seulement maintenant pas 5 fois la taille de fichier plus que vous avez d'abord avec ArrayList<Integer> et un byte[] après.


Mise à jour: selon votre commentaire que vous souhaitez le stocker dans la base de données. Vous pouvez également le faire sans stocker le fichier entier dans la mémoire de Java. Il suffit d'écrire le InputStreamimmédiatement à la base de données en utilisant PreparedStatement#setBinaryStream().

final String SQL = "INSERT INTO file (filename, contentType, content) VALUES (?, ?, ?)"; 
String filename = FilenameUtils.getName(license.getName()); 
InputStream input = license.openStream(); 

Connection connection = null; 
PreparedStatement statement = null; 
try { 
    connection = database.getConnection(); 
    statement = connection.prepareStatement(SQL); 
    statement.setString(1, filename); 
    statement.setString(2, getServletContext().getMimeType(filename)); 
    statement.setBinaryStream(3, input); 
    statement.executeUpdate(); 
} catch (SQLException e) { 
    throw new ServletException("Saving file in DB failed", e); 
} finally { 
    if (statement != null) try { statement.close(); } catch (SQLException logOrIgnore) {} 
    if (connection != null) try { connection .close(); } catch (SQLException logOrIgnore) {} 
} 
+0

Merci pour cet exemple! La raison pour laquelle je mets tout dans un tableau d'entiers est parce que la méthode read() renvoie un int. En regardant le JavaDoc, on dirait qu'il retourne un int entre 0 et 255, donc je suppose que je pourrais lancer en toute sécurité un octet. Je pense que vous êtes le dernier exemple de ce dont j'ai besoin, car j'ai besoin du fichier dans son ensemble pour le stocker dans la base de données comme un blog. Merci! – Scott

+0

Dans ce cas, l'utilisation de 'PreparedStatement # setBinaryStream()' est la plus efficace en termes de mémoire. – BalusC

+0

Cela a du sens, mais j'utilise Hibernate pour la persistance. Y at-il un moyen de le stocker via Hibernate sans tout garder en mémoire? – Scott