英文:
JavaFX exception on setImage in a loop problem
问题
I have a JavaFX program where I continuously set an image in an ImageView using the setImage(new Image(new ByteArrayInputStream(imageBytes))) method within a loop. This entire process is wrapped in Platform.runLater() to ensure UI thread safety.
The issue I'm facing is that when I enable -Dprism.verbose=true, the console continuously prints the message "Growing pool D3D Vram Pool target," followed by an increasing number. Eventually, it leads to a NullPointerException. Surprisingly, adding System.gc() after the setImage call resolves the issue, and the exceptions stop after a short while.
I understand that calling System.gc() in a loop is not recommended for performance reasons, and the garbage collector should handle this automatically. However, without this explicit call, the growing pool and exceptions persist.
I have also tried calling setImage(null) before setting the actual image to inform the garbage collector, but it didn't make any difference. I have even increased the memory, but the issue remains.
Is this behavior normal? What could be causing the growing pool and the NullPointerException? Any suggestions on how to resolve this issue without relying on System.gc()?
英文:
I have a JavaFX program where I continuously set an image in an ImageView using the setImage(new Image(new ByteArrayInputStream(imageBytes))) method within a loop. This entire process is wrapped in Platform.runLater() to ensure UI thread safety.
The issue I'm facing is that when I enable -Dprism.verbose=true, the console continuously prints the message "Growing pool D3D Vram Pool target," followed by an increasing number. Eventually, it leads to a NullPointerException. Surprisingly, adding System.gc() after the setImage call resolves the issue, and the exceptions stop after a short while.
I understand that calling System.gc() in a loop is not recommended for performance reasons, and the garbage collector should handle this automatically. However, without this explicit call, the growing pool and exceptions persist.
I have also tried calling setImage(null) before setting the actual image to inform the garbage collector, but it didn't make any difference. I have even increased the memory, but the issue remains.
Is this behavior normal? What could be causing the growing pool and the NullPointerException? Any suggestions on how to resolve this issue without relying on 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..)
and after a few seconds im getting:
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
});
}
});
答案1
得分: 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;
}
}
您说您正在循环加载图像。我不太确定您为什么使用循环。但我猜您正在尝试定期加载图像。要做到这一点,对于上面定义的任务,您可以使用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();
}
}
演示了使用服务和任务的示例应用程序。它将定期从字节数组后台加载图像并在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();
}
}
用于从图像资源创建一些测试数据的实用程序类。
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;
}
}
此处使用的图像在此答案中提
英文:
I don't have a way to reproduce your exact issue, and the info in this answer may not solve your issue, but perhaps it will help.
General Advice
What you don't want to do is write some busy loop that:
- consumes the JavaFX thread and freezes the application.
- rapidly gobbles up huge amounts of resource memory without allowing the memory management system to appropriately free up stale memory.
- floods the JavaFX runLater queue with more runnables than the system can reasonably process.
- corrupts memory by accessing it on multiple threads.
If you were repetitively loading the same images, you would want to cache the loaded images (there is an example cache implementation in this answer), so you don't need to load them again. You also want to appropriately size the images in the image constructor if you don't need the full image resolution in your rendered images.
As advised by Trashgod in the comments, it is usually a good idea to load Images in the background.
If loading from a URL, you can load (and size) images in the background via the appropriate image constructor. You don't need to multithread with a task or service if you do that. You can listen to the progress property to see how the loading is progressing if you need to know that.
You can periodically load new images using a Timeline or PauseTransition, loading them in the background to ensure that the main JavaFX thread is not impacted.
Example
This example uses a Task and ScheduledService to load images periodically in a background thread.
This is a Task that loads an image from a byte array in the background.
This is only provided because you are not loading from a URL, but from a ByteArrayInputStream for which there is no background loading constructor in the 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;
}
}
You say you are loading the images in a loop. I'm not exactly sure why you use a loop. But I guess you are trying to load images periodically. To do that, for the task defined above, you can use a ScheduledService.
I am not sure where and how you get the bytes for your images. So for this example, I just pass in all of the image bytes in a preloaded List for simplicity. For an actual application implementation with a dynamic source of images, you wouldn't want to do that.
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();
}
}
Sample app which demonstrates the use of the service and task. It will periodically load images in the background from byte arrays and display them in an ImageView, to create a slideshow.
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();
}
}
Utility class to create some test data from image resources.
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;
}
}
The images used here are provided in this answer:
The images should be placed in a resource directory matching the package you place the MonsterImageCreator in.
Some additional points
> I have even increased the memory
You can't really do that. The Direct3D textures are native resources of the 3D graphic system used by your JavaFX implementation, you won't have any control over how the memory is used. The memory used can be texture memory in the graphic card video ram, not memory associated with the Java heap.
What you are doing is blowing up that video memory space in some way. How I don't know.
- One way to do it is to load textures that exceed the max size allowed by the graphic device (typically something 4K x 4K pixels or 8K x 8K pixels).
- Another way to do it is to load lots of textures into memory that exceeds the total texture memory of the graphics card (which will vary by graphics card, but is typically multiple gigabytes of compressed texture memory space).
- You can also cause corruption of the texture memory space by incorrectly threaded code causing race conditions.
- Maybe you are running a busy loop that just loads massive amounts of textures into the system without giving the JavaFX texture manager time to appropriately release obsolete textures.
> What could be causing the growing pool
Allocating new graphics textures is a frequent operation in a UI application. It is normal that the texture memory pool would change in size dynamically. Textures are used widely, not just for your images, but also for the rendering engine (both in 2D and 3D mode).
Generally, you don't really have to worry about the management of texture memory resources as the JavaFX implementation will take care of that work for you, in a similar way that you don't need to worry much about memory management in a Java application.
The only things you really need to worry about are the standard things that you have to consider in managing memory resources in Java. Those are things like:
- Ensuring you don't have multiple threads corrupt memory.
- Not creating very large objects which will exceed the available memory size.
- Not holding onto object references you no longer need, so that the garbage collector (or JavaFX's internal manager of texture memory resources) can free those resources as it wishes.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论