processing4
processing4 copied to clipboard
Heap allocation growth
Description
A simple Processing 4 app seems to add between roughly .5MB to 1MB of memory usage every 5 seconds.
Expected Behavior
If the app isn't doing anything, it shouldn't be adding memory, right?
Current Behavior
Adding .5mb to 1mb of memory usage every 5 seconds. In the time searching for similar issues and typing this message, the app went from 150MB to 295MB
Steps to Reproduce
Create a new app, draw 2 squares, and run it. Now check memory usage of the app. App used:
void settings(){
size(1920,1080);
}
void draw(){
//Fill background
fill(0);
noStroke();
rect(0,0,1920,540);
//Key background
fill(128);
noStroke();
rect(0,540,1920,540);
}
Your Environment
- Processing version: Processing 4.1.2 for Apple Silicon
- Operating System and OS version: Monterey 12.6.2
I could confirm the issue on a similar system: Processing 4.1.2 for Apple Silicon & Monterey 12.5.1
Went from 141Mb to 200Mb and kept slowly creeping up.
@sampottinger do you have some ideas about this one?

Note: The settings()
function is not needed in the sketch above. It's only useful when it's absolutely necessary to define the parameters to size()
with a variable. This doesn't seem to be a factor however as the memory usage keeps growing even when the same sketch is initialised with setup()
.
👀 I can take a look!
I saw #516 from @einarOttestad. Just CC'ing as it might be related.
That looks like the same issue, yes. That also confirms that this is not only an Apple Silicon issue. Forcing garbage collection every x minutes is a working workaround.
Hello all! Yeah so I took a look at this inside VisualVM and have attached what the heap looks like inside Java. Will drop my results here and follow up with some thoughts in a minute.
Experiment 1: No GC
The garbage collector does seem to be doing its job but I noticed that the heap size is never reduced. In Activity Monitor, I see it allocate additional memory up until the first GC and then it didn't continue appreciably allocating after that for me.
void setup(){
size(100,100);
frameRate(120);
}
void draw(){
background(0);
}

Experiment 2: Explicit GC
Per @sj-unit72, the following will keep additional memory from getting allocated.
int lastGc;
void setup(){
size(100,100);
frameRate(120);
lastGc = millis();
}
void draw(){
background(0);
if (millis() - lastGc > 2000) {
System.gc();
lastGc = millis();
}
}

Experiment 3: No GC, let it go into standby
I re-ran experiment 1 for longer and it does appear to continue to GC but, again, Activity Monitor didn't see Java continue to allocate memory afterwards.

Experiment 4: Single GC at the start
Asking for GC at the start seemed to change the behavior of the garbage collector and I didn't see Java continue to allocate memory past the initial memory usage at launch in the Activity Monitor. You can see in VisualVM that it got very aggressive.
void setup(){
size(100,100);
frameRate(120);
System.gc();
}
void draw(){
background(0);
}

To be clear, garbage collecting manually on a time interval should not fix an actual memory leak if there is one; the VM should GC as needed (assuming it's not completely broken, in which case ¯\_(ツ)_/¯), but by default may only do so only at high memory usage; so a buildup to 300MB could be "normal", but it shouldn't grow infinitely without a true leak. An explicit GC just sets a smaller cutoff to how much it can grow before it shrinks.
There could definitely be a memory leak at play here and/or in the linked rPi report, but System.gc();
shouldn't be considered a workaround; it'll just make things clearer if the leak does exist.
Tying this together... @sj-unit72 / @einarOttestad I'm curious what brought this to your attention. Are you running into memory exhaustion on your computer?
What's going on
Yeah I agree @dzaima - on Apple Silicon, it doesn't appear to be a memory leak per-se given the VisualVM output. I'm not seeing much to suggest that there is consistent increase in heap usage after garbage collection happens.
As @benfry and @dzaima suggested in #516, it's potentially just the behavior of the garbage collector potentially trying to hold onto memory to prevent an expensive (time consuming) operation later. I'm not sure if this behavior is a departure from previous JDKs but I will add that I think the Shenandoah GC became standard in JDK 17.
That being said, even giving it one System.gc()
seems to switch it to a conservative allocation strategy that gets reflected in Activity Monitor with the allocation not growing in my test. It's important to note that System.gc()
is pretty complicated and it is more about giving Java hints about the desired garbage collector behavior (if you look at the docs, it may not even necessarily invoke collection).
What's taking up memory?
It's mostly byte[]
but hard to say exactly where that is originating from. However, it isn't too surprising that this would be the leading consumer.
Next steps
I have mixed feelings about the next step here. I don't see evidence of a memory leak but this kind of heap allocation is a little impolite. Assuming there isn't exhaustion, from Procssing's perspective, it's not a unreasonable choice for Java to pre-emptively allocate available memory for speed. We could explicitly invoke System.gc
(even once) to try to convince it to be more aggressive but I'm not sure that's desirable for all sketches as it likely has some kind of performance / memory trade off in the GC later.
I'm building a visualizer that has to run non stop for a week and was evaluating Processing for it. Part of the evaluation process was to look at memory use and that's when I saw the odd behavior. I ran the test app for an extended time and "Real Memory Size" in the Activity Monitor went up to over 1GB before I stopped the app.
Adding the manual garbage collection now keeps it running steady at around 290MB
It's mostly
byte[]
but hard to say exactly where that is originating from
There'll be one byte[]
per String
, and there are a lot of strings usually. So the more interesting things should be below those two (unless of course byte[]
s or String
s are the actual important things being leaked, but I doubt that's the case)
Sorry @dzaima I should have included this (and sorted by size).

@sj-unit72 - yeah like I said this is likely not a memory leak per-se then. It's just the strategy taken by the GC. I'm curious if you had any performance issues with the periodic manual GC or did that sort it for you? I'm curious if you get the same kind of performance profile just by putting one System.gc
in setup as well?
All that said, asking the group here if we think any action should be taken on the processing side? Maybe there's an option to incorporate this into the Processing documentation or we wrap it somewhere in the API as an option (like a "low memory mode" or something with a call to System.gc
from setup - like memoryMode(HIGH_SPEED)
or memoryMode(COMPACT)
that gets gobbled up by the preproc) but I don't know if all sketches will prefer an explicit System.gc
call from inside Processing itself. As it could change the performance profile quite a bit, I'm reluctant to include it without the sketch asking for it. Working with VisualVM and based on what's been said here, there isn't clear evidence that Processing itself is leaking / spooling up non-GC-able references.
LMK if there is interest in adding memoryMode
and I can drop in a PR but it would be similar to size
in that it would need to be in settings
or setup
. After I have the proposal, Ben would probably need to take a look and make the final call on if it makes sense to add to the API (I'm a bit on the fence).
If the issue is fixed by calling System.gc()
then it's not a Processing bug, it's how the Java VM is deciding when to clear memory. If you have plenty of memory on your machine, modern software (Windows, macOS, Java, etc) won't bother doing excessive cleanup work until the memory is actually needed.
So this is likely superficial unless we see something in particular that either 1) causes it to run out of memory (an actual leak) or 2) is causing problems in actual usage, versus just seeming odd. I'd like to assume that JVM folks (and OS developers like them) know what they're doing more than we do.
Meant to say that there's a third option where we're doing unnecessary allocation, which happens from time-to-time, but it doesn't sound like that's what is happening here.
Yeah I would agree with that @benfry. I think this might just be the GC (and specifically shenandoah) trying to avoid a poorly timed "real malloc" later.