skip to content

Walk through what actually happens, step by step, from a plug-in artifact sitting in a directory to it being registered, activated, and callable by the core — and what has to happen for it to be safely unloaded.

level: middleimportance: should knowfreq 50%

answer

  1. discover -> resolve -> instantiate -> register -> activate
  2. unload = deregister first, then stop, then discard classloader
  3. per-plugin classloader enables isolation + reclaim
  4. lingering references pin classloaders = leak

basics

~20 s

The core scans for plug-ins, reads their info, loads their code, calls an init/start hook, and adds them to a lookup table the core uses to find them by name. Unloading reverses this: stop the plug-in, free its resources, remove it from the table.

solid answer

~40 s

Lifecycle typically has these stages: discovery (the core scans a directory, classpath, or config for candidate plug-in artifacts), resolution (reading each plug-in's manifest to check its declared dependencies and API version are satisfiable, and computing a load order), instantiation (loading the plug-in's classes, often in an isolated classloader, and constructing its entry-point object), registration (recording the plug-in and the capabilities it fills in the core's registry, keyed by id), activation (calling an init/start hook so the plug-in can acquire resources), and finally the plug-in is live — the core's dispatch mechanism can look it up and invoke it. Unloading reverses this: call stop/dispose so the plug-in releases resources, deregister it so no new calls are dispatched to it, and, if truly hot-swappable, discard its classloader so its classes can be garbage collected.

go deeper

for a junior

Should describe the lifecycle at a high level: find plug-ins, load them, start them, and that they can later be removed.

for a middle

Should name the distinct stages (discovery, resolution/dependency check, instantiation, registration, activation) and explain roughly what each does.

for a senior

Should explain why deregistration must precede teardown to avoid races, and how per-plugin classloaders enable both isolation and reclaiming memory.

for a principal

Should discuss the operational failure modes at scale — classloader leaks from lingering references, activation-order assumptions breaking under partial failures, and how to make lifecycle transitions atomic and idempotent in a live system.

## From a JAR in a directory to a live capability The lifecycle of a plug-in is the operational backbone of a microkernel system — it's the machinery that turns 'a JAR file in a directory' into 'a capability the running application can call,' and later turns it back into nothing, ideally without restarting the whole application. It's useful to walk through it as a sequence of distinct stages, because production bugs in plug-in systems almost always trace back to one specific stage being done sloppily. ## The load sequence 1. **The first stage is discovery**: the core needs some way to find candidate plug-ins without knowing their names in advance. Common mechanisms are scanning a designated directory for artifacts (JARs, DLLs, bundles) matching a naming convention or manifest marker, reading an explicit configuration file that lists plug-in artifact locations, or using a service-loading facility (Java's `ServiceLoader`, an OSGi bundle repository) that inspects metadata embedded in each artifact. The output of discovery is a list of candidates, not yet trusted or loaded. 2. **The second stage is resolution**: for each candidate, the core reads its manifest — id, version, the core API version(s) it targets, and any declared dependencies on other plug-ins. The core, or a dedicated resolver, checks whether those dependencies can actually be satisfied among the discovered candidates and computes a valid load order, since a plug-in that depends on another must be loaded after its dependency is available. This is also where version incompatibilities are caught — a plug-in built for core API v1 being loaded into a core that only supports v3, for instance — and failing here with a clear diagnostic is far preferable to failing later with a cryptic error deep in a call stack. 3. **The third stage is instantiation**: the plug-in's code is actually loaded into the running process. In managed runtimes this is often done through a dedicated classloader per plug-in, which both isolates the plug-in's classes from others that might have conflicting versions of the same library, and later allows the entire classloader to be discarded to reclaim memory when the plug-in is unloaded. The plug-in's designated entry-point class is instantiated, typically implementing a small bootstrap interface the core recognizes. 4. **Fourth is registration**: the plug-in instance, along with the extension points or services it fills, is recorded in the core's registry — usually a map from capability id or interface type to implementation instance, sometimes supporting multiple registered implementations per capability with a priority order. This is the data structure the core's dispatch mechanism consults at call time, so from this point on the plug-in is 'known' but not necessarily yet running. 5. **Fifth is activation**: the core calls a lifecycle hook — `init()`, `start()`, `activate()` — giving the plug-in a chance to acquire resources it needs (open files, start background threads, connect to a database) and signal readiness. Only after activation succeeds is the plug-in considered fully live and safe to route real calls to; a plug-in that throws during activation should be rolled back out of the registry rather than left in a half-registered zombie state, since a half-registered plug-in that the core still routes calls to is a classic source of null-reference or 'not initialized' errors in production. ## Unloading, in reverse Unloading mirrors this in reverse and is, in practice, the harder half to get right. First, the core must stop routing new calls to the plug-in — deregistering it before tearing anything down, so no caller races with the teardown. Then a `stop()`/`dispose()` hook lets the plug-in release its resources: - close file handles, - stop threads, - flush caches, - unregister its own listeners from shared event buses. Only after this completes cleanly can its classloader be discarded for true hot-swap; if any other part of the system still holds a reference to one of the plug-in's objects (a classic leak: the plug-in registered a listener on a long-lived core object and never removed it), that reference keeps the whole classloader, and every class and object it loaded, alive, which is one of the most notorious memory-leak patterns in plug-in-based systems like application servers and IDEs. ## How lifecycle bugs show up In production, lifecycle bugs show up as: - plug-ins that appear 'installed' but throw not-initialized errors because activation order assumptions were violated; - memory leaks after repeated hot-reload cycles because old classloaders are pinned by lingering references; and - race conditions where a plug-in is mid-deregistration while another thread is still dispatching calls to it, because the registry update and the actual invocation aren't synchronized. Getting the lifecycle right is largely a matter of making each stage atomic and idempotent, and never exposing a plug-in in the registry before it has finished activating.

  • Why is it important to deregister a plug-in from the registry before calling its stop/dispose hook, rather than the other way around?
    If the plug-in is stopped first while still registered, another thread could look it up in the registry and dispatch a call to it mid-teardown, hitting closed resources or a half-torn-down object. Deregistering first ensures no new calls are routed to it, so the stop/dispose logic can safely release resources without racing concurrent callers.
  • What causes a plug-in's classloader to stay in memory even after the plug-in has been 'unloaded', and why is this a common leak?
    Any lingering reference from outside the plug-in — such as a listener the plug-in registered on a long-lived core event bus and never unregistered — keeps the plug-in's objects reachable, and because every class an object was loaded from is reachable through it, the whole classloader stays pinned. It's common precisely because plug-in authors often register callbacks with shared core services and forget to unregister them in their stop() hook.

Like checking a new employee into a building: verify their badge and clearances (resolution), issue the badge and add them to the directory (registration), let them through the door and start their shift (activation) — and when they leave, you revoke directory access before you let them walk out, not after, so no one tries to reach someone who's already gone.

saying these in an interview costs you the question

  • Registers a plug-in in the lookup table before its init/activation has succeeded
  • Stops a plug-in's resources before deregistering it from the registry
  • Assumes hot-unload frees memory with no mention of lingering references pinning the classloader
  • Can't distinguish discovery from resolution from activation

context