James Roome created FELIX-6871:
----------------------------------

             Summary: 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


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
(v8.20.10#820010)

Reply via email to