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.

Reply via email to