> It's hard to fault Java at this point: arguably the best runtime of them all from a perf perspective
The JVM is a massive memory sink compared to the tiny (actual) runtime in Go or the total absence of a VM or runtime in Rust or Zig. I allocate at least 5x more memory for Java tasks, but sometimes its more.
Java is great compared to scripting languages or writing raw C, but not compared to modern compiled languages from a syntax (null, thrown exceptions, inheritance, etc..), library selection, CVE/security, memory usage or raw computer power perspective.
Many times the "massive" memory sink is because people give the VM more memory than it actually needs (or they use the default, which is to take up to 25% of RAM whether or not it's really needed for the required performance). I.e. people will say a process "takes" 1gb, when the same program could run just as well if they tell it to use, say, 300mb. The more memory, the faster it could run. The GCs offered by the JVM are more advanced than Go's by a couple of tech generations (the Go-like GC was removed from the JVM when it was superseded by two generations of newer GCs), and memory consumption is already going down with compact object headers [1] and will go down even further with Valhalla.
All in all, Go may take up a little less memory for similar performance, but the JVM is more flexible (and more observable), offering you better performance if you need it. And the footprint will continue dropping, as it has done for years [2].
C++, Zig, or Rust are not really an apples-to-apples comparison. Sure, they take up significantly less RAM, but their performance is more expensive per unit of effort, and it's not a one-time cost, either. It's a permanent tax on maintenance, not to mention Java's superb observability.
Don't get me wrong -- I'm a fan of Zig, and C++ is the language I still program in most, but you can't really compare them to Java. Java makes you pay for certain things -- especially in RAM -- but you do get your memory's worth in exchange, and in a way that translates to pretty significant time and money. If you can't spare the memory as you're running in a constrained environment that's one thing, but if you can, it's all a matter of what you want to use it for: do you want to use it all just on data, or do you want to spare some to reduce maintenance costs/increase development speed and improve performance?
BTW, I'm not sure what is actually meant by a "lack of runtime" for C++/Rust/Zig. They all have standard libraries, just like Java. Rust even has a (crude) GC in its runtime that most Rust programs use (and, like Java, it compiles down to a VM). I think what people mean is that compilation to native is typically AOT rather than JIT, but that has both pros and cons.
I'm interested by your characterisation of Rust. I assume 'crude GC' is a reference to Rc/Arc, but I would be interested to see some statistics for the claim most programs written in Rust use them extensively. Also, Rc/Arc arent a part of any Rust 'runtime', but rather the standard library, and are not available when they eould not be suitable for the target, e.g. UEFI. Moreover, rustc compiles to LLVM IR, but LLVM is not a JVM/CLR VM, and rustc is not the only Rust compiler (though the others are admittedly not production-ready yet).
> I assume 'crude GC' is a reference to Rc/Arc, but I would be interested to see some statistics for the claim most programs written in Rust use them extensively.
Yes, Rust's GC is used through Rc/Arc, and I never said it is used extensively by most programs, only that most programs do use it. It is because it is not used extensively that it can be crude and designed to minimise footprint rather than offer good performance.
> Also, Rc/Arc arent a part of any Rust 'runtime', but rather the standard library
What's the difference between a standard library and a runtime? In the three decades I've been programming, they've been used interchangeably. A language runtime means some precompiled-code that programs use but is not compiled directly from the program.
> rustc compiles to LLVM IR, but LLVM is not a JVM/CLR VM
I never said that LLVM was a JVM -- these virtual machines have very different instruction sets -- but like a JVM, LLVM is a VM, i.e. an instruction set for an abstract machine.
Now, it is true that Rust is typically (though not always) compiled to native code AOT while Java code is typically (though not always) compiled to native code JIT, but I don't understand why that difference is stated in terms of having a runtime. One could have an AOT-compiled JVM (and, indeed, that exists) as well as a JIT-compiled LLVM (and that exists, too).
It is also true that Rust programs can be compiled without a runtime (or a very minimal one) while Java programs can choose to have more or less in their runtime, but even the most minimal runtime is larger than the most minimal Rust runtime.
> What's the difference between a standard library and a runtime? In the three decades I've been programming, they've been used interchangeably.
First of all, you're right. But despite its definition I think people tend to look at it differently.
A runtime is generally thought of as a platform on top of which your code runs on; it needs to start first, and it manages your code. Or perhaps it runs in a side thread.
A language that has a runtime is hard to embed into something via just the C ABI, because a function call wouldn't use just the standard platform calling convention; it would have to start that runtime, perhaps marshal the parameters into something supported by that runtime, and then finally the runtime runs your function's code.
Take for example cgo, for which you'd need to start the garbage collector first (among other things), hence why the cgo FFI is expensive. Take as another example an async Rust function, which would require e.g. a Tokio runtime to be started first. Another example is Java, for which you'd have to start the whole JVM first.
A language that has no runtime, or a minimal runtime, can be called via the C ABI directly. All the function needs is to follow the calling convention, and then its code starts running immediately.
This is just my opinion of other people's opinions, I may be wrong.
> A runtime is generally thought of as a platform on top of which your code runs on
That's not a well-defined thing.
> A language that has a runtime is hard to embed into something via just the C ABI, because a function call wouldn't use just the standard platform calling convention
But Java can be embedded in native code or embed native code. It has a specified FFI in both directions.
> Take for example cgo, for which you'd need to start the garbage collector first (among other things), hence why the cgo FFI is expensive.
Well, Java doesn't quite work like that, and its (new) FFI is free in most important cases (i.e. same as a non-inlined C-to-C call). Also, "starting the garbage collector" is not well-defined. What "starts" Rust's garbage collector?
I understand what you're trying to get at, but things aren't so simple. There are, indeed, differences especially around JIT vs AOT, but it's not as simple as saying "having a runtime" or not, nor is everything similar in all languages (Rust and C don't work the same vis-a-vis the C ABI, and Java, C#, and Go interop with native code are all quite different from each other).
> A language that has no runtime, or a minimal runtime, can be called via the C ABI directly.
A Java program can easily expose any method via the C ABI to be called directly if the process has been started from Java -- i.e. it's easy for Java code to give native code a function pointer to Java code. Going the other way, i.e. embedding Java in a C program, is somewhat more involved, but even C++'s interop with C, not to mention Rust or Zig, is not always straightforward. Like in Java, certain functions need to be marked as "C-interopable".
> But Java can be embedded in native code or embed native code. It has a specified FFI in both directions.
Most languages have an FFI, but I am talking specifically about the C ABI and the platform calling convention; or more specifically, about starting from scratch, and what is necessary to do from there until your code can finally run.
Anything more complex than the C ABI is what makes people say there is a runtime. It's some layer between your code and the other language's code, inserted there by your language. There's usually no way to remove it, and if there is, it severely limits the language features you can use.
> What "starts" Rust's garbage collector?
Nothing; it doesn't start unless the function itself wants to start one, and the function can choose which one to start, through your code (rather than what the language's required runtime provides).
> A Java program can easily expose any method via the C ABI to be called directly if the process has been started from Java
In that case, the runtime has already been started, and is being reused.
> Going the other way, i.e. embedding Java in a C program, is somewhat more involved
That part is the most important part, and is generally why people say Rust has a minimal runtime; it can be embedded with very little setup. The code you write starts executing almost immediately. Java calling C may add a management layer on Java's side, but C/C++/Rust/Zig/etc need very little (hence, minimal runtime).
> Anything more complex than the C ABI is what makes people say there is a runtime.
But Rust (or Zig, or C++ for that matter) don't use the C ABI, either, except for specifically annotated functions.
> In that case, the runtime has already been started, and is being reused.
True, but I'm trying to say that the notion of "starting" the runtime (or the GC for that matter) is not really well-defined. HotSpot does need to be "started", but, say, Graal Native Image, which is sort of an AOT-compiled, statically linked JVM, isn't really "started".
> Java calling C may add a management layer on Java's side, but C/C++/Rust/Zig/etc need very little (hence, minimal runtime).
In some implementations of Java this may be the case in some situations, yes. I would go further and say that that's the typical case, i.e. if you want to embed the stock HotSpot, you will need to call some initialisation functions.
If that's what's meant by "runtime", then it's mostly correct, but it's more an implementation detail of HotSpot. Even without it there will remain more important differences between C++/Zig/Rust/C and Java, and this matter of "runtime" is not the most interesting aspect. For example, that Java is usually compiled JIT and Rust is usually compiled AOT is a bigger and more interesting difference.
> But Rust (or Zig, or C++ for that matter) don't use the C ABI, either, except for specifically annotated functions.
Not only that, but Rust, C, Zig also require some setup before their `main()` can start as well.
That is why people say they have a "minimal runtime", rather than "no runtime". There is still a bit of setup there, without which the languages cannot function, or can only function in a limited mode.
FWIW Cgo FFI is expensive not because of GC but because Go uses virtual threads (goroutines) and prioritizes the simplicity of runtime implementation. The lower bound of FFI cost in .NET is roughly equivalent to direct not-inlined calls in C, despite the GC support.
> I never said it is used extensively by most programs
True, but to make use of Rc/Arc in Rust comparable to the use of GC in Java, almost evrery value would have to be wrapped in one, something I would think is quite rare.
> only that most programs do use it
I would still be interested in seeing statistics for this though. I have only ever used Arc once, and it was only to share a Mutex containing the program's database across threads.
> What's the difference between a standard library and a runtime?
I would say there are three main differences:
- A runtime is (usually?) not optional - see languages like C#/Java/Python that require some sort of higher 'power' to manage their execution (interpreting/JITing code, GC management, etc), or crt0 - compared to a standard library which is just some extra code - see the C standard library function strlen()
- A standard library can generally be implemented in the language itself. I think this is where the distinction starts to get a little fuzzier, with languages like Roc (its standard library is implemented in Zig), and Haskell (I would assume much of the side-effecty code like the implementation of IO is in C)
- The purpose of a standard library is generally to provide 'helper' code that you wouldn't want to write yourself, e.g. Vec<T>, HashMap<T>, filesystem functions, etc. On the other hand, the purpose of a runtime is, per the first point, to manage the execution of the language, etc.
> I never said that LLVM was a JVM
True again, my point was worded badly. I didn't mean to suggest you thought LLVM was a JVM, rather to draw a distinction between LLVM and a Java/.NET style VM. LLVM used to stand for Low Level Virtual Machine, and the JVM has instructions like instanceof (which, fairly obviously, checks if a reference is an instance of the named class, array, or interface type). They operate at quite different abstraction levels, and the JVM is a lot more similar to the CLR.
> Now, it is true that Rust is typically (though not always) compiled to native code AOT
I would be genuinely interested in finding about a JIT compiler for Rust.
EDIT: it's worth me mentioning none of this is backed by any formal education (I'm 18), so it is very possibly wrong.
I agree with you on the memory sink side. For some kinds of applications the combo of startup time and memory consumption make it unsuitable - think of many small services or short-lived functions. Yes, Graal and such are amazing innovations, but they're very incomplete and poorly supported solutions and not something that's a good plan to bank your future on. This has limited our cloud deployment options to a significant degree, and I know I'm not alone.
Being a memory hog was not such a big deal in the pre-cloud era, but we pay real money for that (far too much!) these days, and leaner approaches are worth a lot in real monetary terms.
I wish push back on some of your other points. Java has evolved quite a lot lately, with improved syntax, much improved runtime (Loom!), better GCs, lots of libraries, etc. The community is a bit stale though, and it's viewed as the Oldsmobile of languages. But that old language still has a skip in it's step!
These other languages do not have the programing affordances of Java (generics, easy memory safety). Tooling is a big factor in favor of Java - IDEs, debuggers, observability etc. Also, the quality and availability of libraries is also a debatable point (although, it depends on the application at hand),
I actually disagree with all of these points and I write Java at work.
Memory safety isn't possible in Java (unlike Rust).
Tooling: they all have debuggers and observability tooling (pprof, pprof-rs, etc..). Rust even has zed now.
Libraries: Rust has really high quality packages. On the flip side, Go has almost everything built into the stdlib. Java requires a ton of third party packages of varying quality and guarantees even for basic things like logging or working with JSON. You have to constantly be on the lookout for CVE's.
In fact, 75% of the cloud-native foundations projects are in Go. K8s and docker are Go. Go is much more web-app or microservice focused than Java is actually. Most Java apps are deployed into the cloud using Go.
Meanwhile, Zig makes using the universe of C/C++ code easy.
I highly recommend you try Zig, Rust, or Go out. They weren't created out of ignorance of Java, but because Java and C++ had areas that could be improved.
Zig or Rust neither attempt to nor have any chance of directly competing with Java or any other high level language, other than on the margins (they have a hard-enough time competing with C++, which is unfortunate, because both are better than C++ -- Rust slightly so and Zig significantly so, IMO). They are for low-level development and target a domain where both the calculus of cost and value is vastly different from the high-level application space.
It is usually just as easy to write a program in a low level language as it is in a high-level one, and this is true not only for Zig and Rust, but also for C++. Even in 1998 it was just as easy to write a program in C++ as it was in Java. But the maintenance later -- when you have a large 10-year-old codebase -- is significantly more costly, and necessarily so. In a low-level language, i.e. one where there's more direct control over memory management, how a subroutine uses memory may affect its callers. Whether the correct use in the caller is enforced by the language, as in Rust, or not always, as in Zig, changes in low-level languages require touching more code than in high-level ones, and can have a bigger impact.
The low-level domain is, and will continue to be, extremely important. But the market share gap between low-level and high-level languages/domains has only grown over the past decades, and there are no signs of the trend reversing.
Now Go is a different beast altogether, and is a high level language. But it has both pros and cons compared to Java. The tools it includes in the SDK are more user-friendly, but they, like the language, are less flexible and more limited than in the Java world. Nevertheless, the out-of-the-box experience is nicer, something we should definitely improve in Java, but you pay for that simplicity later in lost flexibility. Performance also isn't quite as good as Java's, and neither is observability.
Java tends to prefer using available memory, before it has to clean it. This results in memory usage growing more than it actually needs.
Services often run in containers, so this is less than a problem today than it was before, because the allocated memory to the container is fixed anyway.
Try configuring the JVM to only use 75% of the container memory. In many cases, it will trigger an early GC and run better with smaller memory instances
There are of course some overhead for objects compared to primitives, but the Valhalla project is working to reduce the difference significantly on many cases.
You can also reduce memory usage and startup time by compiling to a binary image
The JVM is a massive memory sink compared to the tiny (actual) runtime in Go or the total absence of a VM or runtime in Rust or Zig. I allocate at least 5x more memory for Java tasks, but sometimes its more.
Java is great compared to scripting languages or writing raw C, but not compared to modern compiled languages from a syntax (null, thrown exceptions, inheritance, etc..), library selection, CVE/security, memory usage or raw computer power perspective.