I’ve watched a video from Unity Japan addressing memory leaks in Unity. They highlighted a common pitfall in C# programming where ‘Leaked Managed Shells’ occur when destroying the native object where the C# wrapper remains due to Unity’s object management system. Despite seeming null, Unity objects/wrappers can still persist, leading to memory leaks unless nullifying the shells (that allows for the GC to clean them up).
They show this example where the tex wrapper leaks:
I want to make sure I’m understanding the scope of “managed objects” so that I can apply it everywhere in the project. For instance, I have a class called Molecule, and I have a variable that holds one instance of it like this:
private Molecule _targetMolecule;
Whenever a new molecule has to be prepared, I’m calculating the new molecule and assigning it to the _targetMolecule:
public void PrepareMolecule(int difficultyLevel)
{
int maxValue = _dataStorageSO.CalculateMaxValue();
int minValue = 1;
Molecule molecule = _dataStorageSO.GetRandomMolecule(difficultyLevel, minValue, maxValue);
_targetMolecule = molecule; // here
AssignRandomPositions(_targetMolecule, 4);
}
According to this info, this could potentially (or better say most likely) cause leaks, right? If I want to prevent the old reference of _targetMolecule from becoming “orphaned” and leak, shall I nullify it before setting the new instance?:
public void PrepareMolecule(int difficultyLevel)
{
int maxValue = _dataStorageSO.CalculateMaxValue();
int minValue = 1;
Molecule molecule = _dataStorageSO.GetRandomMolecule(difficultyLevel, minValue, maxValue);
_targetMolecule = null; // here
_targetMolecule = molecule;
AssignRandomPositions(_targetMolecule, 4);
}
It’s holding on to the C# reference of the texture because it is assigned to a field. It will be freed once the component where this field is has been destroyed. A leak is completely unrecoverable. The posted example merely keeps the object in the garbage collection pool until its last reference has been released.
No. Assigning null before assigning the new reference has no effect other than running one extra operation and writing more code. The previous object goes out of scope, and assuming there are no other references to that object, the garbage collector will soon collect that object.
Did you see the video? The memory leak shows up in the Memory Profiler. I think the point they try to make is that if you run the code, say, 10 times, the memory of the managed shell for tex will accumulate. If the memory of the wrapper is 200B, then running the code 10 times will generate 200B x 10 = 2.0KB of leaked memory.
I agree with this, it seems to do nothing, to be honest. I have no idea how to avoid any potential leaked shells then.
This is another problem. I’m not sure yet how to correctly identify the leaked managed shells in the memory profiler (I can’t seem to find a way to track my own methods/classes), so I cannot test with certainty.
Another thing I’m unsure about is if these leaked managed shells only happen after ‘Destroying’ the references (e.g., by calling Destroy() on them). In my example, I’m re-assigning the reference instead of destroying the old one and assigning a new one, so I’m not sure if the data will get overridden (basically reusing the wrapper/C# managed shell) or we are creating a new C# wrapper and ditching the old one (which will be causing a leak).
One thing for sure is that I have identified a few memory leaks during gameplay (only a few, but still), which I detected in Xcode > Instruments. So, I thought I might have a few methods similar to this one (perhaps a bit different) where I assumed references were disposed of properly, but in reality, they were not.
You have some fundemental misunderstandings here. First of all note that it’s impossible to create a memory leak in C#. Any (managed) memory in C# is under full control of the garbage collector. So it’s literally impossible to “leak” memory. You may have object sticking around which you don’t use / need anymore, but you still have access to it because you still have a reference to it. Once all references to a managed object are gone, that object instance is up for garbage collection. The GC does not constantly run to clean up garbage memory, but eventually it will be collected.
Now comes the “issue” with Unity. Unity is NOT a C# engine. Unity is written mainly in C++. Over there in native code, we don’t have managed memory but every object that is created needs to be destroyed properly. Because loosing a reference to a native object / native chunk of memory means you have a memory leak. That’s because nobody keeps track of where that memory location may be or who’s currently using it. So loosing the reference / pointer to a native object means it will leak, because you have no way to free that memory since you don’t even know where that memory is.
The Unity C++ core handles it’s native objects properly. Unity tracks it’s own native objects internally. So here you usually shouldn’t expect to get a memory leak either, except when there’s a bug in the Unity core, or you actually play around with manually allocating native memory.
Every UnityEngine.Object derived managed type has a native counterpart. So all those managed C# objects (GameObject, Mesh, Material, Shader, Texture2D, Transform, YourComponentScript, …) they all have a native object which represents this object in the engine core. For most built-in types the managed C# object is just an almost empty “shell”. So the managed object contains almost no data besides a handle to the native object. All those properties like GameObject.name, Transform.position, etc… they all map to native code methods which actually return the data from the native side. So the majority of the data and functionality of those objects live in the native code object and the managed objects are just “wrappers”.
In C# it is impossible to actually destroy or free any memory “manually”. Every managed object is destroyed by the GC once all references are gone. In Unity when you call Destroy on a UnityEngine.Object, the engine core will actually destroy the native part of that object. The handle in the wrapper will be set to null so the wrapper becomes useless or “dead”. It becomes a “fake null object” because such a mangaged object when compared against null would “fake” that it is null, even it isn’t. That’s how you can test if an object is still alive / available
When you create a new Texture2D in managed code (C#) you will actually create the managed wrapper as well as the actual native object that represents the texture. When you call Destroy on that object, the native part will be destroyed, so the actual texture memory, settings etc will be gone. The managed wrapper is still there. You can still pass it around, store it in a new variables, whatever you like. This is still a normal C# object. However you can no longer interact with any of the properties which require the native object. This state is what you, or that video called “leak” but this isn’t a leak at all. You just still hold a reference to a managed object you most likely don’t need anymore. This object will stick around until all references are gone. You don’t need to explicitly set this variable to null in order to get rid of the reference. When you assign ANY different reference or null to that variable, the reference to the old object will be gone and the GC can clean it up.
Note: When you create a Texture2D object and you don’t destroy it but you simply get rid of all references to that Texture2D object, the object will not be destroyed because, as I said earlier, Unity manages those objects on the native side. Any UnityEngine.Object instance can be “found” again by using FindObjectsOfType. This could also be called a “leak” but also isn’t a real leak. When you do something stupid like this every frame, so creating a new Texture2D every frame, you will eventually run out of memory because all those textures will stick around. Note that Unity will clean up those instances when you call UnloadUnusedAssets. This method is automatically called when you load a new scene. You shouldn’t just “throw away” objects you created and assume / rely on that cleanup mechanic. You should destroy what you don’t need anymore unless you load a new scene anyways.
Keep in mind when you destroy a GameObject instance, all components that are on that gameobject as well as all child gameobjects will be destroyed as well. So you don’t need to destroy individual components. Though you can actually destroy individual components to remove them from a gameobject (except the Transform component, that can not be destroyed manually).
To sum up: Those “empty shells” are just ordinary C# objects. They do not leak, they just stick around like any other C# object. In the case of your own components, you can even use your own fields and methods on a destroyed component. However you can no longer access anything that requires the native counterpart. In the case of MonoBehaviours that also means once it’s destroyed, it will loose the connection to the gameobject it was attached to. So while you still can call any of your the methods on your component, any code that tries to interact with the native part would cause a NullReference exception or MissingComponent exception or something like that.
Hopefully that explains some of the basic concepts.
Is Molecule a Unity object derived type or a regular C# class?
If it’s the latter than everything to do with Unity’s managed objects can be completely disregarded and it’s just regular C# garbage collection stuff to consider.
In either case there’s no leaks in any of your code examples.
This is exactly the answer. My concern was that an empty shell was created (like a duplicate) and lost for good, like a leak in C++/C, where you lose track of that pointer. I think is common sense this isn’t the case, but I wanted to confirm it anyway with a quick test:
public float waitForXSeconds = 15;
List<Texture2D> texList = new();
private IEnumerator Start()
{
for (int i = 0; i < 256; i++) texList.Add(new Texture2D(256, 256));
yield return new WaitForSeconds(waitForXSeconds);
for (int i = 0; i < 256; i++) Destroy(texList[i]);
yield return new WaitForSeconds(waitForXSeconds);
for (int i = 0; i < 256; i++) texList.Add(new Texture2D(256, 256));
yield return new WaitForSeconds(waitForXSeconds);
for (int i = 0; i < 256; i++) Destroy(texList[i]);
}
The test (new empty) project has 33 “UnityEngine.Texture2D” classified as “Leaked Managed Shell” by default:
After adding our 256 textures (the first loop), the Leaked Managed Shell Count for Texture2D remained being 33, since all textures are properly referenced and are in use.
In the second loop, we destroy those textures. Had we cleared the list right after destroying the textures, we would see that the Leaked Managed Shell for Texture2D would be 33, since the unused references are eligible to be Garbage Collected. However, since we didn’t do it, the Leaked Managed Shell count is 33 (default) + 256 (our new textures) = 289:
These unused references are around, meaning that are occupying memory and won’t be cleaned up by the GC, even if you trigger it or even if you call UnloadUnusedAssets, they’ll stay around.
The key thing is what @Bunny83 said: we don’t lose track of the unused references, so it’s not a real leak. If we assign new values to it (as in the 3rd loop), we would be effectively reusing that allocated memory, so it doesn’t grow over time, which is what one would worry the most.
So, for the 3rd loop, you would expect to see a Leaked Managed Shell count for the Texture2D of 33 (since we’re reusing the reference), and indeed, that’s the case:
If internally it worked differently and we were indeed losing track of those C# wrappers, then we would have seen a count of 33 + 256 = 289 again. And, after destroying them, it would have been 33 + 256 + 256 = 545 items (and it will grow more and more if you do operations of the like). Fortunately, this is not the case.
Since it’s a fixed memory allocation for those 256 items, you know it will never take more than 256x40B of memory.
So, to answer my own questions:
(1) Don’t need to set the values to null (for _targetMolecule for instance) if you’re reassigning them.
(2) Dispose of the references by nullifying them if you’re not going to be using them again, so that they will be cleaned up in the next cycle by the garbage collector.
Your test and your reasoning is kinda off. A list has an internal array which may be resized when the (start) capacity is not enough to hold additional elements. Ignoring the unused slots capacity slots of the list, after the first loop the list contains 256 elements of Texture2D objects. In the second loop you iterate through the first 256 elements of the list and destroy each of those instances the references reference. This does nothing to the actual references in that list. So those references are still there and so are the referenced objects, they are just “dead”.
Note that those reference are not removed from the list, so they are still there. When your third loop completes you have added another 256 elements to the list so the list now has 512 elements. The first 256 still contain the old dead objects while the second half of the list contains the newly created textures (index 256-511). That’s how Add of a List works. It adds elements to the end of the list.
Your fourth loop actually does nothing since you again iterate through the first 256 elements of the list which are already dead / destroyed. So all those destroy call don’t actually destroy anything and the newly created textures are all still there.
You simply shouldn’t really care about managed objects that much. As I said, those empty “shells” behave like ANY other C# object. As long as you have a reference to them, they stick around. Once all references are lost, they would be up for garbage collection.
Note that when you actually Remove an element from a List or when you Clear the list, the internal array is still there, but the List actually sets the unused elements of the internal list to null (more generally to the default value). The List does that specifically to avoid keeping references around that should no longer be there.
The final question is: In which situation do you store an object in a class field that suddenly isn’t required anymore. Note that if an object that has a field with a reference to another class instance, but that first object is collected by the GC, if that field was the only reference to that second object, it would be collected as well. So there’s no need to set variables to null in almost all cases. The same is true for local variables in a method. When the method exits, all local variables will go out of scope and extent. So setting such variables to null at the end is also a complete waste of time. The variables don’t exist anymore once the method finishes.
Yeah, I noticed it after writing the post. I should’ve used an array or Texture2D references, I don’t know what I was even thinking!
But the point holds. If you have 50 Texture2D references > Then you assign the references > Then destroy the references, then you’d have 33 + 50 (83) Leaked Managed Shell items for the Texture2D. If after destroying the textures, you assign new Texture2Ds to your 50 Texture2D references, then the Leaked Managed Shells drop back to 83 - 50 (33 default) items.
For sure, knowing this I wouldn’t care so much. I just wanted to make sure that was the case.
For example in a panel that is heavy in graphics resources and only appears at some point in the application. If it takes a few percentage of the total application time, you may want to free those resources.
But the “heavy resources” are on the native side when we talk about Unity objects and “empty shells”. So yes, you should destroy things you don’t need any more, but setting it to null is in almost all cases not necessary. Apart from that, panels, menus and stuff like that are either separate scenes and therefore loaded when needed and when switching back they are removed automatically, or they are actually kept in the background and reused when they are required (so they are cached)
The only cases where I would set a variable specifically to null would be due to a functional reason, like a variable that holds the equipped item / weapon. When you unequip an item you would set the slot to null. However this does not free any memory as we usually have the items / weapons stored in the inventory, we just cleared the reference to indicate that nothing is equipped at the moment.
For a lot of Unity objects (the majority, probably), the C# class is only a wrapper around the C++ side. Once the C++ side is destroyed and the memory released, the memory footprint of C# managed shell is probably negligible.
Mind you that code is going to leak because accessing Renderer.material is going to create a material instance, and remain in memory until the application closes.