Java JIT vs. Java AOT vs. Go for Small, Short-Lived Processes
macias.info
macias.info
The deploy JARs have 100+ MB of class files, so perhaps it's a function of all the dependencies that you "need" for an enterprise Java program and not something more fundamental.
These blog posts also present AOT as if its just another option you can toggle, while my impression is that it's incompatible with common Java libraries and requires swapping / maintaining a parallel compiler in the build toolchain, configuring separate rules to build it AOT, etc. I don't have actual experience with it though so I could be missing something.
This is not a new phenomenon: back in 2000, the IBM System's Journal had a special issue on Java. Most of it was filled with articles detailing how amazingly awesome performance now was and getting better all the time.
The last article in that issue was from IBM's San Francisco Project, the only one with real-world usage. Which reported much more, er, "mixed" results. More accurately: atrocious performance on their real-world tasks, and a lot of extra engineering effort to get it to work at least half reasonably.
(I write a little more about this in "Jittterdämmerung" https://blog.metaobject.com/2015/10/jitterdammerung.html)
(full disclosure: I work at Red Hat and I have contributed to the project)
There's nothing inherent to the JVM that'd need 1GB of memory footprint - the jars are compressed and loaded on demand so unless your server needs all of them instantiated at once that doesn't explain the memory usage. Typically the region where class metadata are stored is the PermGen and it is not more than 256M for even the most heavyweight programs. My guess is that in your case the Xms (min heap) is set to 1Gb and so that's what you see as baseline.
OpenJ9 is designed for lower memory usage than the hotspot VM and also supports AOT by the way if memory usage is your concern.
Not disputing that the JVM needs more memory than anything else because it comes with a lot of features and it is very easy to use more but many J2EE apps can be made to work with a Gig and you do get a lot of features for that. And memory is cheap so you can decide what to optimize for.
Memory may be "cheap", but wasting 950MB of memory per process because the GC might flip out at the wrong time isn't cheap when you multiply it out to many processes.
Also, I find the claim that the JVM starts and is running in 90ms very dubious. I would like to see an all-in timer, something like run each in a container, and see which completes first from container startup to shutdown.
Yeah but in that case your app legitimately needs that much. Not the JVM. Secondly unless your app is extremely performance sensitive setting Xms to lower value doesn't make the GC flip out - it's not as if the GC is going to collect repeatedly to stay within your lower bound - on the contrary it will expand the heap upwards until Xmx limit. Sure there will be a cost to expand the heap above Xms up to Xmx but it is not at all significant due to how clever the GC is.
$ JAVA=/usr/lib/jvm/java-8-openjdk/bin/java; for i in {1..100}; do time -p "$JAVA" -Xshare:on Hello; done 2>&1|grep real|awk 'BEGIN { sum=0 } { sum += $2 } END { print 1000 * sum / NR " ms" }'
90.7 ms
$ JAVA=/usr/lib/jvm/java-13-openjdk/bin/java; for i in {1..100}; do time -p "$JAVA" -Xshare:on Hello; done 2>&1|grep real|awk 'BEGIN { sum=0 } { sum += $2 } END { print 1000 * sum / NR " ms" }'
106.4 ms
$ JAVA=/tmp/jlinked-java13-jre/bin/java; for i in {1..100}; do time -p "$JAVA" -Xshare:on Hello; done 2>&1|grep real|awk 'BEGIN { sum=0 } { sum += $2 } END { print 1000 * sum / NR " ms" }'
82.3 ms
Interestingly, Java 7 is faster than any of the newer versions. $ JAVA=/usr/lib/jvm/java-7-openjdk/bin/java; for i in {1..100}; do time -p "$JAVA" -Xshare:on Hello; done 2>&1|grep real|awk 'BEGIN { sum=0 } { sum += $2 } END { print 1000 * sum / NR " ms" }'
80.6 ms
...and Java 6 is even faster: $ JAVA=/opt/java6/bin/java; for i in {1..100}; do time -p "$JAVA" -Xshare:on Hello; done 2>&1|grep real|awk 'BEGIN { sum=0 } { sum += $2 } END { print 1000 * sum / NR " ms" }'
62.2 ms
So I appreciate that they're finally working on the startup performance regressions, but they apparently have some way to go before achieving parity with the famously lightning-fast startup time of older Java releases.You can run the JVM on very small heaps. It's just that nobody does.
Nope, the main reason is simply that memory is cheap and plentiful, so there is simply no reason to spend any effort to tune base memory usage when writing the kind of applications Java is typically used for.
We fork a child JVM to run our peakiest jobs for just this reason. Also help keep services up when something OOMs.
That's a pretty irrelevant point, as the current default collector in Sun's JVM does reduce the Java heap based on tuneable parameters. While it doesn't return the virtual address space to the OS, that generally doesn't impact memory consumption on the "current default" OS's. (Certainly there are specialized cases where you might care about that, and for that there are other collectors and other JVM's for that matter.)
> So if you have several peaky memory usage apps, you can't try and get them to elastically negotiate heap size tradeoffs with one another - you need to pack them in with max heap limits manually.
That's simply not true. The default GC does adjust heap size based on utilization, so you absolutely can run peaky apps that manage to negotiate different times for their peaks in a constrained memory space.
> We fork a child JVM to run our peakiest jobs for just this reason.
Well, I guess that's one way to address the problem, but you've unfortunately misunderstood how your tool works.
No, I don't think you have the context.
The peaky process will be killed for OOM by Linux; we explicitly don't want services to die, which they would if they lived in the same process. So, the services live in the parent process, and the peaky allocation happens in the child process. For context, at steady state the services consume about 2GB, whereas the peaky process may consume 30GB for 30 minutes or a couple of hours. We use resource-aware queuing / scheduling to limit the number of these processes running concurrency.
It's true that G1 will, under duress (e.g. under micro-benchmark scenarios with explicit calls to System.gc()), give up some heap to the OS, but it's not what you see in practice, without exceptional attention paid to tuning. Process exit is particularly efficient as a garbage collector though.
G1's heap sizing logic is readily adjustable. The old defaults did rarely return memory to the OS, but you could tune them to suit your needs. Either way, this is no longer accurate an accurate representation of G1's behaviour as the runtime has adapted to changing execution contexts: https://bugs.openjdk.java.net/browse/JDK-8204089
Saving 1gb memory is worth it if it does not cost your developer more than 2 minutes to figure out.
You are trying to project your belief to all Java applications and this simply does not work. There are both good apps and not good apps and there are many metrics to evaluate "good".
If indeed this observed memory bloat is just a matter of poorly written Java apps, then that’s even more interesting. Why does it seem like Java has such a high incidence of poorly written apps relative to other languages? Is it OOP or some other cultural element?
It's because Java has strict memory limits. The limit of a bad C++ app is your machines whole memory (in theory more), so most people never notice if an app continues to leak memory or has weird memory spikes where it needs ten GB instead of one for a minute before it goes back to normal. Java forces you to either look at it or go the lazy route and just allocate more RAM to the JVM. Whatever you choose, you at least have to acknowledge it, so people tend to notice.
Java doesn't have strict memory limits.
Sun's JVM has a setting for maximum heap size, but there are of course lots of other JVM's, and there are lots of other ways to consume memory.
> The limit of a bad C++ app is your machines whole memory (in theory more)
Well, that depends. Most people run operating systems that can impose limits, and you can certainly set a maximum heap size for your C++ runtime that works similarly to Java's limit. You just don't tend to do it, because you're already explicitly managing the memory, so there's no reason for setting a generalized limit for your execution environment.
> so most people never notice if an app continues to leak memory or has weird memory spikes where it needs ten GB instead of one for a minute before it goes back to normal
It also helps that short running apps and forking apps tend to hide the consequences of a lot of memory leaks, and in the specific case of C++, where memory mismanagement is often a symptom or a cause of severe bugs, you tend to invest a lot of time up front on memory management.
Clearly you haven't looked at the memory overhead in scripting languages. ;-) They generally have far more object overhead, but their runtimes are designed for a very different use case, so their base runtime tends to be simple and tuned for quick startup. There are JVMs designed for similar cases with similar traits. It's just not the common choice.
> If indeed this observed memory bloat is just a matter of poorly written Java apps, then that’s even more interesting. Why does it seem like Java has such a high incidence of poorly written apps relative to other languages? Is it OOP or some other cultural element?
Your prejudice is showing in the other possibilities you haven't considered: perhaps memory intensive apps are more likely to be written in Java than other languages? Perhaps Java is more often selected in cases where memory utilization isn't a significant concern?
You can find a preponderance of poorly written apps in a lot of languages... JavaScript and PHP tend to be the butt of jokes due to their notoriety. Poorly written apps isn't a language specific phenomena.
For a variety of reasons that don't involve memory (the design of the language, the thread vs. process execution model, the JIT'd runtimes, the market penetration of the language), as well as some that do involve memory (threaded GC offers a great opportunity to trade memory for faster execution), Java applications are often long running applications that execute in environments with comparatively vague memory constraints, and so the common runtimes, frameworks, libraries, and programming techniques, etc., have evolved to trade memory for other advantages.
But if you look at what people do with Java in constrained memory environments, or even look at the hardware that Java has historically run on, you'll plainly see that what you are observing isn't intrinsic to the language.
Accordingly, shaving 1GB off all your runtimes won't save you much money.
There are more recently developed exceptions to that rule: container packing & FaaS offerings like AWS Lambda. Unsurprisingly, this has lead to the emergence of Java runtimes, frameworks, and libraries that are significantly more miserly with their use of memory (and are also designed for faster start up times as well).
That said, while a lot of people complain about their cloud bill, most places I've seen have payrolls and/or software licensing costs that make their cloud bill look like a rounding error. Sure, when you reach a certain size it is worth trying to squeeze out some extra ducats with greater efficiency, but more often than not, your efficiency concerns lie elsewhere.
Saying "no one tunes it" was deliberately overstating the case. If "everyone thinks the JVM needs 1GB just to run", then yes, "no one tunes it". Neither statement is true, but they both likely reflect some people's context.
That seems like empiricle evidence that other factors are far more important.
However, that was the Sun JDK 1.2 - I wouldn't be so sure that a current mainstream JVM can still be that frugal.
It like I see in US a 4-person family can be made to fit in 5000 sq ft home and even with such space crunch you can have lot of features in home.
For one thing, most software isn't churning on an array of integers. It's allocating and managing various types of objects.
I haven't written a ton of java, but I suspect that the various reflection-heavy frameworks also impose a lot of overhead into the resulting software that isn't necessarily inherently part of Java the language.
My understanding is that it's completely uncached. It couldn't be any less efficient.
Once you have the method reference, actually invoking it isn’t any different than a regular method.
I would imagine that Go’s GC is worse than the tunable JVM ones, but go isn’t powerful enough that one would ever be tempted to program in an abstraction heavy style. While I would argue it takes a lot more work to program in Go ca, day Scala, I think the constraints imposed lead to better software. I for one have never seen a JVM desktop or business app that worked super well. The JVM manages to be a highly optimized efficient platform in which almost exclusively slow, laggy, and memory hungry apps are produced. With that being said, it’s a highly productive platform (if you’re using something besides Java), especially for backend business apps.
this is handled super efficiently by generational garbage collectors.
That is a compliment.
One of the design criteria for Go was to push programmers away from being architecture astronauts whose abstraction heavy style results in having no idea how much work you are hiding in all of the layers. And that design criteria was based on analysis of the actual failure modes of significant software projects.
Therefore the fact that real world Go apps generally manage to avoid that failure more is a testament that they succeeded in this design criteria.
This is parroting anti-intellectual nonsense.
Inheritance as done in most OO systems does likewise.
You will note that the core Go team has a desire to add generics. However they also feel that it is a potential Pandora's box.
Generics are still absolutely necessary for ANY serious programming language. They are required by the DRY principle. Algorithms must not need be repeatedly implemented for every new data structure. To say that generics are astronaut is simply unprofessional.
So Go has absolutely everything a true architecture astronaut needs. In my view, the big difference is cultural and has nothing to do with language features.
Also, the lack of generics often forces you to use reflection to avoid code duplication, which makes for very complex, error prone code.
As an example consider the sort package in Go's standard library. Here's what they had to write in order to generically swap two elements in a []interface{}:
Go in the late 2000s after Amdahl's law had been bitch slapping everyone for 10 years. Network bandwidth increased but laterncy didn't. Processor transistor counts increased but clock speeds didn't. Memory increased but memory bandwidth didn't keep up.
So they chose to create a language where there's no way to get around creating vast amounts of boilerplate. This boilerplate heavy code style results in having no idea of the true logic you are hiding in all of the boilerplate.
Go's GC is tuned for low latency. People hate pause times. They hate it even more than they hate slowness. Go makes a trade-off for short pause times, but in doing so they do sacrifice throughput. That's great for a web server. With a web server, you care a lot about pauses offering a bad experience. A 500ms pause is going to give a customer a bad experience. That's less good for batch processing. Let's say you're running a backfill job that you expect to take 10-15 hours. You don't really care if it pauses for 1 minute to GC every 30 minutes. No one will even know. However, you will know whether it took 10 hours or 15 hours.
Go's GC is meant to be simple for users. I think the only option is "how much memory should I be targeting to use?" That makes it dead simple for users. I think the Go authors are right that too many tuning knobs can be a bad thing. People can spend months doing nonsense work. Worst is that knobs are hard to verify that anything is really different. If you're running a web server, was the traffic really the same? What about other things happening on the machine? Wouldn't you rather be writing software instead?
Go's GC is a non-copying GC. One thing this means is that Go's (heap) memory allocation ends up being very different because Go's memory is going to become fragmented. So Go needs to keep a map of free memory and allocations are a bit more expensive. Java (with GCs like G1) can just bump allocate which is insanely cheap. This is because Java is allocating everything contiguously so it just needs to move a pointer. How does that work once something becomes freed? Java's G1 (the default in the latest LTS Java) will copy everything that's still alive to a new portion of memory and the old portion is then just empty. You kind of see this in Go's culture. Web frameworks obsess about not making heap allocations. Libraries often have you pass in pointers to be filled in rather than returning something.
Go misses out on the generational hypothesis. The generational hypothesis is one of the more durable observations we have about programming - that most things that are allocated die really quickly. C# and Java both use generational collectors by default and they've done way better than what came before. C# and Java don't have as-low pause times as Go, but part of that is that they're targeting other things like throughput or heap overhead more.
Go doesn't need GC as much. Go can allocate more on the stack than Java can and, well, Go programmers are sometimes a bit obsessed with stack allocations even when it makes for more complicated code. Having structs means creating something where you can just have contiguous memory rather than allocating separate things for the fields in your object. Go's authors have observed that a lot of their objects that die-young are stack allocated and so while the generational hypothesis holds, it's a bit different. Go has put a good amount of effort into escape analysis to get more stuff stack allocated.
Java has two new algorithms ZGC and Shenandoah which are available in the latest Java. They're pretty impressive and usually get down to sub-millisecond pause times and even 99th percentile pauses of 1-2ms.
Go's new GC was constrained by the fact that "Go also desperately needed short term success in 2015" (Rick Hudson from Google's Go team) and the fact that they wanted their foreign function interface to be simple - if you don't move objects in memory, you don't have to worry about dealing with the indirection you'd need between C/C++ expecting them to be in one place and Go moving them around. Google's going to have a lot of code they want to use without replacing it with Go code and so C/C++ interop is going to be huge in terms of their internal goals (and in terms of what the team targeted regardless of whether it's useful to you). And I think once they had shown off sub-millisecond pause times, they were really hesitant to do something that might introduce things like 5ms pause times. I think they might have also said that Google had an obsession with the long-tail at the time. Especially at Google, there's going to be a very long tail and if that's what people are all talking about and caring about, you end up wanting to target that.
Go has tried other algorithms. They had a request-oriented-collector and that worked well, but it slowed down certain applications that Go programmers care about - namely, the compiler. They tried a non-copying generational GC, but didn't have a lot of success there.
Ultimately, Go wanted fast success and to solve the #1 complaint people had: extreme pause times (pause times that would make JVM developers feel sorry for Go programmers). Going with a copying GC might have offered better performance, but would have meant a lot more work. And Go gets away with some things because more stuff gets stack allocated and Go programmers try to avoid heap allocations which would be more expensive given Go's choices (programming for the GC algorithm).
I don't think that JVM languages have a programming style that lends themselves to an over-reliance on churning through short-term garbage. Well, Clojure probably since I think it does go for the functional/immutable allocate-a-lot style. Maybe Kotlin and Scala if you're creating lots of immutable stuff just to re-allocate/copy when you want to change one field. That doesn't really apply to most Java programs. And I have covered the way that Go potentially leads to more stack allocations. However, I don't think most people know how their Go programs work any more than their JVM programs and this really just seems to be a "I want to dislike Java" kind of thing rather than something about memory.
Java programs tend to start slow because of the JVM and JIT compilation. Java has been focused on throughput more than Go has (at the expense of responsiveness). That is changing with the two latest Java GC algorithms (and even G1 which is really good). Java is also working on modularizing itself so that you won't bring along as much of what you don't need (Jigsaw) and AOT compilation. Saying that Java is slow just isn't really true, but it might feel true - things like startup times and pause times can inform our opinions a lot. There's absolutely no question that Java is a lot faster than Python, but Python can feel faster for simple programs that aren't doing a lot (or are just small amounts of Python doing most of the heavy work in C).
I mean, are you including Android in "all Java apps"?
Java, C#, and Go are all really wonderful languages/platforms (including things hosted on them like Kotlin). They're all around the same performance, but they do have some differences. I think Go should re-visit their GC decisions in the future, especially as ZGC and Shenandoah take shape, but their GC works pretty well. But there are certainly trade-offs being made (and it isn't around language features that make the platform productive for programmers). I think GC is very interesting, but ultimately Java, C#, and Go all have very good GC that offers a good experience.
You can test my assertion simply by writing a "hello world" in Python and Java.
$ cat Hello.java
class Hello { public static void main(String[] args) { System.out.println("Hello from Java!"); } }
$ cat hello.py
print("Hello from Python!")
$ time /usr/lib/jvm/java-13-openjdk/bin/java -Xshare:on -XX:+TieredCompilation -XX:TieredStopAtLevel=1 Hello
Hello from Java!
real 0m0.102s
user 0m0.095s
sys 0m0.025s
$ time python3 -S hello.py
Hello from Python!
real 0m0.034s
user 0m0.020s
sys 0m0.013s
It's a bit faster if you create a custom modular JRE with jlink: $ /usr/lib/jvm/java-13-openjdk/bin/jlink --add-modules java.base --output /tmp/jlinked-java13-jre
$ /tmp/jlinked-java13-jre/bin/java -Xshare:dump
$ time /tmp/jlinked-java13-jre/bin/java -Xshare:on -XX:+TieredCompilation -XX:TieredStopAtLevel=1 Hello
Hello from Java!
real 0m0.087s
user 0m0.050s
sys 0m0.035stakes hat off in appreciation
If a programmer really cares for performant batch-processing (not sure it's a direction we're going for anymore but), you'll just reuse your objects and dataspace. Understanding Go in order to minimize GC is pretty trivial and the standard library includes packages and tooling exactly for such purposes. Most likely, there are only a few hotspots needing to be tended to like this.
So this sounds like Golang is optimized for what people love about computing: fast response time. Also providing programmers with basic performance "out of the box", which is another good tradeoff for me.
The tradeoff works best when you make simple designs, not huge behemoths. Spending more time reiterating clever designs, rather than jamming the keyboard until done.
> or business app
Practically every large site you use (Google, Amazon, FB, GitHub, Apple, Twitter, and many more) are using Java/JVM in the backend in some non-trivial capacity.
And I think none of them work super well.
If you ignore that stuff and write Java programs in a straightforward way (accepting its verbosity, or possibly using code generation instead of runtime frameworks in places where lisp folks would use a bunch of macros), and avoid unnecessary heap allocations, they can be pretty fast.
[1]: https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sle...
[edit] bloated = much higher memory requirements than I’d expect for what they’re doing (yes I know how to configure jvm memory and such), affects the performance of other software on the same machine more than I’d expect, if running on battery, fairly likely to chew through it while doing nothing.
You have access to Java programs all the time; much of the software you depend on is Java. You're talking about software you run locally on your desktop -- that's something quite different. Desktop applications are a relatively exceptional use of Java these days, although they're still doing better in both performance and footprint than the fashionable JS apps.
> affects the performance of other software on the same machine more than I’d expect, if running on battery, fairly likely to chew through it while doing nothing.
Let me repeat what I said in another comment. Java uses less energy than Go, significantly less than JS, and a whole lot less than Python: https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sle...
Python may require less memory, but you'll overall need more hardware to do the same work as Java.
> > and a wide variety of server software & daemons
Mind, I don't hate Java or anything, I'm just saying, I've been hearing folks tell me it's definitely fast & nimble while watching it not actually be fast or nimble for north of two decades. I don't know where theory and practice diverge, but with significant frequency when it comes to Java, they very noticeably do, and always have. And no, not just on desktop applications, but sure, exceptionally so there.
And god, I'd hope it'd do better than any scripting language whatsoever by practically any performance measure. If not, what's the point? Those are basically never reasonably fast at real work unless they're just tying together C libraries that're doing the heavy lifting.
As for the reference, yes, there are always benchmarks, always papers, always articles, always folks who'll patiently explain why Java must, certainly, be incredibly speedy. Seriously, 20+ years of reading that. Meanwhile when I use Java software it's usually not half as impressive on the performance and respectfulness to hardware fronts as those imply it must be. Again, I am not sure why this is, but it's been so consistently true for so long across so many types of Java program I've used that I don't think I've just managed to somehow use every single one of the worst-written Java programs in existence, or something.
You're going to have to get over some perceptual bias to ever understand the people saying thiese things. It's absolutely true that Java's weak points sit in many of the areas that are directly observable (startup time, UI - Swing etc.) while its strong points mostly sit in the ones that aren't (how fast code runs after 10 cycles of JIT profiling etc). But my suggestion is that its worth doing that because just like a million other things in the world that you can't directly observe, it's very valuable to understand nonetheless their underlying properties.
Just for a reference point, I work in a space where performance is very critical and the code I write takes ages to start (where ages = 1-2 seconds) but then competes head to head with C code in efficiency. It's probably half as efficient as well optimised C code but it brings the huge other benefits that the JVM has so it's ultimately easily worth that tradeoff to me.
Well, at least there's a comparison. It's incredibly annoying when zealots characterize their preferred <whatever> with "high performance", "lightweight", or, worst of all, "powerful".
In addition to the inherent lack of specificity, all of those are relative terms to <something else> but that is always omitted.
- most people judge by their personal realtime interaction with Java which is
generally dominated by startup times (command line / desktop) or UI (Swing)
both of which are Java's weak points. They don't have any way to experience
the efficiency that happens silently under the covers after JIT has occurred
in a server side application so they assume it doesn't exist.
- a huge amount of Java software is enterprise garbage that would be garbage
in every lanuage - Java just happened to win the lottery of adoption there
- the ecosystem DOES encourage sucking in a HUGE amount of libraries. Java
developers will pull in a 10mb dependency because it gives them a just
slightly nicer API to something because doing that is so easy. But the
result is enormous runtime library footprint.
About 80% of this is perception bias, about 20% of it is real IMHO. Java is
amazing to use for high performance work, and it really does compete with
fully compiled languages while maintaining many of the benefits of interpreted
languages (true cross platform, hot-swap code, debugging, etc.)Now, clearly to do more complex processing you'll want to give it more than tens of megabytes of heap, but if you are running Java applications that needs hundreds of megs of dependencies and 1gb of memory then you're probably running some legacy application server.
I agree. I have experimented to make one of my smaller application (~20 Java classes) to Graal native image. After 4-5 days of trying I gave up. Most of the commonly used libraries like JDBC drivers, Kafka, log4j etc work in totally opposite way that Graal Native image like to work.
The way I see it is J2EE all over again. Various vendors will find overall complexity of native images for practical or business Java apps as consulting or business opportunity. We can already see Quarkus, Micronaut and others are trying to offer native images for their framework. This would mean only a set of libraries would be compatible to native image in practical sense.
They also struck a good balance between doing transformations in-memory, and on the database side.
side note: There was _one_ web app that we had to host that wasn't developed in-house. It was absolutely god-awful, badly scaling, websphere based crap (I have no idea if Websphere is crap, but that application sure was bloated and underperforming). Websphere was irritating to deal with. What they did with it was worse. We eventually got the source code from them so we could fix it up, after a bunch of legal wrangles. They had 4 different custom created ORMs in it, each clearly developed by a different dev. Then for reports they never used the ORM, instead they'd make a "one query spits out the final report" query. Those queries would often be 30 - 40 subselects deep. `SELECT thing FROM foo WHERE bar in (SELECT bars IN monkey WHERE ....` and so on down the line.
But given most of the standard corporate devs that I've worked with, it is infinitely easier to write crappy Java and not-bad Go than the other way around.
Sadly.
There is also a fixed overhead. Even if I set the heap maximum to 30MB at the cost of running out of memory during spikes the total memory used by the JVM is still around 130MB.
If you want to run 10 microservices that each do very little then you will need anywhere from 1.3GB to 3.3GB (as soon as you have one traffic spike and then never again). With go you could probably get away with just 300MB and during temporary traffic spikes your maximum will be 2.3GB at most but it is unlikely that all your microservices will spike at the same time so you will need significantly less than 2.3GB.
[1] https://openjdk.java.net/jeps/346 [2] https://openjdk.java.net/jeps/310
No, the blog post doesn't, and I quote: "Java AOT comes at a price.
Huge compilation times (may slow-down your CI/CD pipeline).
Very limited reflection API, limiting the usage of several frameworks or requiring of extra, complex, configuration: JPA, Spring... (This aspect will be treated in a future blog post)."
Unless your code is extremely complicated, or you are running a Jack-of-all-trades monolith, the only way to reasonably consume gobs of RAM is to keep an in-memory database / cache. This trades RAM consumption for speed. This is what e.g. IntelliJ does.
Running a heap larger than you need slows you down in the long term, because GC over a large heap consumes more time and CPU.
It's simply not fair of a comparison to GO when you drag in heavy frameworks like Spring, Hibernate, etc... .
I'd also like to add that even without GraalVM and Micronauts, an app that requires 1 Gig of memory seems to be indicative of its architecture and not the choice of language.
I posted a comment similar to this one recently, but it seems more relevant here.
It also saves evacuating thread-local allocation spaces and running all the barriers.
If Go doesn't survive another decade, I would still be happy about what it triggered.
python -c print 'hello' starts up and shuts down an entire Python interpreter in less than 50 ms on my machine, whereas the equivalent Java program never takes less than 120 ms. That seems pretty sad for a language that's had at least an order of magnitude more resources thrown at it.
edit: and as I mentioned above, the HotSpot VM is specifically, explicitly optimized for long running applications.
Then you must be using an old version of Java.
(Although I see now that the comparison disabled CDS on Java 8, so the actual improvement is perhaps not as large as it seemed.)
The sweet spot for me is using Java with OpenJ9 which has very fast startup time while sacrificing only a bit of top-end throughput.
I would only choose AOT if packaging/deployment were issues with the JIT approach. In the case of Go, AOT is practically free but I prefer not being limited to array/slice, map, and channel generics.
Of course for small scale stuff like a QuickSort implementation, JVM starts are fast-ish, but for a nontrivial service, library load times during boot can balloon quickly depending on your build discipline.
I'm looking at a project that makes "The main benefits of doing so is to enable polyglot applications (e.g., use Java, R, or Python libraries)" sound attractive.
What makes start-up time bad in any interpreted language, including Java, python, and JavaScript, is code-loading time. This time is 0(n), where n is the total size of your app, including transitive dependencies. It takes time to load, parse, and validate non-native code. This time far dwarfs any vm start-up time.
As an experiment, write a hello world node.js app and time it's execution. Then add an import statement on the aws sdk. Don't actually use it. Just import it! When I last measured it, this caused start-up time to go from 30 Ms to something like 300 Ms. The extra time mostly comes from loading code.
For a native app, the binary itself just gets mmapped into memory. Shared library loading is more expensive, but not much, and way less than loading source code or byte code.
The tldr is if you want a fast starting non-native app, you have to shrink the transitive dependency closure your app loads to do it's job. This is easy for toy benchmark apps but can be harder for real apps. It also goes against the philosophy of most devs to rely on third party libraries for everything. For instance, If you care about start-up time, it may be worth re implementing that function you'd normally get from guava or apache commons. You can alternatively use a tool like proguard to shrink your dependency closure.
With a big enough hammer, every screw is a nail.
I remember solaris boxes had little of the startup cost and someone told me they preloaded the java runtime.
To get an intuition for JITs: there are pieces of your code that you know will always be run in a certain way (or mostly run in that way), but it isn't provable at compile time that it will always be that way. JITs can notice that pattern and optimize it (or even provide optimizations for common, but not exclusive paths).
In cases where it is not, and it matters, you probably are not programming it in Java or Go.
[p.s.: point is not that X is better than Y.]
Existing expertise.
Existing tooling.
[1] https://kotlinlang.org/docs/reference/native-overview.html
"The context of enterprise software development" has to be the euphemism of the century.