2017-10-18 18 views
4

Existe-t-il un moyen d'obtenir plus de détails au moment de l'exécution sur une exception OutOfMemoryException? Ou, cette exception peut-elle ne pas être attrapée par le try/catch englobant et plutôt essayer/attraper plus haut la pile d'appel? Je ne peux pas reproduire en utilisant WinDBG donc il doit être quelque chose que je peux enregistrer à partir de l'application. Je m'excuse pour la longue explication, mais il y a beaucoup de causes possibles à éliminer, que j'explique.C# OutOfMemoryException dans System.Drawing.Bitmap

J'ai lu toutes les possibilités d'une exception OutofMemoryException et je les ai éliminées. Normalement, l'application fonctionne très bien, mais occasionnellement sur certains ordinateurs uniquement, j'obtiens une exception OutOfMemoryException. Comme ces rapports sont sur le terrain et ne sont pas reproductibles localement, je n'ai que des logs à faire. Mais j'ai assez de détails.

Ce qui est étrange:

  • Tout ce qui pourrait logiquement être allouer de la mémoire à proximité est dans un try/catch, mais l'exception est traitée comme non gérée (et pris beaucoup plus haut dans la pile d'appel)
  • Il n'y a pas de StringBuffers en cours d'utilisation
  • L'exception se produit même après le redémarrage et le redémarrage de l'application.
  • L'exception se produit après seulement quelques minutes, et seulement environ 30 Mo de mémoire allouée, dans les morceaux pas plus de 1,5 Mo.
  • Vérifié l'application (conçue pour "tout" processeur) s'exécute en 64 bits.
  • ne manque pas d'espace disque (270 Go gratuit) et le fichier d'échange est activé.
  • ne semble pas être un problème possible de fragmentation LoH.

Ceci est arrivé plusieurs fois dans différentes parties de l'application récemment. La première fois, j'ai conclu qu'il y avait un assembly .NET corrompu, car l'exception se produisait juste quand il chargerait l'assembly System.Web.Serialization. Je pouvais déterminer qu'il se passait juste pendant un appel de méthode où cet assemblage a été utilisé pour la première fois. Réimager l'ordinateur (pour être identique à l'installation d'origine) et mettre à jour Windows a résolu ce problème.

Mais, il me semble hautement improbable que le second cas, client différent, se produisant dans quelques jours, soit aussi de la corruption. Celui-ci se passe dans un endroit où aucun assemblage ne serait chargé. Je repense à la première erreur maintenant. Ce que je sais:

  • Il se passe dans un fil de pool de threads (System.Timers.Timer, [Statthread])
  • de Petit nombre de threads actifs (< 5)
  • Il arrive à l'époque un Le fichier 1MiB est téléchargé. Ceci est lu dans un MemoryStream, ce qui pourrait être aussi gros que 2MiB. Cela est ensuite fourni à un constructeur System.Drawing.Bitmap, ce qui donne un Bitmap d'environ 8 Mo. Cependant, tout cela est dans un try/catch qui attrape System.Exception. La seule chose qui ne soit pas dans le try/catch est de renvoyer la référence byte [], qui devrait juste être une copie de référence, pas une allocation de mémoire.
  • Aucune autre allocation de mémoire significative n'a été effectuée avant cette date. La consultation de heap dans ma version locale, qui devrait être exécutée de manière identique, montre seulement l'icône de l'application et une douzaine d'objets qui seraient sur Small Object Heap.
  • il est répétable sur un système spécifique avec une entrée spécifique. Cependant, ces systèmes sont clonés les uns des autres.La seule variation évidente serait l'ordre des mises à jour de Windows.
  • L'assemblage que je cours est signé. N'y a-t-il pas une somme de contrôle qui assure qu'elle n'est pas corrompue? Pareil pour tous les assemblys système? Je ne vois pas comment cette instance pourrait être expliquée par des DLL corrompues, ou même des données.
  • La visualisation de la pile d'appels au moment de l'exception est étonnamment peu utile. J'indique dans le code ci-dessous où l'exception est levée.
  • Il existe une utilisation des objets COM. Cependant, dans des conditions normales, nous courons l'application pendant des semaines sans problèmes de mémoire, et quand nous obtenons ces exceptions, ils sont presque immédiats, après seulement en utilisant environ 20 objets COM relativement légers (IUPnPDevice)

    // Stack Trace indicates this method is throwing the OutOfMemoryException 
    // It isn't CAUGHT here, though, so not in the try/catch. 
    // 
    internal void Render(int w, int h) 
    { 
        if (bitmap != null) 
        { 
         bitmap.Dispose(); 
         bitmap = null; 
        } 
    
        if (!String.IsNullOrEmpty(url)) 
        { 
        // this information is printed successfully to log, with correct url 
        // exception occurs sometime AFTER this somehow. 
         Logger.Default.LogInfo("Loading {0}", url); 
    
        // when file contents changed (to go from 1MiB to 500MiB, the error went away) 
         byte[] data = DownloadBinaryFile(url); 
         if (data != null) 
         { 
          try 
          { 
           Bitmap bmp; 
           using (var ms = new MemoryStream(data)) 
           { 
            bmp = new Bitmap(ms); 
           } 
           bitmap = bmp; 
          } 
          catch (Exception) 
          { 
           // We do not catch anything here. 
           Logger.Default.LogWarning("WARNING: Exception loading image {0}", url); 
          } 
         } 
    
         // 
         // if we had any errors, just skip this slide 
         // 
         if (bitmap == null) 
         { 
          return; 
         } 
    
         // QUESTION EDIT: 
         // in the problematic version, there was actually an unnecessary 
         // call here that turns out to be where the exception was raised: 
         using(Graphics g = Graphics.FromImage(bitmap)) { 
         } 
    
        } 
    } 
    
    // calling this would trigger loading of the System.Web assembly, except 
    // similar method has been called earlier that read everything. Only 
    // class being used first time is the BinaryReader, which is in System.IO 
    // and already loaded. 
    internal static byte[] DownloadBinaryFile(String strURL, int timeout = 30000) 
    { 
        try 
        { 
         HttpWebRequest myWebRequest = HttpWebRequest.Create(strURL) as HttpWebRequest; 
    
         myWebRequest.KeepAlive = true; 
         myWebRequest.Timeout = timeout; 
         myWebRequest.ReadWriteTimeout = timeout; 
         myWebRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)"; 
    
         Encoding encode = System.Text.Encoding.GetEncoding("utf-8"); 
    
         using (HttpWebResponse myWebResponse = myWebRequest.GetResponse() as HttpWebResponse) 
         { 
          if (myWebResponse.StatusCode != HttpStatusCode.OK) 
          { 
           Logger.Default.LogWarning("WARNING: Response {0} loading {1}", myWebResponse.StatusCode, strURL); 
           return null; 
          } 
    
          using (Stream receiveStream = myWebResponse.GetResponseStream()) 
          { 
           using (BinaryReader readStream = new BinaryReader(receiveStream)) 
           { 
            // this extension method uses MemoryStream, but seems irrelevant since we don't catch anything here. 
            return readStream.ReadAllBytes(); 
           } 
          } 
         } 
        } 
        catch (Exception e) 
        { 
         // we do not catch anything here. 
         Logger.Default.LogError("ERROR: Exception {0} loading {1}", e.Message, strURL); 
        } 
        return null; 
    } 
    

Alors Après tout cela, je reviens à ma première question. Y a-t-il des propriétés connues sur l'objet OutOfMemoryException que je peux inspecter, ou des appels que je peux faire après que l'exception soit levée, pour affiner cela?

Et ... existe-t-il une raison pour laquelle une exception OutOfMemoryException ne serait pas détectée par le premier try/catch, mais serait interceptée plus loin dans la pile d'appels?

+3

Vous pouvez vous attendre à une fragmentation de mémoire à cause du tableau d'octets (données). Laisser DownloadBinaryFile retourner un flux. Vous pouvez choisir de renvoyer un MemoryStream ou un FileStream en fonction de la taille des données. –

+0

Pouvez-vous nous montrer le stacktrace? Cela peut être dû à la fragmentation, voir [https://stackoverflow.com/a/8564247/4684493](https://stackoverflow.com/a/8564247/4684493) pour une solution de contournement possible. Vous pouvez également utiliser [MemoryFailPoint] (https://msdn.microsoft.com/en-us/library/system.runtime.memoryfailpoint.aspx) pour vous aider à déboguer – Hintham

+0

convenu, éventuellement, il pourrait y avoir une fragmentation. Mais cela se produit sur la première ou la deuxième allocation de tas importante. –

Répondre

4

Merci à tous. La réponse est quelque peu curieuse et j'ai eu quelques détails faux qui l'ont rendu difficile à comprendre. L'erreur est ici:

  Bitmap bmp; 
      using (var ms = new MemoryStream(data)) 
      { 
       bmp = new Bitmap(ms); 
      } 
      bitmap = bmp; 

Dans les remarques dans la documentation sur le constructeur Bitmap, je trouve ceci:

Vous devez garder le flux ouvert pendant toute la durée de la Bitmap.

De toute évidence, fermer le MemoryStream immédiatement après la construction violait cela. Un garbage collection entre ceci et quand j'ai réellement utilisé le Bitmap était apparemment en train de créer l'erreur. (EDIT: en fait, il semble qu'il existe une limite autour de 1MiB où la fonction FromStream ne décompresse que la quantité d'un fichier JPEG initialement.Pour JPEG < 1MiB, l'image entière est décompressée et n'utilise pas réellement le flux après l'initialisation Pour un JPEG plus grand, il ne lira pas au-delà du premier 1 Mo jusqu'à ce que ces pixels soient nécessaires)

Il m'est difficile d'imaginer pourquoi Microsoft a fait comme ça. Je ne veux pas garder le flux original ouvert, soit (ce qui est une connexion HTTP) si seule solution que je vois est de cloner le bitmap:

// Create a Bitmap object from a file. 
using (var ms = new MemoryStream(data)) 
{ 
    bmp = new Bitmap(ms); 
    Rectangle cloneRect = new Rectangle(0, 0, bmp.Width, bmp.Height); 
    System.Drawing.Imaging.PixelFormat format = bmp.PixelFormat; 
    this.bitmap = bmp.Clone(cloneRect, bmp.PixelFormat);  
} 

Ce qui a conduit à ma longue recherche frustrant et l'exclusion d'un L'élément clé de l'information était que le code s'exécutant sur une machine cliente était une version légèrement plus ancienne, avec seulement un changement subtil. Graphics.FromImage() était appelé sur le bitmap dans la version précédente, mais cela avait été supprimé. Pourtant, cette version fonctionnait très bien la grande majorité du temps.