JavaFX循环中setImage异常问题

0 java garbage-collection javafx image render

我有一个 JavaFX 程序,在循环中使用 setImage(new Image(new ByteArrayInputStream(imageBytes))) 方法在 ImageView 中连续设置图像。整个过程被包装在 Platform.runLater() 中以确保 UI 线程安全。

我面临的问题是,当我启用 -Dprism.verbose=true 时,控制台会连续打印消息“Growing pool D3D Vram Pool target”,然后显示不断增加的数字。最终,它会导致 NullPointerException。令人惊讶的是,在 setImage 调用之后添加 System.gc() 解决了该问题,并且异常在一段时间后停止。

据我所知,出于性能原因,不建议在循环中调用 System.gc(),并且垃圾收集器应该自动处理此问题。但是,如果没有这种显式调用,不断增长的池和异常仍然存在。

我还尝试在设置实际图像之前调用 setImage(null) 以通知垃圾收集器,但这没有任何区别。我什至增加了内存,但问题仍然存在。

这种行为正常吗?是什么导致池不断增长和 NullPointerException?关于如何在不依赖 System.gc() 的情况下解决此问题有什么建议吗?

(This code constantily grows D3D Vram Pool like : Growing pool D3D Vram Pool target to 524.167.168 Growing pool D3D Vram Pool target to 524.167.168 Growing pool D3D Vram Pool target to 535.226.368..)

几秒钟后我得到:

java.lang.NullPointerException at com.sun.prism.d3d.D3DTexture.getContext(D3DTexture.java:84) at com.sun.prism.d3d.D3DTexture.update(D3DTexture.java:207) at com.sun.prism.d3d.D3DTexture.update(D3DTexture.java:151)...

   this.packetImage.setImageByteStream(new ImageByteStream() {
                @Override
                public void stream(byte[] imageBytes) {
                    Platform.runLater(() -> {
                        imageView.setImage(new Image(new ByteArrayInputStream(imageBytes)));
  System.gc(); //<- fixes the problem
                    });
                }
            });
Run Code Online (Sandbox Code Playgroud)

jew*_*sea 5

我没有办法重现您的确切问题,并且此答案中的信息可能无法解决您的问题,但也许会有所帮助。

一般建议

你不想做的是编写一些繁忙的循环:

  • 消耗 JavaFX 线程并冻结应用程序。
  • 快速吞噬大量资源内存,而不允许内存管理系统适当地释放陈旧内存。
  • 用比系统可以合理处理的更多的可运行对象淹没 JavaFX runLater 队列。
  • 通过在多个线程上访问内存来破坏内存。

如果您重复加载相同的图像,您可能需要缓存加载的图像(此答案中有一个示例缓存实现),因此您不需要再次加载它们。如果渲染图像中不需要完整的图像分辨率,您还需要在图像构造函数中适当调整图像的大小。

正如 Trashgod 在评论中建议的那样,在后台加载图像通常是一个好主意。

如果从 URL 加载,您可以通过适当的图像构造函数在后台加载图像(并调整大小)。如果这样做,您就不需要对任务或服务进行多线程处理。如果您需要知道的话,您可以监听进度属性来查看加载的进度。

您可以使用 Timeline 或 PauseTransition定期加载新图像,在后台加载它们以确保主 JavaFX 线程不受影响。

例子

此示例使用 Task 和 ScheduledService 在后台线程中定期加载图像。

这是一个在后台从字节数组加载图像的任务。

提供此选项只是因为您不是从 URL 加载,而是从 ByteArrayInputStream 加载,而 JavaFX 20 Image API 中没有后台加载构造函数。

import javafx.concurrent.Task;
import javafx.scene.image.Image;

import java.io.ByteArrayInputStream;

class ImageLoadingTask extends Task<Image> {
    private final byte[] imageBytes;

    public ImageLoadingTask(byte[] imageBytes) {
        this.imageBytes = imageBytes;
    }

    @Override
    protected Image call() throws Exception {
        Image image = new Image(
                new ByteArrayInputStream(
                        imageBytes
                )
        );

        if (image.isError()) {
            throw image.getException();
        }

        return image;
    }
}
Run Code Online (Sandbox Code Playgroud)

你说你正在循环加载图像。我不太确定你为什么使用循环。但我猜你正在尝试定期加载图像。为此,对于上面定义的任务,您可以使用 ScheduledService。

我不确定您在哪里以及如何获取图像的字节。因此,对于这个示例,为了简单起见,我只是在预加载列表中传递所有图像字节。对于具有动态图像源的实际应用程序实现,您不会希望这样做。

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Duration;

import java.util.List;

class ImageLoadingService extends ScheduledService<Image> {
    private static final Duration SHOW_IMAGE_PERIOD = Duration.seconds(2);
    private final List<byte[]> imageByteList;
    private int nextImageIdx = 0;

    public ImageLoadingService(
            ImageView imageView,
            List<byte[]> imageByteList
    ) {
        this.imageByteList = imageByteList;

        setPeriod(SHOW_IMAGE_PERIOD);
        setRestartOnFailure(false);

        configureToLoadNextImage();

        valueProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue != null && !newValue.isError()) {
                imageView.setImage(
                        getValue()
                );

                configureToLoadNextImage();
            }
        });

        exceptionProperty().addListener((observable, oldException, imageLoadingException) -> {
            if (imageLoadingException != null) {
                imageLoadingException.printStackTrace();
            }
        });
    }

    private final ObjectProperty<byte[]> imageBytes = new SimpleObjectProperty<>(
            this,
            "imageBytes"
    );

    public final void setImageBytes(byte[] value) {
        imageBytes.set(value);
    }

    public final byte[] getImageBytes() {
        return imageBytes.get();
    }

    public final ObjectProperty<byte[]> imageBytesProperty() {
        return imageBytes;
    }

    @Override
    protected Task<Image> createTask() {
        return new ImageLoadingTask(
                getImageBytes()
        );
    }

    private void configureToLoadNextImage() {
        setImageBytes(imageByteList.get(nextImageIdx));
        nextImageIdx = (nextImageIdx + 1) % imageByteList.size();
    }
}
Run Code Online (Sandbox Code Playgroud)

演示服务和任务的使用的示例应用程序。它将定期从字节数组加载后台图像并将其显示在 ImageView 中,以创建幻灯片。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.List;

public class BackgroundImageLoader extends Application {
    private static final int IMAGE_SIZE = 96;
    private final List<byte[]> monsterImages = MonsterImageCreator.createMonsterImageBytes();

    @Override
    public void start(Stage stage) throws IOException {
        ImageView imageView = new ImageView();

        ImageLoadingService imageLoadingService = new ImageLoadingService(
                imageView,
                monsterImages
        );
        imageLoadingService.start();

        StackPane layout = new StackPane(imageView);
        layout.setPrefSize(IMAGE_SIZE, IMAGE_SIZE);

        stage.setScene(new Scene(layout));
        stage.show();
    }
}
Run Code Online (Sandbox Code Playgroud)

用于从图像资源创建一些测试数据的实用程序类。

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

class MonsterImageCreator {
    private enum Monster {
        Medusa,
        Dragon,
        Treant,
        Unicorn
    }

    public static List<byte[]> createMonsterImageBytes() {
        List<byte[]> imageBytes = new ArrayList<>(
                Monster.values().length
        );

        for (Monster monster : Monster.values()) {
            try (InputStream inputStream = Objects.requireNonNull(
                    MonsterImageCreator.class.getResource(
                            monster + "-icon.png"
                    )
            ).openStream()) {
                byte[] bytes = inputStream.readAllBytes();

                imageBytes.add(
                        bytes
                );
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        return imageBytes;
    }
}
Run Code Online (Sandbox Code Playgroud)

此答案中提供了此处使用的图像:

图像应放置在与您放置 MonsterImageCreator 的包匹配的资源目录中。

一些补充要点

我还增加了内存

你真的不能那样做。Direct3D 纹理是 JavaFX 实现所使用的 3D 图形系统的本机资源,您无法控制内存的使用方式。使用的内存可以是显卡显存中的纹理内存,而不是与 Java 堆相关的内存。

您正在做的是以某种方式炸毁视频内存空间。我怎么不知道。

  • 一种方法是加载超过图形设备允许的最大尺寸的纹理(通常为 4K x 4K 像素或 8K x 8K 像素)。
  • 另一种方法是将大量纹理加载到超过显卡总纹理内存的内存中(这会因显卡而异,但通常是多个 GB 的压缩纹理内存空间)。
  • 错误的线程代码还可能导致竞争条件,从而导致纹理内存空间损坏。
  • 也许您正在运行一个繁忙的循环,该循环只是将大量纹理加载到系统中,而没有给 JavaFX 纹理管理器时间来适当地释放过时的纹理。

是什么导致池不断增长

分配新的图形纹理是 UI 应用程序中的常见操作。纹理内存池的大小动态变化是正常的。纹理应用广泛,不仅用于图像,还用于渲染引擎(2D 和 3D 模式)。

一般来说,您实际上不必担心纹理内存资源的管理,因为 JavaFX 实现将为您处理这项工作,就像您不需要太担心 Java 应用程序中的内存管理一样。

您真正需要担心的唯一事情是在 Java 中管理内存资源时必须考虑的标准事情。这些是这样的:

  • 确保多个线程不会损坏内存。
  • 不创建会超出可用内存大小的非常大的对象。
  • 不保留不再需要的对象引用,以便垃圾收集器(或 JavaFX 的纹理内存资源的内部管理器)可以根据需要释放这些资源。