Disposer and Disposable
The IntelliJ Platform's
Disposer facilitates resource cleanup. If a subsystem keeps a set of resources alive coincident with a parent object's lifetime, the subsystem's resources should be registered with the
Disposer to be released before or at the same time as the parent object.
The most common resource type managed by
Disposer is listeners, but there are other possible types:
File handles, and database connections,
Caches and other significant data structures.
Disposer is a singleton that manages a tree of
Disposable instances. A
Disposable is an interface for any object providing a
Disposable.dispose() method to release heavyweight resources after a specific lifetime.
Disposer supports chaining
Disposables in parent-child relationships.
Automatically Disposed Objects
Many objects are disposed automatically by the platform if they implement the
Disposable interface. The most important type of such objects is services. Application-level services are automatically disposed by the platform when the IDE is closed or the plugin providing the service is unloaded. Project-level services are disposed when the project is closed, or the plugin is unloaded.
Note that extensions registered in
plugin.xml are not automatically disposed. If an extension requires executing some code to dispose it, you need to define a service and to put the code in its
dispose() method or use it as a parent disposable.
The Disposer Singleton
The primary purpose of the
Disposer singleton is to enforce the rule that a child
Disposable never outlives its parent.
Disposable objects in a tree of parent-child relationships. The tree of
Disposable objects ensures the
Disposer releases children of a parent first.
See The Disposable Interface for more information about creating
Registering a disposable is performed by calling
Choosing a Disposable Parent
To register a child
Disposable, a parent
Disposable of a suitable lifetime is used to establish the parent-child relationship. One of the parent
Disposables provided by the IntelliJ Platform can be chosen, or it can be another
Use the following guidelines to choose the correct parent:
For resources required for a plugin's entire lifetime, use an application or project level service.
For resources required while a dialog is displayed, use
For resources required while a tool window tab is displayed, pass your instance implementing
For resources with a shorter lifetime, create a disposable using
Disposer.newDisposable()and dispose it manually using
Disposable.dispose(). Note that it's always best to specify a parent for such a disposable (e.g., a project-level service), so that there is no memory leak if the
Disposable.dispose()call is not reached because of an exception or a programming error.
Disposer API's flexibility means that if the parent instance is chosen unwisely, the child may consume resources for longer than required. Continuing to use resources when they are no longer needed can be a severe source of contention due to leaving some zombie objects behind due to each invocation. An additional challenge is that these kinds of issues won't be reported by the regular leak checker utilities, because technically, it's not a memory leak from the test suite perspective.
For example, suppose a UI component created for a specific operation uses a project-level service as a parent disposable. In that case, the entire component will remain in memory after the operation is complete. This creates memory pressure and can waste CPU cycles on processing events that are no longer relevant.
Registering Listeners with Parent Disposable
Many IntelliJ Platform APIs for registering listeners either require passing a parent disposable or have overloads that take a parent disposable. For example:
Methods with a
parentDisposable parameter automatically unsubscribe the listener when the corresponding parent disposable is disposed. Using such methods is always preferable to removing listeners explicitly from the
dispose method because it requires less code and is easier to verify for correctness.
To choose the correct parent disposable, use the guidelines from the previous section.
The same rules apply to message bus connections. Always pass a parent disposable to
MessageBus.connect(), and make sure it has the shortest possible lifetime.
Determining Disposal Status
You can use
Disposer.isDisposed() to check whether a
Disposable has already been disposed. This check is useful, for example, for an asynchronous callback to a
Disposable that may be disposed before the callback is executed. In such a case, the best strategy is usually to do nothing and return early.
Ending a Disposable Lifecycle
A plugin can manually end a
Disposable lifecycle by calling
Disposer.dispose(Disposable). This method handles recursively disposing of all the
Disposable child descendants as well.
Implementing the Disposable Interface
Creating a class requires implementing the
Disposable interface and defining the
In many cases, when the object implements
Disposable only to be used as a parent disposable, the method's implementation will be completely empty.
An example of a non-trivial
dispose implementation is shown below:
A lot of code setting-up all the conditions requiring release in
dispose() has been omitted for simplicity.
Regardless, it illustrates the basic pattern, which is:
In this case, the parent disposable is passed into the constructor,
Foodisposable is registered as a child of
parentDisposablein the constructor.
dispose()method consolidates the necessary release actions and will be called by the
Diagnosing Disposer Leaks
When the application exits, it performs a final sanity check to verify everything was disposed. If something was registered with the
Disposer but remains undisposed, the IntelliJ Platform reports it before shutting down.
In test and Debug mode (
idea.disposer.debug is set to
on), registering a
Disposable with the
Disposer also registers a stack trace for the object's allocation path. The
Disposer accomplishes this by creating a dummy
Throwable at the time of registration.
The following snippet represents the sort of "memory leak detected" error encountered in practice:
In this specific case, the IntelliJ Platform (
CoreProgressManager) started a task that contained the
DynamicWizard code. In turn, that code allocated a
Project that was never disposed by the time the application exited. That is a promising place to start digging.
The above memory leak was ultimately caused by failing to pass a
Project instance to a function responsible for registering it for disposal. Often the fix for a memory leak is as simple as understanding the memory scope of the object being allocated - usually a UI container, project, or application - and making sure a
Disposer.register() call is made appropriately for it.