Exploring memory leaks when mixing weak and strong subscriptions in announcers

This page explores issues related to memory leaks that can appear when mixing weak and strong subscriptions within an announcer.

An Announcer Object subclass: #Announcer instanceVariableNames: 'registry' classVariableNames: '' package: 'Announcements-Core-Base' has a a registry (SubscriptionRegistry Object subclass: #SubscriptionRegistry instanceVariableNames: 'subscriptions monitor' classVariableNames: '' package: 'Announcements-Core-Subscription' or FastSubscriptionRegistry Object subclass: #FastSubscriptionRegistry instanceVariableNames: 'monitor subscriberMap' classVariableNames: '' package: 'GToolkit-PharoBasePatch-Subscriptions-Core' ) that holds a list of subscriptions.

Within a subscription registry there can be both strong (AnnouncementSubscription Object subclass: #AnnouncementSubscription instanceVariableNames: 'announcer announcementClass subscriber action' classVariableNames: '' package: 'Announcements-Core-Subscription' ) and weak (WeakAnnouncementSubscription Object weakSubclass: #WeakAnnouncementSubscription instanceVariableNames: 'next announcer announcementClass action' classVariableNames: '' package: 'Announcements-Core-Subscription' ) subscriptions.

Strong subscriptions directly hold on to the receiver. When Announcer>>#when:send:to: when: anAnnouncementClass send: aSelector to: anObject "Declare that when anAnnouncementClass is raised, anObject should receive the message aSelector. When the message expects one argument (eg #fooAnnouncement:) the announcement is passed as argument. When the message expects two arguments (eg #fooAnnouncement:announcer:) both the announcement and the announcer are passed as argument" ^ self when: anAnnouncementClass do: (MessageSend receiver: anObject selector: aSelector) is used to create a strong subscription that sends a message to an object, a MessageSend Object subclass: #MessageSend instanceVariableNames: 'receiver selector arguments' classVariableNames: '' package: 'Kernel-Messaging' is created that captures the message send and AnnouncementSubscription>>#subscriber subscriber ^ subscriber also holds on to the receiver of the message.

Weak subscriptions are created by WeakSubscriptionBuilder Object subclass: #WeakSubscriptionBuilder instanceVariableNames: 'announcer' classVariableNames: '' package: 'Announcements-Core-Subscription' . In this case WeakSubscriptionBuilder>>#when:do:for: creates a WeakAnnouncementSubscription Object weakSubclass: #WeakAnnouncementSubscription instanceVariableNames: 'next announcer announcementClass action' classVariableNames: '' package: 'Announcements-Core-Subscription' . This is a weak class that holds on weakly to the subscriber (WeakAnnouncementSubscription>>#subscriber: subscriber: anObject self subscriber ifNotNil: [ self error: 'subscriber already set' ]. self basicAt: 1 put: anObject. self register ). The subscription then registers itself with a FinalizationRegistry Object subclass: #FinalizationRegistry instanceVariableNames: 'semaphore errorHandler ephemeronList' classVariableNames: 'Default' package: 'System-Finalization-Registry' in WeakAnnouncementSubscription>>#register register self finalizationRegistry add: self subscriber finalizer: self using an ephemeron.

When registering with the finalization registry, the weak subscription adds the subscriber object as the key of the ephemeron, and the actual weak subscription is added as the finalizer. So the weak subscription holds on weakly to the subscriber and if creates an ephemeron that also holds on to the subscriber weakly. There is no strong reference to the subscriber from the subscription.

The ephemeron will call WeakAnnouncementSubscription>>#finalize finalize announcer removeSubscription: self when the subscriber object can be gargabe collected. When that is called the subscription removes itself from the subscription registry in the announcer (Announcer>>#removeSubscription: removeSubscription: subscription "Remove the given subscription from the receiver" ^ registry remove: subscription ). At this point the subscription can be garbage collected as only the ephemeron holds on to it, and the ephemeron also removes itself from the finalization registry when FinalizationRegistryEntry>>#mourn mourn "The key is only referenced by myself. This Ephemeron instance is not ephemeric anymore: it cannot be reused. Ask the container to finalize myself" container finalizeEphemeron: self is executed.

WeakSubscriptionBuilder>>#when:send:to: when: anAnnouncementClass send: aSelector to: anObject ^ self when: anAnnouncementClass do: (WeakMessageSend receiver: anObject selector: aSelector) creates a weak subscription that sends a message to a receiver using a WeakMessageSend Object weakSubclass: #WeakMessageSend instanceVariableNames: 'selector shouldBeNil arguments' classVariableNames: '' package: 'Kernel-Messaging' . The subscription holds on strongly to the message send, but WeakMessageSend Object weakSubclass: #WeakMessageSend instanceVariableNames: 'selector shouldBeNil arguments' classVariableNames: '' package: 'Kernel-Messaging' is also a weak class that holds on weakly to the receiver. So overall using a weak subscription does not create a strong reference to the receiver object.

A case that with ephemerons no longer creates a memory leak is that of having within the same announcer both a strong and weak subscription to the same receiver object.

In this case we create the subscribedTarget object that is an association with an UUID ByteArray variableByteSubclass: #UUID instanceVariableNames: '' classVariableNames: '' package: 'Network-UUID-Base' as a key, and within the same announcer create both a strong and a weak subscription sending a message to that object.

In this case if we remove both the announcer and the subscriber object after two garbage collects the subscriber object is no longer present in memory.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleWeakAndStrongSubscriptionSameObject_targetAndAnnouncerAsSeparateObjects exampleWeakAndStrongSubscriptionSameObject_targetAndAnnouncerAsSeparateObjects <gtExample> | targetId subscribedTarget announcer | targetId := UUID new. subscribedTarget := Association new. subscribedTarget key: targetId. subscribedTarget value: 'target'. announcer := Announcer new. announcer when: Announcement send: #yourself to: subscribedTarget. announcer weak when: Announcement send: #yourself to: subscribedTarget. announcer := nil. subscribedTarget := nil. 2 timesRepeat: [ Smalltalk garbageCollect ]. self assert: (Association allInstances allSatisfy: [ :each | each key ~= targetId ])

This is a variant of the previous example where the announcer object is stored within the subscriber object.

In this case after removing the reference to the subscriber object, the subscriber is garbaged collected successfully after two garbage collects.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleWeakAndStrongSubscriptionSameObject_targetHoldsAnnouncer exampleWeakAndStrongSubscriptionSameObject_targetHoldsAnnouncer <gtExample> | targetId subscribedTarget | targetId := UUID new. subscribedTarget := Association new. subscribedTarget key: targetId. subscribedTarget value: Announcer new. subscribedTarget value when: Announcement send: #yourself to: subscribedTarget. subscribedTarget value weak when: Announcement send: #yourself to: subscribedTarget. subscribedTarget := nil. 2 timesRepeat: [ Smalltalk garbageCollect ]. self assert: (Association allInstances allSatisfy: [ :each | each key ~= targetId ])

In the examples above we do two garbage collects. The first garbage collect is needed to trigger the finalization mechanism for the receiver object. Then the second garbage collect will remove the instance.

The example below registers a finalizer for the subscriber object that adds a log entry to the finalizationLog collection.

In this case after removing the reference to the subscriber object the first garbage collect triggers the finalization, and the second removes the object from memory.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleWeakAndStrongSubscriptionSameObject_logFinalization exampleWeakAndStrongSubscriptionSameObject_logFinalization <gtExample> | finalizationLog targetId subscribedTarget announcer | finalizationLog := OrderedCollection new. targetId := UUID new. subscribedTarget := Association new. subscribedTarget key: targetId. subscribedTarget value: 'target'. announcer := Announcer new. announcer when: Announcement send: #yourself to: subscribedTarget. announcer weak when: Announcement send: #yourself to: subscribedTarget. FinalizationRegistry default add: subscribedTarget finalizer: (ObjectFinalizer new receiver: finalizationLog; selector: #add:; arguments: {targetId printString}). announcer := nil. subscribedTarget := nil. "First garbage collect triggers finalization" Smalltalk garbageCollect. self assert: finalizationLog size equals: 1. self assert: finalizationLog first equals: targetId printString. "Secong garbage collect, removes the reference to the receiver" Smalltalk garbageCollect. self assert: (Association allInstances allSatisfy: [ :each | each key ~= targetId ]). ^ finalizationLog

A different case is when within an announcer we have a strong and a weak subscription for two different objects.

We will look at three cases based on how we remove references to the two objects:

- references to both the strong and weak subscribers are removed: both objects are garbaged collected

- only the reference to the strong subscriber is removed: no object is garbaged collected

- only the reference to the weak subscriber is removed: the weakly subscribed object is garbaged collected

In both case we create two subscribers; one is registered strongly the other weakly.

We remove the references to both subscribers.

After doing two garbage collects both subscribers are removed.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleWeakAndStrongSubscriptionDifferentObjects_clearBothSubscribers exampleWeakAndStrongSubscriptionDifferentObjects_clearBothSubscribers <gtExample> | stronglyTargetId weakTargetId stronglySubscribedTarget weaklySubscribedTarget announcer | stronglyTargetId := UUID new. weakTargetId := UUID new. stronglySubscribedTarget := Association new. stronglySubscribedTarget key: stronglyTargetId. stronglySubscribedTarget value: 'strong target'. weaklySubscribedTarget := Association new. weaklySubscribedTarget key: weakTargetId. weaklySubscribedTarget value: 'weak target'. announcer := Announcer new. announcer when: Announcement send: #yourself to: stronglySubscribedTarget. announcer weak when: Announcement send: #yourself to: weaklySubscribedTarget. announcer := nil. stronglySubscribedTarget := nil. weaklySubscribedTarget := nil. 2 timesRepeat: [ Smalltalk garbageCollect ]. self assert: (Association allInstances allSatisfy: [ :each | each key ~= stronglyTargetId ]). self assert: (Association allInstances allSatisfy: [ :each | each key ~= weakTargetId ]).

Now we just remove the reference to the strong subscriber.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleWeakAndStrongSubscriptionDifferentObjects_clearStrongSubscriber exampleWeakAndStrongSubscriptionDifferentObjects_clearStrongSubscriber <gtExample> | stronglyTargetId weakTargetId stronglySubscribedTarget weaklySubscribedTarget announcer | stronglyTargetId := UUID new. weakTargetId := UUID new. stronglySubscribedTarget := Association new. stronglySubscribedTarget key: stronglyTargetId. stronglySubscribedTarget value: 'strong target'. weaklySubscribedTarget := Association new. weaklySubscribedTarget key: weakTargetId. weaklySubscribedTarget value: 'weak target'. announcer := Announcer new. announcer when: Announcement send: #yourself to: stronglySubscribedTarget. announcer weak when: Announcement send: #yourself to: weaklySubscribedTarget. announcer := nil. stronglySubscribedTarget := nil. "weaklySubscribedTarget := nil." 2 timesRepeat: [ Smalltalk garbageCollect ]. self assert: (Association allInstances allSatisfy: [ :each | each key ~= stronglyTargetId ]) not. "The reference is not removed" self assert: (Association allInstances allSatisfy: [ :each | each key ~= weakTargetId ]) not. "The reference is not removed"

After we do two garbage collects none of the two references is removed. The reference to the weakly subscribed object cannot be removed as we still explicitly reference it.

For the strongly referenced object, there is still a path holding on to it:

the finalization registry holds on strongly to the weak subscription

the weak subscription holds on strongly to the announcer

the announcer has a strong subscription holding on to the strongly subscribed object

Based on this chain, the announcer object and its subscriptions cannot be garbaged collected. So when we mix strong and weak subscriptions, even if the strongly referenced object is no longer used, as the weakly subscribed object is used, that weak subscription preserves a strong reference to the announcer, that holds on strongly to the strongly referened object.

When we remove only the reference to the weakly subscribed object that object can be successfully removed.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleWeakAndStrongSubscriptionDifferentObjects_clearWeakSubscriber exampleWeakAndStrongSubscriptionDifferentObjects_clearWeakSubscriber <gtExample> | stronglyTargetId weakTargetId stronglySubscribedTarget weaklySubscribedTarget announcer | stronglyTargetId := UUID new. weakTargetId := UUID new. stronglySubscribedTarget := Association new. stronglySubscribedTarget key: stronglyTargetId. stronglySubscribedTarget value: 'strong target'. weaklySubscribedTarget := Association new. weaklySubscribedTarget key: weakTargetId. weaklySubscribedTarget value: 'weak target'. announcer := Announcer new. announcer when: Announcement send: #yourself to: stronglySubscribedTarget. announcer weak when: Announcement send: #yourself to: weaklySubscribedTarget. announcer := nil. "stronglySubscribedTarget := nil." weaklySubscribedTarget := nil. 2 timesRepeat: [ Smalltalk garbageCollect ]. self assert: (Association allInstances allSatisfy: [ :each | each key ~= stronglyTargetId ]) not. "The reference is not removed" self assert: (Association allInstances allSatisfy: [ :each | each key ~= weakTargetId ])

The case of a weak and strong subscription for the same subscriber within the same announcer does not create a memory leak. If not referenced the subscriber and annoucer can be garbaged collected.

What becomes problematic is adding another weak subscription to that announcer for a second subscriber. Now just the fact that we added a weak subscription creates a strong reference from the finalization registry to the announcer and to the initial subscriber.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleThirdWeakSubscription exampleThirdWeakSubscription <gtExample> | targetId subscribedTarget extraTargetId extraSubscribedTagget | targetId := UUID new. subscribedTarget := Association new. subscribedTarget key: targetId. subscribedTarget value: Announcer new. subscribedTarget value when: Announcement send: #yourself to: subscribedTarget. subscribedTarget value weak when: Announcement send: #yourself to: subscribedTarget. "We add another weak subscription to the same announcer for another object" extraTargetId := UUID new. extraSubscribedTagget := Association new. extraSubscribedTagget key: extraTargetId. extraSubscribedTagget value: 'extra subscriber'. subscribedTarget value weak when: Announcement send: #yourself to: extraSubscribedTagget. subscribedTarget := nil. 2 timesRepeat: [ Smalltalk garbageCollect ]. self assert: (Association allInstances allSatisfy: [ :each | each key ~= targetId ]) not "The reference is not removed"

This example uses the same code as before, just has different names for variables to model an explicit case that of a graphical element and its model using announcements.

In this case the graphical element holds on to the model strongly and also has a strong subscription.

When we remove the model and the graphical element, they will not be garbaged collected as the extra weak subscription holds on to them until the extra object is garbaged collected. The weak subscription holds on to the announcer, which holds on to the strong subscription.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleDomainModelAndElement_strongSubscription exampleDomainModelAndElement_strongSubscription <gtExample> | modelId model graphicalElementId graphicalElement extraSubscriberId extraSubscriber | modelId := UUID new. model := Association new. model key: modelId. model value: Announcer new. graphicalElementId := UUID new. graphicalElement := Association new. graphicalElement key: graphicalElementId. "The graphical element can hold on to the model" graphicalElement value: model. "We create a subscription from the model to the graphical element." model value when: Announcement send: #yourself to: graphicalElement. "We add another weak subscription to the model, maybe for another graphical element" extraSubscriberId := UUID new. extraSubscriber := Association new. extraSubscriber key: extraSubscriberId. extraSubscriber value: 'extra subscriber'. model value weak when: Announcement send: #yourself to: extraSubscriber. model := nil. graphicalElement := nil. 2 timesRepeat: [ Smalltalk garbageCollect ]. "The graphical element is not removed as through the new weak subscription we create a strong reference to it" self assert: (Association allInstances allSatisfy: [ :each | each key ~= graphicalElementId ]) not. "The model is not removed as the graphical element holds on to it" self assert: (Association allInstances allSatisfy: [ :each | each key ~= modelId ]) not.

In case we have a weak subscription between the model and element, the extra weak subscription will not prevent the model and element to be garbaged collected.

The extra weak subscription holds on to the announcer strongly, but the announcer does not hold on strongly to the model and element.

GtAnnouncementsMemoryLeaksPageExamples>>#exampleDomainModelAndElement_weakSubscription exampleDomainModelAndElement_weakSubscription <gtExample> | modelId model graphicalElementId graphicalElement extraSubscriberId extraSubscriber | modelId := UUID new. model := Association new. model key: modelId. model value: Announcer new. graphicalElementId := UUID new. graphicalElement := Association new. graphicalElement key: graphicalElementId. "The graphical element can hold on to the model" graphicalElement value: model. "We create a subscription from the model to the graphical element." model value weak when: Announcement send: #yourself to: graphicalElement. "We add another weak subscription to the model, maybe for another graphical element" extraSubscriberId := UUID new. extraSubscriber := Association new. extraSubscriber key: extraSubscriberId. extraSubscriber value: 'extra subscriber'. model value weak when: Announcement send: #yourself to: extraSubscriber. model := nil. graphicalElement := nil. 2 timesRepeat: [ Smalltalk garbageCollect ]. self assert: (Association allInstances allSatisfy: [ :each | each key ~= graphicalElementId ]) . self assert: (Association allInstances allSatisfy: [ :each | each key ~= modelId ]) .