Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift deinit is being called but object still not deallocating

In swift I am getting the deinit function to print out a line saying that the object has been de initialized, but the object is still being reported as live in Instruments, allocations tool. I didn't think this was even possible. Is there a way to find out why it's not being freed? Or is there a way to find out what child objects could be holding it up?

like image 737
NJGUY Avatar asked Mar 12 '15 20:03

NJGUY


1 Answers

Update: For Swift 4, see the additional note at the end.

Warning: This answer goes into some detail about the way the Swift runtime is implemented. The information here does not affect how you write code in Swift, except in some advanced scenarios. The main point is that from your point of view as a programmer, once deinit is called, the object is dead to you and you cannot use it any more.

The reason the memory is not freed is that objects in Swift are not necessarily deallocated (freed) straight away when they are deinited. Weak references to an object will cause the 'husk' of the object to stay allocated (in memory - you still can't use it!) until all the weak references have been zeroed.

Weak references in Swift are not zeroed immediately when the object is deinited, but they will be zero the next time they are accessed. That is, Swift zeroes weak references lazily. Here's an example:

public class MyClass {
}
var object = MyClass()
weak var weakObject = object
print (weakObject) // Points at a MyClass instance
object = MyClass()
// A: weakObject is not yet nil
print(weakObject) // prints 'nil'
// B: now weakObject is nil

After assigning object to a new instance (line 6), you would think that the weak reference to the original object would be zero, but it's not (yet). The object is deinited but stays allocated (in memory) until all the weak references are gone. At point A, a weak reference still exists, it's only on the next line when you try to evaluate the weak reference that Swift checks and notices that the object it references is deinited, so it zeroes the weak reference and then passes it to the print function to be printed. This mechanism needs the object's empty husk to remain allocated until all the weak references are gone. It is called a husk because all of its properties have been zeroed and released in deinit so it doesn't keep anything else alive (the amount of memory for an object is quite small, just enough to store its internal headers and members).

Why and how?

Each object has an internal weak reference count instead of a list of references that need to be zeroed. This is much quicker and less resource intensive to deinit because zeroing a list of weak references in a thread-safe way requires quite a long atomic/synchronized operation.

When the strong reference count reaches zero, deinit is called and the object enters the deallocating state. The run-time keeps the memory allocated because it needs to check the state of the object whenever a weak reference is accessed. Once all weak references have been accessed and zeroed (the weak reference count is zero), the memory will be freed and deallocation is complete.

Take a look at the implementation of swift_weakLoadStrong from the swift source code - this is the code that is inserted when a weak reference is accessed and made strong (for example to be assigned to a strong reference or passed into a function etc.). I have abbreviated it below. Look at the original code on github to see the full complexity of loading a weak reference:

if (object == nullptr) {
    __atomic_store_n(&ref->Value, (uintptr_t)nullptr, __ATOMIC_RELAXED);
    return nullptr;
}
if (object->refCount.isDeallocating()) {
    __atomic_store_n(&ref->Value, (uintptr_t)nullptr, __ATOMIC_RELAXED);
    SWIFT_RT_ENTRY_CALL(swift_unownedRelease)(object);
    return nullptr;
}
auto result = swift_tryRetain(object);
__atomic_store_n(&ref->Value, ptr, __ATOMIC_RELAXED);
return result;

You can see that the object husk still exists in memory, and the mechanism that loads the weak reference (i.e. when you access it in your code) checks if it is deallocating, and if so it zeroes the weak reference, calls swift_unownedRelease to decrement the weak reference count and free the object if the count has reached zero, and returns nullptr (nil).

Swift 4 Update

Since Swift 4, weak references have an improved implementation that means the object husk no longer needs to hang around. Instead a small side-table is used and weak references actually point to that instead. The side-table contains a pointer to the real object and Swift knows to follow this pointer when accessing weak references. For a much more detailed explanation, read this great blog post from Mike Ash.

like image 132
jhabbott Avatar answered Sep 19 '22 09:09

jhabbott