Tomcat и многопоточность aspose imaging

Доброе время суток!
Пробую возможность перехода на Ваш продукт. При использовании в web-приложении под управлением Tomcat (10.1.44 )+ Struts2 (7.0.x) + ubuntu, во время остановки сервера всегда выскакивает следующий ужОс :slight_smile:

29-Oct-2025 20:12:33.082 INFO [main] org.apache.catalina.core.StandardServer.await A valid shutdown command was received via the shutdown port. Stopping the Server instance.
29-Oct-2025 20:12:33.082 INFO [main] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
29-Oct-2025 20:12:33.095 INFO [main] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
29-Oct-2025 20:12:33.213 WARNING [main] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [weak-ref-list-remover] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 java.base/jdk.internal.misc.Unsafe.park(Native Method)
 java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)
 java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:519)
 java.base/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3780)
 java.base/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3725)
 java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1707)
 java.base/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:67)
 java.base/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)
 java.base/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:234)
 com.aspose.imaging.internal.na.t$a.run(Unknown Source)
 java.base/java.lang.Thread.run(Thread.java:1623)
29-Oct-2025 20:12:33.213 WARNING [main] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [DisposerCleaner] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 java.base/jdk.internal.misc.Unsafe.park(Native Method)
 java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)
 java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:519)
 java.base/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3780)
 java.base/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3725)
 java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1707)
 java.base/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:67)
 java.base/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)
 java.base/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:234)
 com.aspose.imaging.internal.st.a.run(Unknown Source)
 java.base/java.lang.Thread.run(Thread.java:1623)
29-Oct-2025 20:12:33.215 SEVERE [main] org.apache.catalina.loader.WebappClassLoaderBase.checkThreadLocalMapForLeaks The web application [ROOT] created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal@2725c740]) and a value of type [com.aspose.imaging.system.Threading.Thread] (value [com.aspose.imaging.system.Threading.Thread@252e2e16]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
29-Oct-2025 20:12:33.216 SEVERE [main] org.apache.catalina.loader.WebappClassLoaderBase.checkThreadLocalMapForLeaks The web application [ROOT] created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal@75618e16]) and a value of type [com.aspose.imaging.internal.nq.g] (value [com.aspose.imaging.internal.nq.g@7cf7583d]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
29-Oct-2025 20:12:33.216 SEVERE [main] org.apache.catalina.loader.WebappClassLoaderBase.checkThreadLocalMapForLeaks The web application [ROOT] created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal@5b0e558a]) and a value of type [com.aspose.imaging.internal.oi.m] (value [com.aspose.imaging.internal.oi.m@226661c3]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
29-Oct-2025 20:12:33.225 INFO [main] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
29-Oct-2025 20:12:33.237 INFO [main] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]

Сама библиотека Aspose.Imaging, используется следующим образом:

        try(JpegImage jpegImage = (JpegImage)Image.load(filePath))
        {
            //формируем большое preview
            byte [] buffer = createPreview(jpegImage 500);

           //Сохраняем байтовый массив в файл

            //формируем маленькое preview
            buffer = createPreview(jpegImage, 100;

            //Сохраняем байтовый массив
        }

Сам метод создания preview

    protected byte [] createPreview(JpegImage image, int width)
    {
        //если изображение не кэшировано
        if(!image.isCached())
        {
            image.cacheData();
        }

        //выполнить установку размеров
        image.resizeWidthProportionally(width, ResizeType.LanczosResample);

        //опции для JPEG сжатия
        JpegOptions options = image.getJpegOptions();
        options.setCompressionType(JpegCompressionMode.Baseline);
        options.setColorType(JpegCompressionColorMode.YCbCr);
        options.setQuality(quality);

        //создать байтовый поток, записать в него изображение и выйти
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        image.save(byteArrayOutputStream, options);
        return byteArrayOutputStream.toByteArray();
    }

Все отрабатывает как надо, создаются две preview (500 на X и 100 на X), все хорошо, НО при остановке Tomcat, каждый раз выскакивает предупреждение и по мониторингу за памятью, я вижу, что потребление оперативки растет и не высвобождается.
У меня большое подозрение, что так как делаю я (два раза подряд resize) делать не правильно, но я не нашел ни где примера.
Подскажите пожалуйста, как правильно? Надо ли каждый раз после image.save освобождать image и загружать заново или есть более адекватный способ.
Спасибо.

@noxius

Good day!

The issue you are encountering is related to the fact that some threads created by the Aspose.Imaging library do not stop properly when the Tomcat server is shut down, which can lead to memory leaks. This often occurs if resources are not released correctly.

In your code, you are using JpegImage and resizing it twice. This may be the reason why resources are not being released. It is recommended to explicitly release resources after each use of JpegImage to avoid memory leaks.

Here are several steps you can take:

  1. Close Resources: Ensure that you close all streams and release resources after use. For example, use jpegImage.dispose() after you are done working with the image.

  2. Reload the Image: If you plan to use the same image multiple times, it may be better to reload it after each save to avoid accumulating unreleased resources.

  3. Check Threads: Make sure that all threads you create are properly terminated when the application stops. You can do this by adding handlers to terminate threads in your application’s shutdown method.

Here is an example of how you can modify your code:

JpegImage jpegImage = null;
try {
    jpegImage = (JpegImage) Image.load(filePath);
    // Create large preview
    byte[] buffer = createPreview(jpegImage, 500);
    // Save byte array to file

    // Create small preview
    buffer = createPreview(jpegImage, 100);
    // Save byte array
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // Release resources if necessary
    if (jpegImage != null) {
        jpegImage.dispose();
    }
}

Try these recommendations and see if they help eliminate the warnings about threads and memory leaks when stopping Tomcat. If the problem persists, it may be worth reaching out to Aspose support for a more detailed analysis.

If you have any further questions, feel free to ask!

Но разве вариант

try(JpegImage jpegImage = (JpegImage)Image.load(filePath))
{
       // что-то делаем
}

Не эквивалентен вариант?

JpegImage jpegImage = null;
try 
{
    jpegImage = (JpegImage) Image.load(filePath);
    //что-то делаем
}
finally
{
    // Release resources if necessary
    if (jpegImage != null) 
    {
        jpegImage.dispose();
    }
}

Причем, метод close(), разве не вызывает внутри себя dispose()

И второй вопрос, вариант с повторной загрузкой - единственный возможный в данном случае?

Я попробовал после каждого resize освобождать image.close() и перезагружать изображение, более того, попробовал просто единожды делать resize. Упростил вариант до вырожденного случая:

  1. загружается ОДНО изображение
  2. resize этого изображения
  3. освобождение ресурса
  4. останавливаем Tomcat

Результат без изменений, я пробовал это и на ubuntu и на windows

Забыл отметить, по факту, все это выполняется в одном потоке - в рамках одного Http запроса.

@noxius Добрый день! Благодарим вас за интерес к нашему продукту. Мы внимательно изучим данную проблему и свяжемся с вами с результатами в ближайшее время.

Спасибо, буду ждать!

Привет @noxius
По сути вопроса, могу сказать следующее

  1. Метод close() вызывает dispose(). По сути это одно и тоже.
  2. В коде Aspose.Imaging есть использование ThreadLocal (поэтому есть уведомление в логе). Есть планы заменить их.
  3. Память не выгружается окончательно до тех пор, пока GC не отработает, поэтому визуально может быть не видно освобождения памяти. close/dispose просто закрывает файловые ресурсы и сбрасывает ссылки на объекты, чтобы GC из собрал.
  4. По поводу потоков weak-ref-list-remover и DisposerCleaner. Мы проведем исследование. По идее эти потоки выгружают неуправляемые ресурсы и не должны препятствовать остановке сервиса. Но мы проверим и если нужно устраним проблему в следующем релизе.

Все утверждения абсолютно верны

Уточнение, правильно ли я понял?
У меня есть изображение file.jpg, я хочу создать две preview file1.jpg и file2.jpg, при этом исходное изображение не должно меняться. Я должен это делать следующим образом (упрощенное описание):

  1. Image.load(file.jpg)
  2. resize и сохранение в file1.jpg
  3. Image.close()
  4. Image.load(file.jpg)
  5. resize и сохранение в file2.jpg
  6. Image.close()

Или я могу

  1. Image.load(file.jpg)
  2. resize и сохранение в file1.jpg
  3. resize и сохранение в file2.jpg
  4. Image.close()

Заранее спасибо.

Корректный 1 вариант


    Image.load(file.jpg)
    resize и сохранение в file1.jpg
    Image.close()
    Image.load(file.jpg)
    resize и сохранение в file2.jpg
    Image.close()

Спасибо большое за ответы :slight_smile: У меня больше нет вопросов, Вы закроете тему?

1 Like