JVM: память без кучи вызывает OutOfMemoryError, поскольку сборщик мусора не запущен. Что я делаю не так?

Я столкнулся со странной проблемой, когда приложение Groovy, над которым я работаю, потребляет больше памяти (намного выше пределов xmx-аргумента, поэтому это не может быть куча) до тех пор, пока у компьютера не закончится ОЗУ, в в этот момент JVM ведет себя одним из двух разных способов: либо внезапно освобождает (почти) всю занятую память, либо аварийно завершает работу с ошибкой OutOfMemoryError. Этой проблемы можно избежать, регулярно вызывая System.gc().

Мне кажется, что, несмотря на выделение все большего и большего объема памяти, JVM не вызывает сборщик мусора. Неясно, всегда ли он (пытается) вызывать его, как только у компьютера заканчивается ОЗУ, и только иногда это удается, или иногда он выдает OOME без вызова GC (хотя это нарушит спецификации). Может быть интересно отметить, что ResourceMonitor нет сообщает, что экземпляр java.exe выделяет больше памяти (оно остается на уровне ~ 500 МБ), но плата за фиксацию все равно увеличивается.

Единственное, что я делаю, пока это происходит, - это чтобы таймер запускал новый поток каждые 33 мс для вызова repaint() JComponent. Я слышал, что каждому новому потоку выделяется некоторая память за пределами кучи, поэтому я подозреваю, что проблема может заключаться в том, что эта память никогда не собирается, но я могу ошибаться (я действительно чувствую себя здесь не в своей тарелке, TBH).

Очевидно, я мог бы решить проблему, просто заставив Timer регулярно вызывать System.gc() (хотя и не реже, чем раз в несколько секунд), но мне это кажется очень плохой практикой, и я искренне надеюсь, что есть что-то, что я m делаю неправильно, а не в какой-то странной проблеме с 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-битная
16 ГБ ОЗУ, без файла подкачки
SDK 1.8.0_25 (проблема также подтверждена в 13.0.1)
Крутой 2.5.8
Я использую IntelliJ IDEA, но проблема также возникает, если я создаю .jar и запускаю его независимо.

Обновлено: ewramner указал, что я должен вызывать flush() для (или повторно использовать) VolatileImages, так что я принял это как решение. Мне все еще было бы интересно, если бы кто-нибудь мог объяснить Зачем, что GC не действует раньше, особенно если это приводит к сбою JVM с OOME.

Основы программирования на Java
Основы программирования на Java
Java - это высокоуровневый объектно-ориентированный язык программирования, основанный на классах.
Концепции JavaScript, которые вы должны знать как JS программист!
Концепции JavaScript, которые вы должны знать как JS программист!
JavaScript (Js) - это язык программирования, объединяющий HTML и CSS с одной из основных технологий Всемирной паутины. Более 97% веб-сайтов используют...
1
0
36
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если вы читаете документацию для VolatileImage, там написано:

When a VolatileImage object is created, limited system resources such as video memory (VRAM) may be allocated in order to support the image. When a VolatileImage object is no longer used, it may be garbage-collected and those system resources will be returned, but this process does not happen at guaranteed times. Applications that create many VolatileImage objects (for example, a resizing window may force recreation of its back buffer as the size changes) may run out of optimal system resources for new VolatileImage objects simply because the old objects have not yet been removed from the system.

Решение состоит в том, чтобы вызвать промывать (где вы вызываете System.gc) или, возможно, повторно использовать изображение вместо его повторного создания для каждой операции рисования.

Я думал, что VRAM — это отдельная вещь на GPU, а не часть (обычной) RAM. Я ошибся? (Я также думал, что повторное использование VolatileImages было плохой идеей, потому что они могли исчезнуть)

Callid 16.05.2022 21:37

Это предположение, но в документации говорится, что когда у них закончится «ускоренная память», они все равно будут созданы, но не будут работать так же хорошо. Я предполагаю, что в этот момент они вернутся к обычной оперативной памяти, и в какой-то момент ее все равно нужно будет освободить. Попробуйте заменить System.gc на flush и посмотрите, поможет ли это!

ewramner 17.05.2022 08:47

Пробовал и это, и повторное использование VolatileImage в paint() сейчас, и оба, кажется, работают. Я еще не уверен, могут ли быть проблемы с чрезмерным отрисовкой, так как я, в конце концов, в настоящее время только рисую (непрозрачный) новый VolatileImage на повторно используемом, но я полагаю, я замечу, если это оказывается проблемой.

Callid 17.05.2022 22:49

Другие вопросы по теме