IMHO you are underestimating your heap space requirement.
- For 20_000 by 5_000 pixels the
BufferedImage
needs at least 400 MB (100_000_000 pixels at 4 bytes per pixel)
ByteArrayOutputStream
starts with a buffer size of 32 bytes and increases the buffer size every time when there is not enough room for writing data, at least doubling the size of the buffer every time
- in my test the final buffer size in the
ByteArrayOutputStream
was 536_870_912
- because that was size was probably reached by doubling the previous size, it temporarily needed 1.5 times that memory
- the final byte array another 300_000_054 bytes to store the final byte array.
The critical point in memory consumption is the line return stream.toByteArray();
:
- the
BufferedImage
cannot be garbage collected (400MB)
- the
ByteArrayOutputStream
contains a buffer of about 540MB
- the memory for the final byte array needs to be allocated (another 300MB)
- giving a total needed memory at that specific point of 1240MB for the image processing alone (not taking into account all the memory that the rest of your application consumes)
You can somewhat reduce the needed memory by presizing the ByteArrayOutputStream
(by about 240MB):
public byte[] imageAsBytes (BufferedImage image) {
int imageSize = image.getWidth()*image.getHeight()*3 + 54; // 54 bytes for the BMP header
try (ByteArrayOutputStream stream = new ByteArrayOutputStream(imageSize)) {
ImageIO.write(image, "bmp", stream);
return stream.toByteArray();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
Another problem:
The method createNewChart()
is marked as @Transactional
. I don't know about your database, the technology that you use for database access and transaction management (JPA?) or how the pictures are effectively stored in the database (BLOB? base64 encoded?).
Most probably the whole database stack (and in this term I include the persistence framework like JPA too) keeps all the pictures in memory until the whole transaction is commited.
To verify this assumption you could remove the @Transactional
attribute from createNewChart()
so that the chart and the fragments are stored into the database in different transactions.
So it seems that something in your application holds references to memory when you expect them to be freed. Diagnosing this just from the source code alone is (IMHO) impossible - it could be something that you have written, it could be something in a library that you use.
To detect what keeps those references you should as a first step create a heap dump when the OutOfMemoryError is raised. You can do this by adding -XX:+HeapDumpOnOutOfMemoryError
to the JVM options like this:
java -XX:+HeapDumpOnOutOfMemoryError -Xmx3G ..remaining start options..
This will produce a java_pid<somenumber>.hprof
file when the OutOfMemoryError occurs.
The next step is then analyzing this file to find out what is referencing the memory. What works for me is the Eclipse Memory Analyzer (it is maintained by the Eclipse Foundation but you can use it for all heap dumps). Loading the previously generated heap dump into this tool gives a good overview into what is still referencing the images.
The next step would then be to find out why those references are still there.