Memory usage seems surprisingly high, mostly coming from native code (non-Java)
See original GitHub issueBased on my experience (and some basic testing), JB Compose seems to have a relatively high memory overhead.
Even a basic hello world (auto-generated by IntelliJ) uses 108 MB on my Mac and 146 MB on Linux. Jetbrains Toolbox uses 153 MB on my Mac, so real-world apps have a similar footprint. I use JB Toolbox every day and it pretty consistently has this much overhead. This is significantly higher memory usage than Flutter, and only a little bit better than Electron.
What are your thoughts on this? From what I can tell, most of this memory usage is mostly coming from native code outside the Java heap (see below).
Here are some more details on my (unscientific) benchmarking:
Basic Benchmarking
Here are the results of my (admittedly bad) memory usage benchmarks:
Application | Framework | Memory Usage (Mac) | Memory Usage (Linux) |
---|---|---|---|
JB Toolbox | JB Compose | 153 MB | 253 MB |
Compose Hello World | JB Compose | 108 MB | 146 MB |
Flutter hello world | Flutter | 59.9 MB | 113 MB |
Spotify | Electron | 313 MB | ~300 MB |
JavaFX Hello world | JavaFX | (can’t compile) | 209 MB |
Discord | Electron | ~600 MB | ~300 MB |
MultiMC | QT | 58.3 MB | TODO |
Hexchat | GTK3 | N/A | 45 MB |
Strangely the Flutter on Linux has much higher memory usage than the one on Mac 🤷
My Mac system has 32 GB of RAM, my Linux system only has 8 GB.
The memory usage spikes significantly after startup (both for Toolbox & Hello World). Sometimes, the memory usage goes down when I explicitly trigger a garbage collection. The results I gave in my benchmark table are the “average” memory usage. I have occasionally seen significantly less memory, and it is much worse at startup.
Profiling & Thoughts
Would it be possible to tune the memory usage to be more lightweight? Is that even a reasonable goal?
It appears that the vast of the memory seems to be used by native code outside of the heap. I can fix the java heap size to 30MB with Xms30M -Xmx30M
and the “hello world” app still uses 109MB. Unfortunately, the memory analysis of Yourkit and VisualVM (my heap profiling tools) doesn’t seem able to analyze this native memory. I assume a significant portion of this is what’s used by the Skia bindings. I’m not really a graphics expert, so I don’t really know how to analyze this any further 😦
I don’t know much about graphics frameworks, but I would expect the memory usage of Flutter and JB Compose to be similar (Electron is a different story). Flutter and Compose both use garbage collected VMs and rely on Skia for rendering. So it’s somewhat surprising that the memory usage of JB Compose is so much higher than Flutter (at least on my Mac).
I’m sorry for the long post (and my imprecise benchmarks), I’m just curious about the (seemingly) high memory usage in a framework which otherwise seems really amazing 😄 It appears to be coming from native graphics code, which I don’t really know much about…
-- Techcable
Issue Analytics
- State:
- Created 2 years ago
- Reactions:18
- Comments:8 (5 by maintainers)
Top GitHub Comments
It’s extremely difficult to run a compose app with Grall
native-image
, because of the significant restrictions on reflection. I’m currently running into problems with Kotlin coroutines and gave up 😦Also using the “fallback” image doesn’t really do much for memory usage. In my tests, it actually made it significantly worse.
In my tests, it appears that most of the memory is not used by the java heap (only 8MB), and also very little is used by class metadata (15MB). Without all the other overhead, the ideal app footprint would be 24-32MB (combining heap and metaspace).
Presumably, Graal’s memory savings come mostly from reducing class footprint, not externally allocated native memory. As a result, I would recommend focusing on optimization and profiling with the standard JVM instead.
You can print a detailed summary of JVM heap usage with
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:NativeMemoryTracking=summary
.I have done a detailed analysis of my hello world app and have some recommendations below 😄
Profiling Results
Total memory usage is about 103.63 MiB according to the JVM. (Although see below note, the system claims “150MB”)
Here’s a breakdown of the different memory usage (based on
-XX:+PrintNMTStatistics
):It appears to me that the biggest bloat comes from thread stack size. It uses more than heap and codespace combined.
-Xmx16M -Xms8M
Some entries have been omitted in the interest of space (see below for full data).
Cass metaspace (15MB) is something that cannot really be avoided,
If we add together class metaspace and heap we only get 23MB of usage (that’s only 1/4 of the total).
As I mentioned above, seems the main culprit of this memory bloat is threads. It uses more memory than the heap and codespace combined. Of the 103 MB of total memory, thread stack sizes use up almost half!
In our case the process spawned 21 different threads, using a total of 52.3MB of stack space.
I would recommend figuring out ways to reduce the number of background threads and also reducing the size of their stacks.
The JVM has a flag
-Xss<size>
to change the default stack size. By simply setting-Xss500K
I was able to cut thread stack usage in half, saving 25M from the total memory amount used 🚀Based on this encouraging result, it would appear that memory optimization should start by focusing on threads.
In addition to the global flag, Java threads can have their stack size explicitly set although the result is platform dependent.
Presumably, there are options to reduce the number of background threads spawned by the JVM and third-party libraries. In general, The JVM tends to be optimized for large heaps where stack size and number of threads are insignificant. In our case it’s the biggest usage of memory!
It looks like JB Toolbox is using 52 threads at the moment. Extrapolating from the stack usage we have here, that would put stack usage for that app at almost 100 MB. In some cases it can be lower like 42 or 35 (but that’s still a ton of memory).
class data sharing
Class data sharing allows part of the class metadata to be shared between all JVM processes on the system. In our case it looks like 11.8MB is shared. I am not sure if this is counted as part of “class metadata” or if it’s separate. See oracle docs), so there is 11.8MB of memory that is shared between all the JVM processes on my system 🎊
I believe it is possible to customize this, so this is also another possible avenue for optimization.
Extra Notes & Raw Output
Original command line and raw output
The original command line is `java -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:NativeMemoryTracking=summary -Xshare:on -XX:ReservedCodeCacheSize=5M -XX:+UseS erialGC -Xmx16M -Xms8M -jar build/compose/jars/compose-hello-world-macos-arm64-1.0.0.jar > summary.txt`A note on JVM flags used
I've tried tuning flags to optimize memory usage of the hello-world. I was able to limit the heap to 16Mb (`-Xmx16M`). Adding `XX:+UseSerialGC` seemed to shave of about 5MBs (instead of UseG1GC). Most other flags I tried beyond that didn't make much of a difference.Limitting the code space saved maybe 1MB at most.
Asside from limiting heap and setting GC, tuning flags seems like a dead end.
Different methods give different results for "overall memory usage".
There are a couple different ways to track overall memory usage (on my mac). Some of them give different results - The JVM's total "committed" memory: 103.63 MiB - bpytop claims ~124MB (I think this gives RRS on linux) - Activity monitor claims ~150MB - Real memory 124MB - Shared memory 134MB (presumably for [class data sharing](https://docs.oracle.com/en/java/javase/13/vm/class-data-sharing.html#GUID-7EAA3411-8CF0-4D19-BD05-DF5E1780AA91)] - Private memory size 26MBYou should check memory usage when Compose JVM Desktop is compiled to a GraalVM Native Image.