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)