JVM:非堆内存导致OutOfMemoryError,因为垃圾收集器没有运行.我做错了什么?



我遇到了一个奇怪的问题,我正在开发的Groovy应用程序越来越多地消耗内存(远远超出了xml参数的限制,所以它不可能是堆),直到计算机的RAM耗尽,这时JVM的行为有两种不同的方式之一——它要么突然释放(几乎)它所占用的所有内存,要么崩溃并产生OutOfMemoryError。这个问题可以通过定期调用System.gc()来避免。

发生在我身上的似乎是,尽管分配了越来越多的内存,JVM不调用垃圾收集器。不清楚它是否总是(尝试)在计算机RAM耗尽时调用它,并且只是有时成功,或者它是否有时抛出OOME而不调用GC(即使这会违反规范)。值得注意的是,ResourceMonitor没有报告java.exe实例提交了更多内存(它保持在500MB左右),但是提交费用还是上升了。

我所做的唯一的事情,而这正在进行的是有一个定时器启动一个新的线程,每33ms调用一个JComponent的repaint()。我听说每个新线程都在堆外分配了一些内存,所以我怀疑问题可能是内存从未被收集,但我可能错了(我真的觉得自己没有深度,TBH)。

我显然可以通过让Timer定期调用System.gc()来解决问题(尽管每隔几秒钟不少于一次),但这对我来说似乎是非常糟糕的做法,我真诚地希望我做错了什么,而不是JVM的一些奇怪问题。

我当时运行的唯一代码是下面的代码(我已经删除了一些注释和一些日志到控制台)。当然,还有一大堆代码,但唯一活动的是,如前所述,计时器调用repaint()。

//snip: package and imports

@groovy.transform.CompileStatic
class MapWindow extends BasicWindow {
//BasicWindow provides a constructor that stores its two arguments as windowX and windowY (and set dimensions accordingly) and creates and stores a basic frame
//It also overrides setVisible() to call frame.setVisibile() as well. It does nothing else.
int xPos
int yPos
MapWindow(int x, int y) {
super(x, y)
frame.setTitle("EFG")
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
frame.pack()
}
@Override
public void paint(Graphics gA) {
VolatileImage img = createVolatileImage(windowX, windowY)
Graphics2D g = (Graphics2D) img.getGraphics()
VolatileImage tmp = getTestImage()
g.drawImage(tmp, 0, 0, null)
g.dispose()
gA.drawImage(img, 0, 0, windowX, windowY, null)
if (Game.game.rnd.nextInt(100) == 0) {
//System.gc()  <--If I uncomment this, things work
}
}
VolatileImage getTestImage() {
VolatileImage img = createVolatileImage(windowX, windowY)
Graphics2D g = img.createGraphics()
for (int x = 0; x < tileSet.x; x++) {
for (int y = 0; y < tileSet.y; y++) {
g.drawImage(tileSet.images[x][y], x * tileSet.resolution, y * tileSet.resolution, null)
}
}
char[] msg = "test complete".toCharArray()
for (int x = 0; x < msg.length; x++) {
char c = msg[x]
if (!c.isWhitespace()) {
g.drawImage(tileSet.getImage("symbol.$c"), x * tileSet.resolution, tileSet.resolution * tileSet.y, null)
}
}
g.dispose()
return img
}
}
//Located in a different class, called once during startup. It also subject to a @CompileStatic
void startTimer() {
timer = new java.util.Timer()
int period = config.getInt("framePeriod")
boolean fixed = config.getBoolean("frameFixedRate")
if (fixed) {
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
activeWindow?.frame?.repaint()
}
}, period, period)
} else {
timer.schedule(new TimerTask() {
@Override
public void run() {
activeWindow?.frame?.repaint()
}
}, period, period)
}
}
如果需要,我可以提供更多的代码/信息,但我不想通过张贴整个程序来阻塞这一点。问题似乎非常可能在这里的某个地方,可能在paint()或getTestImage()(或JVM)中。

Windows 7 64位
16GB RAM,无pagefile
SDK 1.8.0_25(问题也在13.0.1中确认)
Groovy 2.5.8
我使用IntelliJ IDEA,但如果我构建一个.jar并独立运行它,问题也会发生。

编辑:ewramner已经指出,我应该调用flush()上(或重用)的挥发图像,所以我已经接受了作为解决方案。如果有人能解释为什么GC没有更早地采取行动,我仍然很感兴趣,特别是如果它导致JVM崩溃并出现OOME。

如果你读过VolatileImage的文档,它说:

当创建一个VolatileImage对象时,可能会分配有限的系统资源,例如VRAM来支持该映像。当一个VolatileImage对象不再被使用时,它可能会被垃圾收集,并且那些系统资源将被返回,但是这个过程不会在保证的时间内发生。创建了许多VolatileImage对象的应用程序(例如,一个调整大小的窗口可能会在大小改变时强制重新创建它的后端缓冲区)可能会耗尽新的VolatileImage对象的最佳系统资源,仅仅是因为旧的对象还没有从系统中删除。

解决方案是调用flush(在这里调用System.gc),或者重用图像,而不是在每次绘制操作时重新创建图像。

最新更新