Unloaded actor not being destroyed?

Hello there,

I’ve noticed that sometimes upon unloading a data layer, I notice an actor in that layer being unloaded. Specifically, UnregisterAllComponents() is called on that actor via ULevel::RemoveLoadedActors() and the actor shows as unloaded in the outliner. However, BeginDestroy() is never called on the actor and the actor is never GC’ed.

This is problematic because that actor maintains FComponentReference uproperties to components in the same data layer, so when the data layer is reloaded, the actor is not reconstructed and reloaded since it was never destroyed, and the weak pointers backing the component references become stale.

First of all, unloading an object without destruction via data layers something that is known to happen? If yes, I can use FSoftComponentReference to handle this case.

If this is not something that is known to happen, how can I debug garbage collection for the exact object I care about, to figure out specifically why it isn’t GC’ed? I’ve already figured out that the actor I’d like GC’ed is never marked as unreachable in the object iteration parallelfor in GarbageCollection.cpp::GatherUnreachableObjects(), because it is never marked as “maybeunreachable” but I’m not sure how to check on where that should happen for my actor to see why that isn’t happening.

Thanks for any input!

Hey Ryan,

> First of all, unloading an object without destruction via data layers something that is known to happen?

Yes, the reuse of recently destroyed levels/actors is intentional, the level streaming logic will reuse actors after a level has been unloaded if those are still around and haven’t been GC’ed yet.

This behavior is controlled by the cvar LevelStreaming.ShouldReuseUnloadedButStillAroundLevels. As a quick test you can try is setting that to 0 to see if that fixes the stale references.

This would result in BeginDestroy not being called and instead the object going from Uninitialize Components back to PreInitialize Components. You can see this special case in the diagram on the Actor Lifecycle here.

However, the fact that the FComponentReference gets stale due to this actor reuse happening is definitely unintended behavior and sounds like a bug. A soft reference sounds like a good workaround in this case as it should be able to deal with the other actor becoming temporarily invalid.

FComponentReference is fairly old and not widely used, so it’s possible this special case was never fixed/encountered.

If the actor is indeed never destroyed then there’s also a possibility of there being further references that prevent it’s destruction.

Does the “obj refs name=XXX” command print any existing references to the actor after the level has been unloaded?

(You can list existing actors via “obj list class=Actor” or use a partial name).

We have changed the engine behavior from the old PendingKill (where actor references where forcefully set to nullptr) and instead introduced the “Garbage” state, where IsValid() on such an actor will return false, but any hard references will keep that actor alive.

So if any system outside of the level is holding a strong reference to such an actor it would be kept in memory.

Kind Regards,

Sebastian