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 ]) .