2013-04-18 10 views
10

È noto in .NET che i tipi non sono garbage collection, il che significa che se stai giocando con f.ex. Reflection.Emit, devi stare attento a scaricare AppDomains e così via ... Almeno così ho capito come funzionano le cose.I tipi di dati MakeGenericType/generici vengono raccolti?

Questo mi ha fatto chiedo se i tipi generici sono garbage collection, per essere più precisi: i generici creati con MakeGenericType, diciamo ... per esempio in base all'input dell'utente. :-)

Così ho costruito il seguente test case:

public interface IRecursiveClass 
{ 
    int Calculate(); 
} 

public class RecursiveClass1<T> : IRecursiveClass 
            where T : IRecursiveClass,new() 
{ 
    public int Calculate() 
    { 
     return new T().Calculate() + 1; 
    } 
} 
public class RecursiveClass2<T> : IRecursiveClass 
            where T : IRecursiveClass,new() 
{ 
    public int Calculate() 
    { 
     return new T().Calculate() + 2; 
    } 
} 

public class TailClass : IRecursiveClass 
{ 
    public int Calculate() 
    { 
     return 0; 
    } 
} 

class RecursiveGenericsTest 
{ 
    public static int CalculateFromUserInput(string str) 
    { 
     Type tail = typeof(TailClass); 
     foreach (char c in str) 
     { 
      if (c == 0) 
      { 
       tail = typeof(RecursiveClass1<>).MakeGenericType(tail); 
      } 
      else 
      { 
       tail = typeof(RecursiveClass2<>).MakeGenericType(tail); 
      } 
     } 
     IRecursiveClass cl = (IRecursiveClass)Activator.CreateInstance(tail); 
     return cl.Calculate(); 
    } 

    static long MemoryUsage 
    { 
     get 
     { 
      GC.Collect(GC.MaxGeneration); 
      GC.WaitForFullGCComplete(); 
      return GC.GetTotalMemory(true); 
     } 
    } 

    static void Main(string[] args) 
    { 
     long start = MemoryUsage; 

     int total = 0; 
     for (int i = 0; i < 1000000; ++i) 
     { 
      StringBuilder sb = new StringBuilder(); 
      int j = i; 
      for (int k = 0; k < 20; ++k) // fix the recursion depth 
      { 
       if ((j & 1) == 1) 
       { 
        sb.Append('1'); 
       } 
       else 
       { 
        sb.Append('0'); 
       } 
       j >>= 1; 
      } 

      total += CalculateFromUserInput(sb.ToString()); 

      if ((i % 10000) == 0) 
      { 
       Console.WriteLine("Current memory usage @ {0}: {1}", 
            i, MemoryUsage - start); 
      } 
     } 

     Console.WriteLine("Done and the total is {0}", total); 
     Console.WriteLine("Current memory usage: {0}", MemoryUsage - start); 

     Console.ReadLine(); 
    } 
} 

Come si può vedere, i tipi generici sono definiti 'forse ricorsivo', con una classe di 'coda' che segna la fine della ricorsione . E per garantire che GC.TotalMemoryUsage non stia ingannando, ho anche aperto Task Manager.

Fin qui tutto bene. La prossima cosa che ho fatto è stato sparare questa bestia e mentre stavo aspettando un 'Out of memory' ... ho notato che era - contrariamente alle mie aspettative - non consumare più memoria nel tempo. In effetti, mostra un leggero calo nel consumo di memoria nel tempo.

Qualcuno può spiegarlo? I tipi generici sono effettivamente raccolti dal GC? E se è così ... ci sono anche i casi Reflection.Emit che sono raccolti?

+0

Si potrebbe sperimentare la creazione di un 'WeakReference' al vostro tipo generico creato, quindi verificare se il' Target' è nullo dopo l'esecuzione di un passaggio GC. –

+0

Forse questo aiuterà [Come i generici vengono compilati dal compilatore JIT?] (Http://stackoverflow.com/questions/5342345/how-do-generics-get-compiled-by-the-jit-compiler) –

risposta

19

Per rispondere alla tua prima domanda:

costruzioni generici dei tipi non sono raccolti.

Tuttavia, se si costruisce C<string> e C<object>, il CLR in realtà genera il codice per i metodi una sola volta; poiché il riferimento alla stringa e il riferimento all'oggetto sono garantiti per essere della stessa dimensione, possono farlo in modo sicuro. È abbastanza intelligente. Se si costruiscono C<int> e C<double>, il codice per i metodi viene generato due volte, una volta per ogni costruzione. (Supponendo che il codice per i metodi viene generato affatto naturalmente; metodi sono jitted su richiesta, per questo la sua chiamata jitting.)

Per dimostrare che tipi generici non vengono raccolti, invece creare un tipo generico

class C<T> { public static readonly T Big = new T[10000]; } 

C<object> e C<string> condividere qualsiasi codice generato per i metodi, ma ognuno ottiene i propri campi statici e quei campi vivranno per sempre. Più tipi costruisci, più memoria sarà riempita con quei grandi array.

E ora sai perché questi tipi non possono essere raccolti; non abbiamo modo di sapere se qualcuno tenterà di accedere a un membro di uno di quegli array in qualsiasi momento in futuro. Dal momento che non sappiamo quando sarà l'ultimo accesso all'array, dovranno vivere per sempre, e quindi il tipo che lo contiene deve vivere per sempre.


Per rispondere alla seconda domanda: Esiste un modo per creare assiemi emessi dinamicamente che vengono raccolti?

Sì.La documentazione è qui:

http://msdn.microsoft.com/en-us/library/dd554932.aspx

+0

@ ErikLippert Wow, questa è un'ottima risposta Eric, grazie! Ho capito cosa fosse la statica, ed è per questo che non ho aggiunto una statica nelle classi. Sto ancora pensando che la linea "in realtà genera il codice per i metodi solo una volta [..]". Significa anche che se si hanno due classi generiche di tipo di riferimento senza statiche, internamente esiste solo un "tipo" in memoria? (ad esempio poiché un tipo è solo codice e nessuna memoria verrà creato una sola volta) Ma ciò implica anche un limite di ottimizzazione JIT (l'inlining sarà impossibile se T può essere Foo e Bar con solo 1 istanza del codice)? – atlaste

+0

Se una copia del codice per un metodo come 'string GetTypeName (T param) {return typeof (T) .ToString();}' sarà condiviso per tutti i tipi di classe 'T', come fa il codice a sapere di che tipo si chiama con? È passato in qualche modo come parametro nascosto? – supercat

+3

@supercat: Magic! –

Problemi correlati