Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

What am I missing about Unity's memory management?

Discussion in 'Scripting' started by danshampf, Nov 29, 2014.

  1. danshampf

    danshampf

    Joined:
    Mar 25, 2014
    Posts:
    11
    Hello all. I've been investigating a memory leak in our app and was able to boil it down to a minimal reproducer: a single game object that does nothing except run this script:

    Code (CSharp):
    1. void Start()
    2.     {
    3.         for( int i=0; i<20; ++i )
    4.         {
    5.             TextAsset asset = Resources.Load("test") as TextAsset;
    6.             Texture2D tex = new Texture2D(0,0,TextureFormat.RGB24,false,false);
    7.             tex.LoadImage( asset.bytes );
    8.             Debug.Log( i + " tex dims: " + tex.width + " x " + tex.height );
    9.         }
    10.     }
    "test" is a 4096x4096 PNG file loaded as .bytes resource (so that Unity stores it compressed until load time). After a few iterations (4 on an iPad Air), this fails with an out of memory error.

    My main question is: why does this not work? In each loop iteration, as the 'tex' reference goes out of scope, shouldn't the memory get marked as unused by C#'s automatic memory management; then at some point the GC should kick in and thus I should be able to run this loop for as long as I want... I thought?

    Now, I also found out that it *does* work if there is a DestroyImmediate(tex) at the end of the loop body. So Unity is able to free the memory, it just doesn't do it automatically...? Am I misunderstanding something fundamental about Unity's memory management that makes it necessary to destroy some objects manually?

    Thanks
     
  2. LuckyBurger

    LuckyBurger

    Joined:
    Jan 25, 2013
    Posts:
    10
    The GC does not get a chance to clean up before an out of memory error occurs.
     
  3. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    Unity objects such as Texture2D and so on are not part of Mono/.net and therefore won't be garbage collected. You'd need to use Resources.UnloadUnusedAssets to get Unity to clean up unused Unity objects.

    --Eric
     
    karljj1, Stoven and djfunkey like this.
  4. danshampf

    danshampf

    Joined:
    Mar 25, 2014
    Posts:
    11
    Thanks for the replies. @LuckyBurger, I forgot to mention: even if I add

    tex = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();

    in the loop body, it will still fail. (In the actual game, the allocations are spread across multiple frames anyway).

    @Eric5h5, I tried calling Resources.UnloadUnusedAssets in each loop iteration, but it didn't make a difference. Afaict, that would anyway only clean up things loaded with Resources.Load, i.e. the TextAsset, but not the Texture2D which is manually new'ed ? The only thing I found that really helped was to explicitly call DestroyImmediate on the tex object, which in turn would mean that memory has to be managed completely manually for some types of objects (which I thought the point of C# was to automate that).

    It's interesting to learn that Unity objects are not part of Mono, I didn't know that was even possible. Do you have any pointers to more documentation on this, I'd love to read up on it? E.g. to know which types are affected exactly, and under what circumstances they are not GC'd. I can't seem to find any details in Unity's scripting docs.

    Thanks!
     
  5. Kirlim

    Kirlim

    Joined:
    Aug 6, 2012
    Posts:
    126
    If I recall correctly, you need to use Destroy(Texture) to unload them from memory. The same goes for materials and meshes.

    Be careful with DestroyImmediate. It literaly destroy the asset. Even from the project.
     
    karljj1 likes this.
  6. danshampf

    danshampf

    Joined:
    Mar 25, 2014
    Posts:
    11
    Is the fact that textures, materials, and meshes need to be destroyed by hand officially documented anywhere?
     
  7. Kirlim

    Kirlim

    Joined:
    Aug 6, 2012
    Posts:
    126
    To be destroyed by hand is relative. If you created them by hand (on code), usually you'll need to manually unload them to avoid memory usage to explode.

    I don't know how it is now, but back in Unity3 I had to work in a photos time line project where users would scroll hundreds and hundreds and hundreds of photos per hour. I had lots of crashes due to memory not being properly released. Back then, by trial and error, and lots of googleing, I managed to control the problem with

    Destroy(texture) (textures generated by code, loaded from WWW class, etc)
    Destroy(material) (special situations where I cloned materials on code)
    (WWW instances) www.Dispose() (this one took me a long time to discover)

    I'm without time to research again now, so I can't point any official or trustworthy references.
     
  8. danshampf

    danshampf

    Joined:
    Mar 25, 2014
    Posts:
    11
    Thanks for elaborating. I find it a bit weird that something so fundamental doesn't seem to be mentioned in the docs (at least I can't seem to find anything). And since you found out about this using trial-and-error as well, I wonder if it's actually a Unity bug and not intentional? Like e.g. I could imagine that the texture just doesn't free GPU memory correctly or something...
     
  9. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    No, nothing to do with Resources.Load:

    So, basically GC, except for Unity objects. Loading a new level will also clean up unused Unity objects.

    You still need to unload the assets; Destroy by itself won't do that. That removes them from the hierarchy and marks them as unused.

    --Eric
     
    Stoven and Kirlim like this.
  10. danshampf

    danshampf

    Joined:
    Mar 25, 2014
    Posts:
    11
    Ok so after accepting that Unity is only "semi garbage collected", I figured no big deal, I'll just clean up after myself (used to it from C++ anyway). Just to run into the next unexpected behavior right away.

    Similar to the previous repro, this is slightly closer to what my game actually does -- manually accessing the pixels of the loaded textures. This again leaks memory and fails (on an iPad) after a couple of iterations with OOM:

    Code (CSharp):
    1. void Start()
    2.     {
    3.         for( int i=0; i<100; ++i )
    4.         {
    5.             TextAsset asset = Resources.Load("test") as TextAsset;
    6.             Texture2D tex = new Texture2D(0,0,TextureFormat.RGB24,false,false);
    7.             tex.LoadImage( asset.bytes );
    8.             Debug.Log( i + " tex dims: " + tex.width + " x " + tex.height );
    9.  
    10.             Color32[] pix = tex.GetPixels32();
    11.             Debug.Log( "pix[0].r = " + pix[0].r );
    12.             // pix = null;
    13.        
    14.             Resources.UnloadAsset( asset );
    15.             DestroyImmediate( tex );
    16.         }
    17.     }
    Note the commented-out line that says pix = null. Commenting this in actually makes it work! Again I'm confused..shouldn't the GC understand that in each loop iteration, the previous 'pix' array is no longer needed? Or does C# have some subtle rules about variable lifetimes in loops that I don't understand, like keeping all the references of all iterations around until the loop terminates (seems weird but who knows?)

    So then I figured alright I'll just manually null the pixel arrays on top of manually destroying textures. Then I hit the following.. again this is a bit closer to what the game really does. Each iteration now loads 2 textures and accesses the pixels of one of them. Even though the textures are deleted and the pix array is null'ed, we run OOM again:

    Code (CSharp):
    1. Texture2D Alloc( string name )
    2.     {
    3.         TextAsset asset = Resources.Load(name) as TextAsset;
    4.         Texture2D t = new Texture2D(0,0,TextureFormat.RGB24,false,false);
    5.         t.LoadImage( asset.bytes );
    6.         Resources.UnloadAsset( asset );
    7.         return t;
    8.     }
    9.  
    10. void Start()
    11.     {
    12.         for( int i=0; i<100; ++i )
    13.         {
    14.             Debug.Log( "Iter=" + i );
    15.  
    16.             Texture2D tex = Alloc("test");
    17.             Texture2D tex2 = Alloc("test2");
    18.  
    19.             Color32[] pix = tex2.GetPixels32();
    20.             pix = null;
    21.  
    22.             DestroyImmediate( tex2 );
    23.             DestroyImmediate( tex );
    24.         }
    25.     }
    For this one I haven't been able to find a workaround, or understand what's going on assuming it isn't a Unity bug..so any help is highly appreciated. Thanks!
     
    Last edited: Dec 2, 2014
  11. danshampf

    danshampf

    Joined:
    Mar 25, 2014
    Posts:
    11
    In case anyone is interested, I filed a Unity bug on this (issue 652749). Turns out it's a little harder to repro than what I described in my previous reply...those cases do fail, but not 100% of the time, so YMMV. But something odd is definitely going on, hence the bug report.