3

In my application, I have some objects which represent local currency, and other objects which represent currency exchange rates.

My question is, if my local currency objects subscribe to a single subject on the currency object to be alerted to rate changes (but the money objects don't actually save the subscription) and then the single currency instance defines the Subject of all those subscriptions is set to null, do all those 'subscriptions' disappear if I don't call unsubscribe on each of the 50,000 money objects?

For a concrete (simplified) example, this:

import { Subject } from 'rxjs'
interface MyChangeEvent {
  oldValue : number;
  newValue : number;
}
export class Currency {
  rateSubject : Subject<MyChangeEvent>;
  private _rate : number;
  private _name : string;
  constructor(name : string, rate : number) {
    this.rateSubject = new Subject();
    this._rate= rate;
    this._name = name;
  }
  get rate() : number {
    return this._rate;
  }
  set rate(v : number) {
    let oldrate = this.rate;
    this._rate = v;
    let ce : MyChangeEvent
    ce = {} as MyChangeEvent;
    ce.newValue = v;
    ce.oldValue = oldrate;
    this.rateSubject.next(ce);
  }
}


export class Money {
  private _rate : number = 1;
  private _localCurr : number = 0;
 
  get dollarValue() {
    return this._localCurr * this._rate;
  }

  constructor(localCurr : number, curr : Currency) {
    this._localCurr = localCurr;
    this._rate = curr.rate;
    curr.rateSubject.subscribe((change)=>{
        this._rate = change.newValue;
    })
  }
}

const test = function() {
    let c = new Currency("USD", 1);
    let m = new Money(500, c);
    c.rate = .5;
    c=null;
}

So my question is, let's say I have 50,000 money objects in my application, and then I set c=null as in the last line here. Do the 50,000 listeners I've set up for all those money objects persist somewhere in memory? Are they all garbage collected when the Currency object goes out of scope?

GGizmos
  • 3,443
  • 4
  • 27
  • 72
  • I'd guess the JS object from `this.rateSubject = new Subject();` is still retained because it contains references to callbacks you pass into `subscribe` that are in the scope of all money objects that are still referenced somewhere – Max Koretskyi Aug 06 '20 at 09:15
  • I'm not sure I follow. Are you saying that the money objects retain a reference to the subscription even though the money object doesn't itself save the subscription? If not, I'm not following why the rateSubject in Currency wouldnt be GCd. That it maintains references to callbacks wouldn't seem to be sufficient, so long as no live object has references to it. The consensus based on the votes for Andre's answer seems to disagree. However, you've got 77K rep, so - how confident are you? – GGizmos Aug 08 '20 at 21:13

1 Answers1

11

EDIT

You could also check out RxJS: Why memory leaks occur when using a Subject.


I'd say that there will be no memory leaks.

This is based on my understanding as to why memory leaks actually occur. Usually this sort of problems take place when the source is infinite(e.g will not complete/error, like a global service that is used by components).

For example, in Angular, a common pattern is to inject a app-scoped service into components and subscribe to one of the observable properties exposed by the service.

class Service {
  private usersSrc = new Subject();
  users$ = this.usersSrc.asObservable();
}

Then you'd do this in your component:

class FooComponent {
  ngOnInit () {
    this.subscription = this.service.users$.subscribe(() => {} /* callback */)
  }
}

Note: this is just for demonstration purposes, as you'd want to use other approaches so that you won't have to manually subscribe, e.g async pipe

When users$ is subscribed, because users$ comes from usersSrc, the newly created subscriber will be added to the Subject's list of subscribers. And that subscriber's next callback will be the () => {} callback.

Now when the component is destroyed(e.g due to navigating to another route), if you don't do something like this.subscription.unsubscribe(), that subscriber will still be part of that subscribers list. The unsubscribe method would remove that subscriber from that list.

So, the next time the component is created and that ngOnInit is created, a new subscriber will be added, but the old one would still be there if you didn't use this.subscription.unsubscribe().


I'd say that setting that source to null would be enough.

If the source happens to be a Subject, you could also use Subject.unsubscribe, although it may not make any difference.

unsubscribe() {
  this.isStopped = true;
  this.closed = true;
  this.observers = null!;
}

Here would be a simplified version. You could paste this in your console.

src = {
 subscribers: [],
 addSubscriber(cb) {
  this.subscribers.push(cb);
  return this.subscribers.length - 1
 },
 removeSubscriber(idx) {
  this.subscribers.splice(idx, 1)
 },
 next (data) {
  this.subscribers.forEach(cb => cb(data));
 }
}

// the component
class Foo {
 
constructor () {
   this.subIdx = src.addSubscriber(() => { console.log('foo') })
 }

 onDestroy () {
  src.removeSubscriber(this.subIdx);
 }
}

// usage

// creating a new component
foo = new Foo() // Foo {subIdx: 0}

// sending data to subscribers
src.next('test')

// destroying the component - without calling `onDestroy`
foo = null


src.next('test') // the subscribers is still there
VM506:18 foo

foo = new Foo() // registering a new instance - Foo {subIdx: 1}

src.next('test2')
foo
foo

foo.onDestroy()
src.next('test2')
foo
Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31
  • The question has nothing to do with Angular – Adrian Brand Aug 05 '20 at 07:23
  • And that’s a reason to downvote? I just gave an example and that should illustrate the problem. Could you elaborate on what’s being wrong with this answer? Also the last example has nothing to do with angular, so you might want to have a closer look. – Andrei Gătej Aug 05 '20 at 08:57
  • your answer seems to conflict with Adrian's. Adrian suggests that the subscription instance is kept alive (why?) irrespective that the object which was holding the subject has gone out of scope. Meaning in my example, I'd have 50,000 subscriptions referring to a non-existing subject taking up memory in my app? True or false? – GGizmos Aug 05 '20 at 15:21
  • I’d say that they will be garbage collected since they are no longer referenced - the one which held their callbacks, the Subject, is mulled out – Andrei Gătej Aug 05 '20 at 21:38
  • It is all still in memory until it is garbage collected. Since there is no way for next to be called on any of your subjects the subscriptions wont do anything. My example is to show that subscriptions are still active even when the objects are marked for garbage collection. They are taking up memory but that memory will be free up if garbage collection is triggered. Unsubscribing will make no difference to how much memory is used. – Adrian Brand Aug 05 '20 at 22:35
  • 1
    That said, I would still always unsubscribe form any subscriptions I am finished with. The standard pattern is to use a void subject named finalised and put takeUntil(finalised) on all your observables. Then when you are done call next and complete on finalised. This completes all your observables in a single call. – Adrian Brand Aug 05 '20 at 22:41