You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决JavaFX中Timeline绑定Observable监听器的内存泄漏?

解决JavaFX滚动动画多次启动变慢的内存泄漏问题

我开发了一个带有图片滚动动画的JavaFX应用,多次运行后启动速度越来越慢,推测是内存泄漏导致JVM阻塞。查阅Oracle文档中SimpleDoubleProperty的说明后,定位到问题根源:

ObservableValue会持有监听器的强引用,阻止监听器被垃圾回收,可能导致内存泄漏。建议使用后调用removeListener注销监听器,或使用WeakChangeListener避免该问题。

我对Lambda表达式不太熟悉,尝试用WeakChangeListener替换xPosition.addListener时发现和现有代码配置不兼容,下面是我的代码:

LoginController.java

public class LoginController {
    public Region content;

    public void initialize() {
        DoubleProperty xPosition = new SimpleDoubleProperty(0);
        xPosition.addListener((observable, oldValue, newValue) -> setBackgroundPositions(content, xPosition.get()));
        Timeline timeline = new Timeline(
                new KeyFrame(Duration.ZERO, new KeyValue(xPosition, 0)),
                new KeyFrame(Duration.seconds(200), new KeyValue(xPosition, -15000))
        );
        timeline.play();
    }

    void setBackgroundPositions(Region region, double xPosition) {
        String style = "-fx-background-position: " +
                "left " + xPosition/6 + "px bottom," +
                "left " + xPosition/5 + "px bottom," +
                "left " + xPosition/4 + "px bottom," +
                "left " + xPosition/3 + "px bottom," +
                "left " + xPosition/2 + "px bottom," +
                "left " + xPosition + "px bottom;";
        region.setStyle(style);
    }
}

Main.java

public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            VBox root = (VBox)FXMLLoader.load(getClass().getResource("Login.fxml"));
            Scene scene = new Scene(root,600,600);
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();
            System.out.println("launch");
        } catch(Exception e) {
            e.printStackTrace();
        }

        primaryStage.setOnCloseRequest(e -> {
            System.out.println("Program closed");
        });
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Login.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import java.net.URL?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox fx:id="content" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="456.0" prefWidth="600.0" styleClass="root" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.LoginController">
   <children>
      <BorderPane prefHeight="453.0" prefWidth="600.0">
         <center>
            <AnchorPane prefHeight="200.0" prefWidth="600.0" BorderPane.alignment="CENTER">
               <children>
                  <TextField id="e" layoutX="226.0" layoutY="129.0" />
                  <PasswordField layoutX="226.0" layoutY="161.0" />
                  <Label id="userNameLabel" layoutX="115.0" layoutY="130.0" styleClass="userNameLabel" stylesheets="@Moving.css" text="Username : " textFill="#ff0101">
                     <font>
                        <Font name="OCR A Extended" size="18.0" />
                     </font>
                  </Label>
                  <Label id="passWordLabel" layoutX="115.0" layoutY="163.0" styleClass="passWordLabel" stylesheets="@Moving.css" text="Password : " textFill="#ff0101">
                     <font>
                        <Font name="OCR A Extended" size="18.0" />
                     </font>
                  </Label>
                  <HBox alignment="TOP_CENTER" layoutY="209.0" prefHeight="101.0" prefWidth="600.0" style="-fx-spacing: 10;">
                     <cursor>
                        <Cursor fx:constant="DEFAULT" />
                     </cursor>
                     <children>
                        <Button id="loginBtn" fx:id="loginBtn" alignment="CENTER" contentDisplay="CENTER" mnemonicParsing="false" onAction="#buttonPressed" prefWidth="80.0" styleClass="loginBtn" stylesheets="@Moving.css" text="Log In" />
                                                <Button id="registerBtn" fx:id="registerBtn" mnemonicParsing="false" onAction="#buttonPressed" prefWidth="80.0" styleClass="registerBtn" stylesheets="@Moving.css" text="Register" />
                     </children>
                  </HBox>
               </children>
            </AnchorPane>
         </center>
         <cursor>
            <Cursor fx:constant="DEFAULT" />
         </cursor>
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
      </BorderPane>
   </children>
   <stylesheets>
      <URL value="@application.css" />
      <URL value="@Moving.css" />
   </stylesheets>
   <cursor>
      <Cursor fx:constant="DEFAULT" />
   </cursor>
</VBox>

修复方案

方案一:手动注销监听器

保存监听器的引用,在应用关闭或控制器销毁时主动移除监听器,切断强引用链。

修改后的LoginController.java:

public class LoginController {
    public Region content;
    private ChangeListener<Number> positionListener;
    private Timeline timeline;
    private DoubleProperty xPosition;

    public void initialize() {
        xPosition = new SimpleDoubleProperty(0);
        // 将Lambda监听器赋值给变量,方便后续移除
        positionListener = (observable, oldValue, newValue) -> 
            setBackgroundPositions(content, newValue.doubleValue());
        xPosition.addListener(positionListener);
        
        timeline = new Timeline(
                new KeyFrame(Duration.ZERO, new KeyValue(xPosition, 0)),
                new KeyFrame(Duration.seconds(200), new KeyValue(xPosition, -15000))
        );
        timeline.play();
    }

    // 清理资源方法,在应用关闭时调用
    public void cleanup() {
        if (timeline != null) {
            timeline.stop();
            xPosition.removeListener(positionListener);
        }
    }

    void setBackgroundPositions(Region region, double xPosition) {
        String style = "-fx-background-position: " +
                "left " + xPosition/6 + "px bottom," +
                "left " + xPosition/5 + "px bottom," +
                "left " + xPosition/4 + "px bottom," +
                "left " + xPosition/3 + "px bottom," +
                "left " + xPosition/2 + "px bottom," +
                "left " + xPosition + "px bottom;";
        region.setStyle(style);
    }
}

修改Main.java,在关闭时触发清理:

@Override
public void start(Stage primaryStage) {
    try {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Login.fxml"));
        VBox root = loader.load();
        LoginController controller = loader.getController();
        
        Scene scene = new Scene(root,600,600);
        scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
        System.out.println("launch");

        primaryStage.setOnCloseRequest(e -> {
            controller.cleanup();
            System.out.println("Program closed");
        });
    } catch(Exception e) {
        e.printStackTrace();
    }
}

方案二:使用WeakChangeListener包装Lambda

WeakChangeListener会持有监听器的弱引用,不会阻止垃圾回收。注意需要将Lambda赋值给局部变量,避免被立即回收。

修改LoginController的initialize方法:

public void initialize() {
    DoubleProperty xPosition = new SimpleDoubleProperty(0);
    // 先将Lambda转为普通ChangeListener实例
    ChangeListener<Number> listener = (observable, oldValue, newValue) -> 
        setBackgroundPositions(content, newValue.doubleValue());
    // 用WeakChangeListener包装
    xPosition.addListener(new WeakChangeListener<>(listener));
    
    Timeline timeline = new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(xPosition, 0)),
            new KeyFrame(Duration.seconds(200), new KeyValue(xPosition, -15000))
    );
    timeline.play();
}

额外优化

  • 监听器中直接使用newValue.doubleValue()替代xPosition.get(),减少属性访问开销。
  • 可给Timeline设置setCycleCount(1),动画结束后自动停止,避免无效运行;或在控制器销毁时强制停止Timeline。

内容的提问来源于stack exchange,提问作者user20482809

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 10:20:39