[ 
https://issues.apache.org/jira/browse/FELIX-6871?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul Rütter updated FELIX-6871:
-------------------------------
    Fix Version/s: framework-8.1.0

> 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)

Reply via email to