Epsilon: The JDK’s Do-Nothing Garbage Collector
blogs.oracle.com
blogs.oracle.com
If you want to read more about these new GCs, this is a great post: https://blog.plan99.net/modern-garbage-collection-part-2-1c8...
Okay.
What JVM really really needs for performance is value types: a lot of GC problems will simply not exist with value types.
Project Valhalla started five (!) years ago, and they are still working on it.
JVM could also use multiple heaps per process (so multiple GCs could run independently not affecting other heap execution and GC). There were talks about it years ago, but nobody is working on it AFAIU.
ZGC and Shenandoah are Oracle and Redhat's respective projects with similar goals. I like the options and activies around GC all personally.
I'm wondering if there are any security implications here. Like a container could figure out the workload of another container probablistically. Thinking out loud here but sometimes these side channels are leaky.
Epsilon tied on speed but lost to Shenandoah on latency because never freeing memory isn't ideal either, even if you never run out of memory.
It’s something I do all the time in C++.
The object graph could be stored in a succinct compressed format to reduce it's size as much as possible, compared to actual 64 bit pointers.
Then the GC algorithm could crawl the graph much much faster by virtue of needing to access far fewer memory pages, and increased likelihood that those pages will be in a fast cpu cache most of the time.
It doesn't need to access the actual data when sweeping.
I think the parent proposed moving pointers out of the objects, so you wouldn't even have to touch the user's objects while marking.
One thing you can do, if you know that all your objects will live in one arena, and the maximum size of that arena is <= 2^N objects for some N, is to store "pointers" as N-bit rather than 64-bit values. Java does this for N = 32: https://wiki.openjdk.java.net/display/HotSpot/CompressedOops
Someone at RC suggested using succinct data structures and I spent a little while studying them, but they seemed to me like they'd have more niche-y properties than a more obvious approach would.
Just spitballing, but I suspect the issue would be that you now lose the objects locality when reading/following/writing references in conjunction with data. The GC algorithm will be crawling much less than your program will hopefully so it might be a net loss.
Just speculation!
https://instagram-engineering.com/dismissing-python-garbage-...
Although you might gain some performance, I guess this "GC" is going to be used mostly by people running financial - or other - low latency analysis/streaming code where it has been common for years to try to tune the JVM to never even attempt to GC - to avoid latency.
The code in these cases are written to reuse most memory, and when the unreclaimable part grows to big, that cluster node stops taking requests - and is then restarted.
I was especially frustrated by this, because I had spent some time writting a Clojure program that had to be able to cold-start fast. Decompiling the program's JAR and pruning out unnecessary classes gave a considerable speed-up, but it was not enough.
I would look into GraalVM native images if you want fast startup.
In modern GCs, allocation is already as fast as can be (pointer-bump allocation), so I imagine the only win in chopping out the GC is that you don't need to initialize the GC (it's otherwise roughly equivalent to simply terminating before the GC needs to be invoked).
Perhaps the DMD example isn't quite the same, though, as it's possible its GC has slower allocation than pointer-bump.
[0] https://www.drdobbs.com/cpp/increasing-compiler-speed-by-ove...
You could try OpenJ9 for CLI tools which claims to offer faster startup times out of the box compared to openjdk. Tweaking the JIT behavior (number of compiler threads, compilation thresholds for the tiers etc.) or using CDS can help too.
You can use GraalVM to build fast startup Clojure binaries.
From the post:
> There is a strong temptation to use Epsilon on deployed programs, rather than to confine it to performance tuning work. As a rule, the Java team discourages this use, with two exceptions. Short-running programs, like all programs, invoke the garbage collector at the end of their run. However, as JEP 318 explains, “accepting the garbage collection cycle to futilely clean up the heap is a waste of time, because the heap would be freed on exit anyway.”
Also these help reduce load times:
- Class Data Sharing (CDS; JDK 5+) https://docs.oracle.com/en/java/javase/11/vm/class-data-shar...
- Application Class Data Sharing (AppCDS; JDK 10+) https://openjdk.java.net/jeps/310
- Ahead-Of-Time compilation (AOT; jaotc; JDK 9+ Linux-x86_64 only): http://openjdk.java.net/jeps/295 - JVM runtime trimmer (jlink; JDK 9+): http://openjdk.java.net/jeps/282
---
Drip: https://github.com/ninjudd/drip
Nailgun:
(You won't get the chance to log any write errors reported by close() or react to the errors, though.)
That script could probably have used POSIX::_exit() to get the same speedup without the calling process thinking it crashed.
uWSGI, a web application container for Python, Perl and other languages has an option for that:
--skip-atexit-teardown
The teardown time delay comes from:- Unreferencing objects recursively, therefore tracing all objects.
- Calling destructor functions.
- Potentially doubling idle memory use due to copy-on-write as all the objects are written to.
When a web application server restarts all its child processes, the third item in particular can result in a large spike in memory use.
> can't the regular GCs just not clean up on program exit
Why do you need to clean up your memory - at all, GC or otherwise - on program exit? The process and all its resources will be gone.
So how does calling GC help with anything?
Which GCs do that? I'm not aware of any.
"Short-running programs, like all programs, invoke the garbage collector at the end of their run."
If the platform doesn't feature finalizers, or if it is known that they've not been used, there'd be no point at all.
Additionally, global destructors, while not guaranteed, can be very helpful if you let them run rather than just exiting and letting the system clean up file descriptors: for example, a clean disconnect from a database is often faster overall (on the database side, e.g. freeing up a connection slot) than a dirty "client hasn't phoned in for awhile/received unexpected FIN" disconnect via hard-exit.
Just unmap the heap pages. Don't run the GC!
> global destructors, while not guaranteed, can be very helpful if you let them run
If you want them to run on exit then you want Runtime.runFinalizersOnExit, not the GC. Finalizers are non-deterministic, asynchronous, and would take an indefinite number of GC cycles to run them for all objects.
When I was writing the Epsilon JEP, I meant that it might be futile to have a hundreds-of-ms-long GC cycle, when the program exits very soon anyway, and the heap would be abandoned wholesale. The important bit of trivia is that GC might be invoked long before 'the whole memory' is exhausted. There are several reasons to do this: learning the application profile to size up generations or collection triggers, minimizing the startup footprint, etc. GC cycle then can be seen as the upfront cost that pays off in future. With the extremely short-lived job that future never comes.
Contrived example:
$ cat AL.java
import java.util.*;
public class AL {
public static void main(String... args) throws Throwable {
List<Object> l = new ArrayList<>();
for (int c = 0; c < 100_000_000; c++) {
l.add(new Object());
}
System.out.println(l.size());
}
}
$ javac AL.java
Ooof, 12.5 seconds to run, and about 2 cpu-minutes taken with Parallel: $ time jdk11.0.5/bin/java -XX:+UnlockExperimentalVMOptions -Xms3g -Xmx3g -XX:+UseParallelGC -Xlog:gc AL
[0.015s][info][gc] Using Parallel
[0.988s][info][gc] GC(0) Pause Young (Allocation Failure) 768M->469M(2944M) 550.699ms
...
[12.281s][info][gc] GC(3) Pause Full (Ergonomics) 1795M->1615M(2944M) 7660.045ms
100000000
real 0m12.464s
user 1m53.618s
sys 0m1.087s
Much better with G1, but we still took 11 cycles that accrued enough pauses to affect the end-to-end timing. Plus GC threads took some of our precious CPU. $ time jdk11.0.5/bin/java -XX:+UnlockExperimentalVMOptions -Xms3g -Xmx3g -XX:+UseG1GC -Xlog:gc AL
[0.031s][info][gc] Using G1
[0.452s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 316M->314M(3072M) 124.119ms
...
[2.518s][info][gc] GC(11) Pause Young (Normal) (G1 Evacuation Pause) 2321M->2324M(3072M) 79.496ms
100000000
real 0m2.953s
user 0m16.880s
sys 0m0.872s
Now Epsilon, whoosh, 1.5s end-to-end, and less than 1s of user time, which is probably the only running Java thread itself, plus some OS memory management on allocation path. $ time jdk11.0.5/bin/java -XX:+UnlockExperimentalVMOptions -Xms3g -Xmx3g -XX:+UseEpsilonGC -Xlog:gc AL
[0.004s][info][gc] Using Epsilon
...
[1.387s][info][gc] Heap: 3072M reserved, 3072M (100.00%) committed, 2731M (88.93%) used
real 0m1.480s
user 0m0.830s
sys 0m0.699s
You might think fully concurrent GCs would solve this, and they partially do, by avoiding large pauses. But they still eat CPUs. For example, while Shenandoah is close to Epsilon in doing the whole thing in about 1.7s wall clock time, it still takes quite significant CPU time. Therefore, that benefit is there because machine has spare CPUs to offload that work to. $ time jdk11-shenandoah/bin/java -XX:+UnlockExperimentalVMOptions -Xms3g -Xmx3g -XX:+UseShenandoahGC -Xlog:gc AL
[0.009s][info][gc] Using Shenandoah
...
[0.913s][info][gc] Trigger: Learning 3 of 5. Free (1651M) is below initial threshold (2150M)
[0.913s][info][gc] GC(2) Concurrent reset 1265M->1267M(3072M) 0.689ms
[0.914s][info][gc] GC(2) Pause Init Mark 0.111ms
[1.276s][info][gc] GC(2) Concurrent marking 1267M->1925M(3072M) 361.985ms
[1.306s][info][gc] GC(2) Pause Final Mark 0.465ms
[1.306s][info][gc] GC(2) Concurrent cleanup 1924M->1748M(3072M) 0.171ms
real 0m1.761s
user 0m5.688s
sys 0m0.633sYes, the article is incorrect about this. We’ll make sure it gets fixed.
These numbers are quite interesting. Thanks for doing this analysis!