Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observer reference is not released on unsubscribe #1292

Closed
fdoyle opened this issue May 30, 2014 · 74 comments
Closed

Observer reference is not released on unsubscribe #1292

fdoyle opened this issue May 30, 2014 · 74 comments

Comments

@fdoyle
Copy link

fdoyle commented May 30, 2014

With the following code
http://pastebin.com/VUZ5aApe
and using the latest version of rxjava (0.18.4),

rotations should result in the activity calling unsubscribe on my subscription.

This should release the reference to my observer, which should in turn release the reference to my activity.

However, the memory tests I've done show that as I rotate the phone, my activity count keeps going up, which means the reference to my activity is not being released.

(let me know if the pastebin expires for some reason, I'll get something back up)

@benjchristensen
Copy link
Member

Can someone who has worked with Android and the AndroidScheduler take a look at this please?

@zsxwing
Copy link
Member

zsxwing commented May 31, 2014

This should release the reference to my observer, which should in turn release the reference to my activity.

Thread.sleep(10000); will block the Observable so that unsubscribe can not release the reference to the observer at once. Did the number of activity decrease after a few seconds?

@fdoyle
Copy link
Author

fdoyle commented Jun 1, 2014

I changed it to Thread.sleep(500) and even after a few minutes, the activity count did not go down.

@zsxwing
Copy link
Member

zsxwing commented Jun 2, 2014

@fdoyle, just another question, How do you monitor the activity count? Even if an activity has no reference, it may still in memory if GC does not free its memory. Can you force a GC and then check it again?

@fdoyle
Copy link
Author

fdoyle commented Jun 2, 2014

I'm monitoring the activity count with
adb shell dumpsys meminfo com.lacronicus.rxjavaleaktest.app

And after forcing a GC on the process with android debug monitor, the activity count remains high (10+ after as many rotations)

@fdoyle
Copy link
Author

fdoyle commented Jun 23, 2014

Has anyone else been able to confirm that this is an issue? I'm worried about using rxjava in any of my projects until this is resolved.

@benjchristensen
Copy link
Member

Anyone that works on Android able to comment on this?

@devisnik
Copy link
Contributor

Seems your long-running task never finishes?

@zsxwing
Copy link
Member

zsxwing commented Jul 11, 2014

@fdoyle, sorry that I forgot this one.

In your code, the anonymous Subscriber will hold the Activity reference. Because your long-running task never finishes, the Subscriber will always be used and never be released. Finally, neither the Subscriber or the Activity object can be recycled.

@evant
Copy link

evant commented Jul 11, 2014

I feel that calling unsubscribe() not clearing the reference to the Subscription until the task finishes is non-obvious and could bite a lot of people working on Android. Is there a reason for this design decision? Is there a way to rewrite the example so that the Activity is not leaked while the task is running?

@dpsm
Copy link
Contributor

dpsm commented Jul 11, 2014

@fdoyle @evant AndroidObservable.fromActivity(..) should avoid leaking your Activity.

@devisnik
Copy link
Contributor

@evant Just don't bind your activity to the subscription, which is done through the inner class in the example.

@evant
Copy link

evant commented Jul 11, 2014

@dpsm Do you mean AndroidObservable.bindActivity()? fromActivity() is deprecated.

@devisnik
Copy link
Contributor

@dpsm From the javadoc of AndroidObservable.fromActivity():
"Currently, this is equivalent to calling observeOn(AndroidSchedulers.mainThread()), but this behavior may change in the future, so it is encouraged to use this wrapper instead."

So I guess not.

@dpsm
Copy link
Contributor

dpsm commented Jul 11, 2014

@evant @devisnik I meant AndroidObservable.bindActivity(..)

"This helper will schedule the given sequence to be observed on the main UI thread and ensure
* that no notifications will be forwarded to the activity in case it is scheduled to finish."

@evant
Copy link

evant commented Jul 11, 2014

Is there any reason the reference to the Subscription is kept at all though? This would be a problem for non-Android uses too.

@devisnik
Copy link
Contributor

@dpsm 👍 Yes, that one voids the reference on the next notification, won't help in the example here though.

@fdoyle
Copy link
Author

fdoyle commented Jul 11, 2014

So there is no way for an activity, or inner class of an activity, to safely subscribe to an observable?

edit: If that is the case, it needs to be made a lot more clear. Practically every android rxjava tutorial uses this pattern, some even explicitly saying that unsubscribing prevents memory leaks such as this one.

@dpsm
Copy link
Contributor

dpsm commented Jul 11, 2014

@fdoyle I guess the issue here is that SubscriptionList and other Subscription implementations of unsubscribe() do not clear the references to their child subscriptions after invoking unsubscribe() on them.

@JakeWharton
Copy link
Contributor

@dpsm Yeah. And I'd argue that this is a larger bug not scoped to just Android because of that.

@benjchristensen
Copy link
Member

When unsubscribe is called on a Subscription, it is done and nothing should be holding on to it any longer and thus it and everything it contains can be garbage collected.

So what is holding the reference to the outer Subscription or Subscriber after it is unsubscribed?

@dpsm
Copy link
Contributor

dpsm commented Jul 11, 2014

@benjchristensen in the example above OperatorMap while invoking Func1.call(..) from another thread context bound to a GC root holds a strong reference to the downstream chain of subscribers which ends at the Activity which is in turn subscribed to the observable.

@JakeWharton the interesting thing is that CompositeSubscription has a clear() method. Apparently not used at all. Not sure why wouldn't a subscription clear it's children when unsubscribed?

@fdoyle I guess you'd have to do something like this until we have a better solution:

@OverRide
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
s = Observable.from(1).observeOn(Schedulers.io()).map(new myMapFunction()).observeOn(AndroidSchedulers.mainThread())
.lift(new WeakSubscriptionOperator())
.subscribe(
new Action1() {
@OverRide
public void call(Integer integer) {
Toast.makeText(MainActivity.this, "lol", Toast.LENGTH_SHORT).show();
}
}
);
}

private static class WeakSubscriptionOperator<T> implements Observable.Operator<T, T> {
    @Override
    public Subscriber<? super T> call(final Subscriber<? super T> child) {
        return new WeakSubscriptionSubscriber<T>(child);
    }
}

private static class WeakSubscriptionSubscriber<T> extends Subscriber<T> {

    private final WeakReference<Subscriber<? super T>> mWeakReference;

    private WeakSubscriptionSubscriber(final Subscriber<? super T> child) {
        mWeakReference = new WeakReference<Subscriber<? super T>>(child);
        child.add(this);
    }

    @Override
    public void onCompleted() {
        final Subscriber<? super T> subscriber = mWeakReference.get();
        if (!isUnsubscribed() && subscriber != null) {
            subscriber.onCompleted();
        }
    }

    @Override
    public void onError(Throwable throwable) {
        final Subscriber<? super T> subscriber = mWeakReference.get();
        if (!isUnsubscribed() && subscriber != null) {
            subscriber.onError(throwable);
        }
    }

    @Override
    public void onNext(T t) {
        final Subscriber<? super T> subscriber = mWeakReference.get();
        if (!isUnsubscribed() && subscriber != null) {
            subscriber.onNext(t);
        }
    }
}

@evant
Copy link

evant commented Jul 11, 2014

You will have to hold an explicit reference to Action1 in your Activity right? That way it will only get collected when the activity is.

@fdoyle
Copy link
Author

fdoyle commented Jul 11, 2014

@evant yeah, I was thinking that as well. It's a solution, but a cumbersome one at best. You'd have to keep track of every onNext, onError and onComplete you use.

@dpsm
Copy link
Contributor

dpsm commented Jul 11, 2014

@benjchristensen is there any reason why we would not want to clear the children subscription references after unsubscribing them?

@benjchristensen
Copy link
Member

Not sure why wouldn't a subscription clear it's children when unsubscribed?

Because the assumption is that the Subscription is no longer being held and will be freed for garbage collection.

Calling Subscription.unsubscribe signals a terminal state for that Subscription -> https://github.com/Netflix/RxJava/blob/master/rxjava-core/src/main/java/rx/subscriptions/CompositeSubscription.java#L117

Thus, once it is called, there is no reason for that Subscription to be held and it will be freed for garbage collection, along with all of its children.

is there any reason why we would not want to clear the children subscription references after unsubscribing them?

No specific reason, just that it shouldn't be needed since the Subscription/Subscriber itself should also be freed for garbage collection after it is unsubscribed.

I still don't understand what is holding a reference to the Subscription/Subscriber after it is unsubscribed.

@zsxwing
Copy link
Member

zsxwing commented Jul 12, 2014

@benjchristensen, the problem in this example is that the map function never returns. Therefore the subscriber used in the map operator won't be recycled.

@evant
Copy link

evant commented Jul 12, 2014

I created a sample project that simply creates an Observable that runs forever and subscribes to it. I then ran it, rotated my phone once, and took a heap dump. Opening it MAT shows 2 instances of MyActivity, and when I do a "Merge Shortest Path to GC Roots" with one of them, I get

Class Name                                                                              | Ref. Objects | Shallow Heap | Ref. Shallow Heap | Retained Heap
----------------------------------------------------------------------------------------------------------------------------------------------------------
java.lang.Thread @ 0x428861f0  RxCachedThreadScheduler-1 Thread                         |            1 |           80 |               200 |         4,472
'- <Java Local> rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber @ 0x42882370|            1 |           40 |               200 |            56
   '- observer rx.observers.SafeSubscriber @ 0x42881980                                 |            1 |           24 |               200 |           288
      '- actual rx.Observable$26 @ 0x42881118                                           |            1 |           24 |               200 |           152
         '- val$onNext me.tatarka.rxjavaleak.MyActivity$2 @ 0x42880960                  |            1 |           16 |               200 |            16
            '- this$0 me.tatarka.rxjavaleak.MyActivity @ 0x4285a420                     |            1 |          200 |               200 |         1,368
----------------------------------------------------------------------------------------------------------------------------------------------------------

sample project: https://github.com/evant/RxJavaLeak
main activity: https://github.com/evant/RxJavaLeak/blob/master/app/src/main/java/me/tatarka/rxjavaleak/MyActivity.java
hprof: https://github.com/evant/RxJavaLeak/raw/master/rxleak.hprof

@benjchristensen
Copy link
Member

the subscriber used in the map operator won't be recycled.

It shouldn't be if map never finished. If it never finishes, it isn't unsubscribed. Why would it ever be expected to if it is an infinite Observable?

I then ran it, rotated my phone once, and took a heap dump

I'm sorry, but I don't do Android development so I the implication of rotation doesn't mean much to me.

Is the issue that the Activity itself is maintained forever, and it has a reference to the Subscription? => https://github.com/evant/RxJavaLeak/blob/master/app/src/main/java/me/tatarka/rxjavaleak/MyActivity.java#L31

If so, just null out the reference here: https://github.com/evant/RxJavaLeak/blob/master/app/src/main/java/me/tatarka/rxjavaleak/MyActivity.java#L51 or use something like takeUntil to trigger the unsubscribe when onDestroy happens instead of storing the reference as an instance variable.

@evant
Copy link

evant commented Jul 12, 2014

Rotating the device means that the Activity is destroyed and a new one is created. It does not matter that the Activity has a reference to the subscription because when the Activity is destroyed the system will no longer hold a reference to it. The problem is that the subscription is keeping a hold of the Activity because something up the chain is keeping a reference to it.

@evant
Copy link

evant commented Jul 12, 2014

Yes that works correctly which is good because it will catch many cases
where this is an issue. I still want to try nulling out the subscription
though to better handle cases where either the operation is uninterruptible
or not implemented correctly. I'm not seeing any downsides to doing it.
On Jul 12, 2014 12:20 PM, "Ben Christensen" [email protected]
wrote:

@evant https://github.com/evant and @fdoyle https://github.com/fdoyle
For the long-running Observable that may run longer than the Activity,
are those doing IO? If so, doesn't interrupting them work?

For example:

import rx.Subscriber;import rx.Subscription;import rx.schedulers.Schedulers;
public class UnsubscribeObservable {

private static rx.Observable<Void> observable = rx.Observable.create(new rx.Observable.OnSubscribe<Void>() {
    @Override
    public void call(Subscriber<? super Void> subscriber) {
        System.out.println("Observable starting [" + Thread.currentThread() + "] ...");
        while (!subscriber.isUnsubscribed()) {
            try {
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                System.err.println("Interrupted so exiting.");
                return;
            }
        }
        System.out.println("... unsubscribed so exiting.");
    }
});

public static void main(String[] args) {
    for (;;) {
        try {
            Subscription s1 = observable.observeOn(Schedulers.computation()).subscribeOn(Schedulers.io()).subscribe();
            Subscription s2 = observable.observeOn(Schedulers.computation()).subscribeOn(Schedulers.io()).subscribe();
            Thread.sleep(1000);
            s1.unsubscribe();
            Thread.sleep(1000);
            s2.unsubscribe();
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}}

This outputs the following and does not wait for 50 seconds before
unsubscribing:

Observable starting [Thread[RxCachedThreadScheduler-2,5,main]] ...
Observable starting [Thread[RxCachedThreadScheduler-1,5,main]] ...
Interrupted so exiting.
Interrupted so exiting.
Observable starting [Thread[RxCachedThreadScheduler-1,5,main]] ...
Observable starting [Thread[RxCachedThreadScheduler-2,5,main]] ...
Interrupted so exiting.
Interrupted so exiting.
Observable starting [Thread[RxCachedThreadScheduler-1,5,main]] ...
Observable starting [Thread[RxCachedThreadScheduler-2,5,main]] ...
Interrupted so exiting.
Interrupted so exiting.
Observable starting [Thread[RxCachedThreadScheduler-2,5,main]] ...
Observable starting [Thread[RxCachedThreadScheduler-1,5,main]] ...
Interrupted so exiting.
Interrupted so exiting.
Observable starting [Thread[RxCachedThreadScheduler-1,5,main]] ...
Observable starting [Thread[RxCachedThreadScheduler-2,5,main]] ...
Interrupted so exiting.
Interrupted so exiting.

IO should behave similarly as long as the IO library isn't catching and
ignoring interrupts.


Reply to this email directly or view it on GitHub
#1292 (comment).

@benjchristensen
Copy link
Member

I'm not seeing any downsides to doing it.

Nor do I. Let's make the change against both the 0.19.x branch and master. Do you want to do the pull requests?

@zsxwing
Copy link
Member

zsxwing commented Jul 13, 2014

I still want to try nulling out the subscription
though to better handle cases where either the operation is uninterruptible
or not implemented correctly.

IMHO, looks there is no way to solve it. Any instance of anonymous inner class created in an activity instance will keep an reference to the activity implicitly. E.g., if you change the map function in this example to an anonymous inner class. It does not finish, so the map function must be referred in some place, and then the activity reference in the map function cannot be released. I cannot find any way to release this reference in the map function.

Actually, I think this is not a RxJava issue. The problem is due to the anonymous inner class in Java. I can write the following code that leaks the activity without RxJava:

public class SomeActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Thread(new Runnable() {
                 // this instance will keep the reference to the outer activity
                @Override
                public void run() {
                       while(true) {
                             // Some long-running task
                       }
                }
        }).start();
    }
}

@fdoyle, In summary, if some operation may run too long, we should avoid to use any anonymous class that may keep an reference to the activity implicitly, such as @mttkay's suggestion. such as using static class like the map function in the example.

@JakeWharton
Copy link
Contributor

I still firmly believe clearing the observer/action reference upon unsubscribe is a win despite that fact.

@evant
Copy link

evant commented Jul 13, 2014

Sorry I've been away from my computer so I haven't had the chance to send a pull request yet. I just tried building rxjava locally and I'm getting Artifact 'com.jcraft:jsch:0.1.44-1@jar' not found. Since it's a simple enough change, I don't mind if someone else does it.

@benjchristensen
Copy link
Member

That artifact issue sounds like your ~/.m2 maven caches are corrupted.

@zsxwing
Copy link
Member

zsxwing commented Jul 14, 2014

I still firmly believe clearing the observer/action reference upon unsubscribe is a win despite that fact.

I doubt that if clearing the subscriber reference can help much. I suppose anonymous inner functions are very common when using RxJava. Is it true in your case?

@benjchristensen
Copy link
Member

No idea if this change will help ... as per the ongoing discussion ... but I've merged in the change and will release in 0.19.7: #1440

@zsxwing
Copy link
Member

zsxwing commented Jul 15, 2014

@benjchristensen I'm afraid that #1440 changes nothing. It does not clear the subscriber reference. Here is my commit to fix it: zsxwing@2e6e4b5
However, I don't like it because actual and op become volatile and will impact the performance since actual.onNext is called very frequently. IMO, any anonymous inner function storing the activity reference will make the effort to clear the subscriber reference useless. I still doubt if it's worth to do it.

@zsxwing
Copy link
Member

zsxwing commented Jul 15, 2014

Please notice that I have updated the link to the commit...

@dpsm
Copy link
Contributor

dpsm commented Jul 15, 2014

@zsxwing with your changes the observable sequence does not reference anything after unsubscribe so I am not sure I follow "any anonymous inner function storing the activity reference will make the effort to clear the subscriber reference useless"?

@zsxwing
Copy link
Member

zsxwing commented Jul 15, 2014

@dpsm, this is an example to show it

public class MainActivity extends Activity {
    Subscription s;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        s = Observable.from(1).observeOn(Schedulers.io()).map(new Func1<Integer, Integer>() {
            @Override
            public Integer call(Integer integer) {
                // this Func1 will store a reference to the activity.
                // Because this method does not finish, the activity reference will not be cleared.
                int x = 1;
                while (x == 1) {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return 1;
            }
        }).observeOn(AndroidSchedulers.mainThread()).subscribe(
                new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        Toast.makeText(MainActivity.this, "lol", Toast.LENGTH_SHORT).show();
                    }
                }
        );
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (s != null)
            s.unsubscribe();
    }
}

@dpsm
Copy link
Contributor

dpsm commented Jul 15, 2014

@zsxwing in the example above with your changes, the map operator having unreferenced the downstream subscriber will no longer bind the last subscriber (and the action in the activity) to a GC root (the thread that runs the map function and points to the operator).

@zsxwing
Copy link
Member

zsxwing commented Jul 15, 2014

I mean there is a reference to the outer activity in the anonymous Func1 implicitly because the Func1 is created in the activity. And Func1 will not finish, so GC cannot recycle the Func1 and the activity.

new Func1<Integer, Integer>() {
            @Override
            public Integer call(Integer integer) {
                // this Func1 will store a reference to the activity.
                // Because this method does not finish, the activity reference will not be cleared.
                int x = 1;
                while (x == 1) {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return 1;
            }
        }

@dpsm
Copy link
Contributor

dpsm commented Jul 15, 2014

@zsxwing I was referring to the original example where the functions is a static inner class. In your example there is really nothing we can do in order to fix that container object reference.

@zsxwing
Copy link
Member

zsxwing commented Jul 15, 2014

@dpsm, I prefer to keep consistent. Otherwise, people may be confused about why anonymous subscribers created in the activity can work, but anonymous functions created in the activity may leak the activity.

@evant
Copy link

evant commented Jul 15, 2014

But the difference is long running operation as an anonymous class vs a short running one. I think that is a clear enough distinction. Also it's clear that your anonymous class is keeping a reference to what it is inside, but less clear that it's also keeping a reference to the Observable.

@dpsm
Copy link
Contributor

dpsm commented Jul 15, 2014

IMHO both @benjchristensen @zsxwing changes are valid and address the original issue with the static inner class scenario. The case where the Action is a non static anonymous inner class and there is a long running operation such as in the example is nothing we can do within the library.

@zsxwing
Copy link
Member

zsxwing commented Jul 15, 2014

@evant, I think people won't write a long running function in the real code. It's usually a long running Observable. That's the root of this problem. If we apply any anonymous function, action or subscriber created in the activity on the long running Observable, it will leak the activity. E.g.,

public class MainActivity extends Activity {
    Subscription s;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        s = new LongRunningObservable().observeOn(Schedulers.io()).map(new Func1<Integer, Integer>() {
            @Override
            public Integer call(Integer integer) {
                // this Func1 will store a reference to the activity.
                // If LongRunningObservable does not finish, the activity reference will not be cleared.
                return integer + 1;
            }
        }).observeOn(AndroidSchedulers.mainThread()).subscribe(
                new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        Toast.makeText(MainActivity.this, "lol", Toast.LENGTH_SHORT).show();
                    }
                }
        );
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (s != null)
            s.unsubscribe();
    }
}

I feel both subscriber and function are same. They are just some operations applied on the long running Observable.

@evant
Copy link

evant commented Jul 15, 2014

I had the impression that the above changes would fix this case as well. Are you saying they don't? Can that not be fixed as easily/not at all? If so then I agree that the differences there could be confusing.

@zsxwing
Copy link
Member

zsxwing commented Jul 15, 2014

For the examples in #1292 (comment) there is nothing we can do within the library.

For the example in #1292 (comment) I'm not sure if it can be fixed. But it's definitely not an easy one.

@evant
Copy link

evant commented Jul 15, 2014

Well then, I do agree. A half solution to this is probably not a good idea. I wouldn't mind spending the time looking at what would need to be done to fix that second case.

@benjchristensen
Copy link
Member

Any conclusions here?

@benjchristensen
Copy link
Member

Closing out ... reopen if anyone wants to re-awake the discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants