[
https://issues.apache.org/jira/browse/FELIX-6871?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul Rütter resolved FELIX-6871.
--------------------------------
Resolution: Fixed
> BundleClassLoader leaves a class lock behind when another thread defines the
> class first
> ----------------------------------------------------------------------------------------
>
> Key: FELIX-6871
> URL: https://issues.apache.org/jira/browse/FELIX-6871
> Project: Felix
> Issue Type: Bug
> Components: Framework
> Affects Versions: framework-7.0.5
> Environment: Any OS and JDK; most visible with virtual threads (JDK
> 21+). Observed on macOS aarch64, JDK 25.
> Reporter: James Roome
> Priority: Minor
> Labels: classloader-leak
> Fix For: framework-8.1.0
>
>
> h3. Problem
> {{BundleClassLoader.defineClassParallel}} can leave an entry in
> {{m_classLocks}} that is never removed:
> {code:java}
> while (clazz == null && m_classLocks.putIfAbsent(name, me) != me)
> {
> clazz = findLoadedClass(name);
> }
> if (clazz == null)
> {
> try { ... defineClass ... }
> finally { m_classLocks.remove(name); }
> }
> {code}
> When {{putIfAbsent}} returns {{{}null{}}}, this thread has just taken the
> lock, but the loop condition is still true, so the body runs
> {{findLoadedClass}} once more. If another thread defined the class in the
> meantime, {{clazz}} is no longer {{null}} and the loop exits. The {{finally}}
> that would release the lock is never reached, so the entry {{name -> thread}}
> stays in the map for the life of the class loader.
> This happens whenever a thread reads a class's bytes and another thread
> defines that class before the first one reaches {{{}defineClassParallel{}}}.
> For example, thread B spins while thread A holds the lock; A defines the
> class and removes its entry; B's next {{putIfAbsent}} succeeds, and its
> {{findLoadedClass}} then finds A's class.
> h3. Impact
> The class still loads correctly, but the stale entry keeps its {{Thread}}
> reachable for as long as the bundle's class loader lives.
> For a platform thread that pins the thread object and its context class
> loader. For a virtual thread it also pins the continuation and everything its
> task references. We found this in a downstream OSGi consumer that activates
> components on virtual threads: after a feature was uninstalled, a long-lived
> bundle's {{m_classLocks}} still held a terminated virtual thread whose task
> referenced a departed bundle's component, which kept that bundle's retired
> class loader reachable.
> h3. Proposed fix
> When the loop exits with the class already found, release the lock if this
> thread holds it:
> {code:java}
> else
> {
> // This thread may have taken the lock (putIfAbsent returned null) and
> then found
> // the class already defined by another thread, so release it if it is
> ours.
> m_classLocks.remove(name, me);
> }
> {code}
> {{remove(name, me)}} only removes an entry owned by the current thread. A
> thread that found the class while another thread still held the lock leaves
> that thread's entry alone.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)