I added this to the bug report. Thanks.
-- Kevin
On 10/30/2026 1:50 AM, Craig Raw wrote:
The test is a window of 50 labels and 50 buttons, plus a script that
reads its accessibility tree through System Events and counts live
objects with jcmd GC.class_histogram (which runs a full GC first).
Each read of an unchanged window retains exactly 306 MacVariant and
106 long[] objects, with no growth while idle:
after first read: MacVariant=406 long[]=2922
round 1 (+10 reads): MacVariant=3466 (+3060) long[]=3982 (+1060)
round 2 (+10 reads): MacVariant=6526 (+3060) long[]=5042 (+1060)
round 3 (+10 reads): MacVariant=9586 (+3060) long[]=6102 (+1060)
idle 30s, no reads
idle: MacVariant=9586 (+0) long[]=6102 (+0)
Steps:
1. Grant the terminal Accessibility permission (System Settings >
Privacy & Security > Accessibility).
2. Save the two files below and run chmod +x a11yleak.sh.
3. In one terminal: java --module-path <javafx-sdk>/lib --add-modules
javafx.controls --enable-native-access=javafx.graphics A11yLeak.java
It prints its PID.
4. In a second terminal: ./a11yleak.sh <pid>
----- A11yLeak.java -----
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class A11yLeak extends Application {
@Override
public void start(Stage stage) {
FlowPane pane = new FlowPane();
for(int i = 0; i < 50; i++) {
pane.getChildren().add(new Label("Label " + i));
pane.getChildren().add(new Button("Button " + i));
}
stage.setTitle("A11yLeak");
stage.setScene(new Scene(pane, 800, 400));
stage.show();
System.out.println("PID " + ProcessHandle.current().pid());
}
public static void main(String[] args) {
launch(args);
}
}
----- a11yleak.sh -----
#!/bin/sh
# Usage: a11yleak.sh <pid> [rounds] [reads-per-round]
PID=$1
ROUNDS=${2:-4}
READS=${3:-10}
counts() {
jcmd "$PID" GC.class_histogram | awk '$4 ==
"com.sun.glass.ui.mac.MacVariant" { v = $2 } $4 == "[J" { j = $2 } END
{ print v + 0, j + 0 }'
}
read_tree() {
osascript -e "tell application \"System Events\" to tell (first
process whose unix id is $PID) to get entire contents of window 1" >
/dev/null || exit 1
}
read_tree
set -- $(counts)
echo "after first read: MacVariant=$1 long[]=$2"
PREV_V=$1
PREV_J=$2
ROUND=1
while [ "$ROUND" -le "$ROUNDS" ]; do
I=0
while [ "$I" -lt "$READS" ]; do
read_tree
I=$((I + 1))
done
set -- $(counts)
echo "round $ROUND (+$READS reads): MacVariant=$1 (+$(($1 -
PREV_V))) long[]=$2 (+$(($2 - PREV_J)))"
PREV_V=$1
PREV_J=$2
ROUND=$((ROUND + 1))
done
echo "idle 30s, no reads"
sleep 30
set -- $(counts)
echo "idle: MacVariant=$1 (+$(($1 - PREV_V))) long[]=$2 (+$(($2 -
PREV_J)))"
----- end -----
Some detail from the original submission that may help whoever picks
it up:
- A heap dump before and after two full tree reads in my application
attributes all of the growth to JNI_LOCAL GC roots on a single thread
(MacVariant, long[] and String). These callbacks run on the toolkit
thread nested inside the event loop's native call, so their local
references are only released at exit.
- On macOS, GlassAccessible.m has no DeleteLocalRef or
PushLocalFrame/PopLocalFrame. The references come from the
CallObjectMethod results in the accessibility callbacks
(accessibilityAttributeNames, accessibilityAttributeValue,
accessibilityArrayAttributeValues,
accessibilityParameterizedAttributeNames,
accessibilityAttributeValue:forParameter, accessibilityActionNames),
and from the GetObjectField and GetObjectArrayElement calls inside
variantToID and jArrayToNSArray, some of them in loops. Separately,
accessibilityAttributeValue, accessibilityAttributeValue:forParameter
and accessibilityActionDescription invoke object-returning methods
with CallLongMethod and cast the result to jobject.
- On Windows, GlassAccessible.cpp has the same gap: GetPropertyValue
(WinVariant plus the String read for VT_BSTR), callArrayMethod,
get_BoundingRectangle and get_ValueString, and it doesn't use the
JLocalRef helper used elsewhere in Windows glass. I haven't run the
test case on Windows. In my application, driven by a UI Automation
client on Windows Server 2025, live WinVariant objects grew by about
13,700 per tree read, and after roughly two hours of automated reads
the process held over 8 million WinVariant objects (452 MB).
- Bracketing each callback with PushLocalFrame/PopLocalFrame (or
JLocalRef on Windows) seems simpler than individual DeleteLocalRef
calls, since the conversions create references in loops and recursively.
- On macOS 26, Spotlight alone activates JavaFX accessibility
(JDK-8391459), so users who don't run a screen reader are affected too.