They lack in performance, stability (compatibility), observability (telemetry), productivity, or some combination thereof. They are chosen, of course (especially C#; Go and Rust are far behind), but not as much as Java.
Saying that Go lacks in those is just showing how people are making software those days. It’s just terrifying.
As to Rust - we all, hopefully, agree that it’s great language, but not for some startup making websites or Mongo based, boring backends. It’s great for the stable, system level products.
I don't know what compiler and GC quality has to do with how people are making software these days, and I don't think state-of-the-art optimising compilers and GCs are terrifying at all. Go opts for more traditional, simpler algorithms under the assumption that for many purposes they're good enough. That may be so, but sometimes workloads really are very demanding, and you need the best performance.
and the costs they are willing to pay. Go/Rust just kill everything else (except maybe C++) for performance and resource needs. JVM requires so many resources just to run small apps.
Quite the opposite, and the reason is that you can't extrapolate from small programs to large ones. Low-level languages (like C++) incur some significant overheads as they grow large (because of essential constraints of low-level languages that prevent them from doing certain optimisations that matter mostly in large programs), and these are exactly the overheads the JVM is designed to reduce. In small or short-lived programs, the situation is different, because Java does have some warmup costs and some fixed memory overheads that matter when you're small or short-lived. Go's compiler and GC are pretty basic, and are certainly good enough for smaller things, but don't scale as well to high workloads. Just the other day a colleague tested Caffeine, an old and well-established Java caching library, and Moka, a Rust caching library with the same workload. Caffeine had the same latency as Moka across all percentiles at twice the throughput.
I use Java every day but just to point out that your info about Go‘s GC seems out of date. They switched to Green Tea in 1.25 (I think?) - new GC that even has AVX-512 optimizations. Not sure what you mean by basic about the compiler but it‘s very fast and supports a large set of platforms. That‘s not basic to me.
We are using JDK25 and are considering rewriting parts of our product to Go because of lower memory pressure and faster startup time, i.e., cloud friendly. I actually love both languages.
> I use Java every day but just to point out that your info about Go‘s GC seems out of date.
I'm well aware that Go's GC has improved, but the moving algorithm was designed not just to be fast for a GC, but to be faster than no GC. So Go's new GC is good - for a mark and sweep collector. But it can't compete with a moving collector (the only thing that can is arenas, which are user-friendly only in Zig).
> We are using JDK25 and are considering rewriting parts of our product to Go because of lower memory pressure and faster startup time, i.e., cloud friendly.
Java probably will never have perfect warmup, but it's getting very good - https://openjdk.org/jeps/544 - probably in JDK 28.
As for memory, I think Java's memory strategy is generally misunderstood and I've given a talk about it: https://youtu.be/xr73mR7ii9M The footprint overhead exists to compensate for CPU utilisation when the CPU utilisation is more disruptive than memory usage. The problem is that many Java developers - and I'm not blaming them - don't understand this tradeoff and how to configure the JVM for optimal resource usage, but the great news is that a solution is coming soon, too - https://openjdk.org/jeps/8377305 - also possibly in JDK 28.
So it's very likely that both of these issues will be resolved six months from today, and you'd still get to enjoy better performance and telemetry than all alternatives.
CPU utilization is a red herring. Unless you're doing heavy number crunching (which these days heavily favors GPUs) the practical bottleneck on CPU utilization for large general purpose programs (especially when spanning multiple cores) is memory bandwidth. And moving GC is terrible for memory bandwidth compared to both Go-style concurrent GC (which doesn't have to do bulk moves) and manual memory management.
A couple data points, I like Java but I've seen metrics of container fleets at multiple companies that were memory constrained with low CPU usage sitting around underutilized. The reason in both cases was a bunch of memory-heavy yet CPU-efficient Java processes.
Go's compiler is fast because it doesn't do as many advanced (read: computationally expensive) optimizations as other compilers do. No clue about Green Tea and how awesome it is :-).
Lower memory pressure is certainly a difficult thing to beat Go at, Java (OpenJDK) is probably never gonna get there. You get a lot of other stuff, like better peak performance, instead.
Btw, have you tried Leyden/AOT for better startup times? Curious about your experiences with that.
> Btw, have you tried Leyden/AOT for better startup times? Curious about your experiences with that.
Nope, not yet. It's a good question given that up to now we used to deliver our product only on-premises and Windows Server-only, but this year we are now finally going with the Cloud, which means Docker containers and Linux.
If I remember correctly Leyden required some sort of warm-up and training data collection before being able to effectively execute AOT, right? I need to freshen up my info on that.
I did try GraalVM-compiled Java executables a couple of years ago and they were not bad, but the binaries were quite big (not a showstopper though) and the class-loading issues were kind of a PITA.
> Which specific optimizations are you referring to?
A JIT with speculative optimisation and a moving GC.
There are two constraints in low-level languages that trump any of their performance goals, one technical and one a matter of preference.
The technical limitation is that they must use stable pointers (because they need to be low-level and so having an FFI layer that separates "hardware pointers" from "language pointers", as we have in Java defeats their main purpose). This means that you need to translate data storage or code storage to hardware addresses, and that interferes with both moving collection and with JIT compilation.
The other constraint is that low-level languages value worst-case performance over the average-case and even amortised performance. These languages prefer an operation (e.g. dynamic dispatch) to be slow as long as it's never too slow. With a JIT (and I describe more later), virtual dispatch can be super-fast almost all the time, but occassionally, you'll hit a trap because the speculation was wrong, and then you need to deoptimise and recompile.
> In my experience, this is largely a myth; compared to Rust, you actually get even faster code right away.
We wouldn't be doing it in the first place if it was a myth. In a low-level language, you can get very fast code if you do some manual optimisations, but they don't easily scale as the program grows and evolves, because they're viral. The two most basic examples are dynamic dispatch (which is the most general mechanism, which scales the best in terms of program evolution) and shared heap objects (again, the most general mechanism). These become more common and less easily avoided over time, and they're slow in low-level languages because of the constraints I mentioned.
That low-level languages make it harder and harder to preserve good performance over time as they evolve and grow is a problem familiar to those who've worked for years on large software written in a low level language (as I have). The JVM was designed, among other things, to solve this performance problem in large programs.
> JIT is effective for languages where the source code lacks sufficient information (dynamic typing, where anything can be null).
A JIT can make such languages decently fast, but that's not how it's used in Java. In Java it is used for speculative optimisation, which allows far more aggressive optimisation than an AOT compiler can do. E.g. by default, Java inlines and specialises virtual calls 15 levels deep. An AOT compiler can't do that or its code will explode. We get around it with selective use of templates in C++ (or comptime in Zig), but it has to be selective, and it's viral.
> A JIT with speculative optimisation and a moving GC.
Idiomatic Rust, through its concepts of ownership and borrowing, encourages a pattern where you receive data as an argument or create it directly, perform operations on it, and then discard it via RAII. This bears some resemblance to functional programming. This approach does not apply to buffers of unknown size, which still require heap allocation; unfortunately, Rust lacks automatic buffer reuse. However, such optimization is theoretically possible. The stack is definitely faster than anything else.
> This means that you need to translate data storage or code storage to hardware addresses, and that interferes with both moving collection and with JIT compilation.
You don't need GC if you allocate data on stack. You also do not need to dereference the pointer.
> dynamic dispatch
You mentioned templates. In Rust, traits that are monomorphized - much like templates-are the standard approach; using vtables or `dyn trait` is a relatively rare use case. This stems from the fact that all code is known at compile time and there is no dynamic loading, allowing the compiler to eliminate polymorphism from the code entirely.
> and shared heap objects
This might be considered convenient, but in my view, it also leads to code that is harder to maintain when objects can be modified from multiple places. However, I think that is outside the scope of the current discussion.
> We get around it with selective use of templates in C++ (or comptime in Zig), but it has to be selective, and it's viral.
Yes, monomorphization is the default solution in Rust. It is not always viral either, because when using it, you often define specific types, and they do not spread beyond that scope.
I suppose you could say that the programming style I am talking about is complex, inconvenient, unmaintainable, and so on. What I mean is, assuming this programming style is sufficiently convenient—and perhaps even has its own advantages - then none of the optimizations you listed offer an edge, and the Rust code will definitely be faster.
> The stack is definitely faster than anything else
I have seen it mentioned everywhere, but is this actually true?
I mean, of course it is faster than random cold memory, but is it actually faster than a hot, in-cache part of the heap? It is not special in any other way, AFAIK.
And for what it's worth, what pron mentioned, Java uses a pretty similar structure for initial allocation, a thread local buffer where you just pointer bump. Another thread can then in the background copy still alive objects from this "arena" and then reset the whole thing.
> I have seen it mentioned everywhere, but is this actually true?
Yes, it just adding or subtraction int to stack pointer register. I’m not certain, but the only thing that might be faster is accessing data at a fixed address - that is, global variables.
> However, such optimization is theoretically possible. The stack is definitely faster than anything else.
What you're describing isn't a stack, but an automatic arena, and this optimisation is easier to do in Java. It's easier to do in Java because it requires setting a "current arena" or inlining, both of which Java can do more easily, and then either the arena will be heap allocated (which will be slower in Rust) or associated with the thread, which is not something low-level languages tend to do.
> You don't need GC if you allocate data on stack. You also do not need to dereference the pointer.
Moving collectors don't need to dereference anything (they don't know and don't want to know when an object is "dead"), and stack allocation works in both languages, only, as you pointed out, is not quite general (not every data structure with a known lifetime can be allocated on the stack).
> You mentioned templates. In Rust, traits that are monomorphized - much like templates-are the standard approach; using vtables or `dyn trait` is a relatively rare use case. This stems from the fact that all code is known at compile time and there is no dynamic loading, allowing the compiler to eliminate polymorphism from the code entirely.
Sure, except Java does this automatically, and it can do it more aggressively. Dynamic dispatch is rare in low-level languages because it's expensive in those languages. But it's not easy to avoid as programs get larger. That is exactly one of the problems in large programs that the JVM set out to solve.
> This might be considered convenient, but in my view, it also leads to code that is harder to maintain when objects can be modified from multiple places. However, I think that is outside the scope of the current discussion.
I agree that whether it has downsides is outside the scope of this discussion, but the point is that as programs evolve and grow, the abstractions tend to be more general, and low-level languages suffer from "abstraction cost", where the more general abstraction (which becomes more common over time) is more expensive. Again, this is exactly why large C++ programs suffered from performance issues and what the JVM tried to address.
> Yes, monomorphization is the default solution in Rust.
... and in C++. But it is viral, and Java monomorphises without suffering from "zero overhead abstractions".
The ability to move pointers, both to data and to code, opens up the possibility of using JITs and moving GCs, which are very powerful optimisations. A JIT does impose two further tradeoffs (aside from the need for an FFI layer), though, which are warmup and the possibility of deoptimisation. We can now cache the generated machine code from one execution to another (https://openjdk.org/jeps/544), but the possibility of deoptimisation remains (in fact, it's what enables the aggressive speculative optimisations), which means you gain average (or even amortised) performance at the cost of the worst case.
Anyway, the JVM was designed as a solution for the performance issues low-level languages suffer from as programs grow and/or evolve. It comes with tradeoffs, but those most affect small or short-lived programs.
The thing to remember is that low-level languages are not optimised for performance but for low-level control (i.e. pointers are direct addresses etc.). Such control can translate to good performance when programs are small (see next) but it becomes a practical hindrance to performance when they're large.
> I suppose you could say that the programming style I am talking about is complex, inconvenient, unmaintainable, and so on. What I mean is, assuming this programming style is sufficiently convenient—and perhaps even has its own advantages
That advantage is a performance advantage. The question isn't "does there exist (in the mathematical sense) some program that is fast?" but "how fast is the program we can write within the budget we have?" When programs are small, manual optimisation is practical; when they grow large - not so much. And that's excluding the matter of a moving collector, which is just hard to compete with on speed regardless of program size, unless you use areans, but they're not at all easy to use in most low-level languages except Zig.
> and the Rust code will definitely be faster.
This is true only in the abstract mathematical sense. The reason we don't write programs that we want to be fast in Assembly (which is faster than anything in the same sense: for any program in any language, there exists and Assembly program that's at least as fast) is not because other languages are fast enough, but because in practice the programs we can actually write in the budget we have will be faster than the Assembly programs we could write. Of course, that could change when AI is able to generate perfect low-level code, but when that happens, it might as well generate machine code directly.
> Caffeine had the same latency as Moka across all percentiles at twice the throughput.
Caffeine's next release has roughly 25% higher read throughput, with unchanged write throughput, thanks to fixing a false sharing mistake. That won't be visible in real workloads, but is fun nonetheless (500M reads/s on 8 cores).
I think you have a biased view. The number of stuff written in Rust in the last couple of years has absolutely exploded. For example, I see a lot of projects now that provide SDKs in Rust but don’t bother with Java. And I say this as someone who still writes most of my code ( or tell my LLM to write) in Java.
There's a difference between number of programs and number of LOC (the latter is related to the number of people involved). I am not aware of any SDK targeting the industries I mentioned that "doesn't bother with Java". It's not only a popular choice in those industries, it's not only among the top choices, but it's the top choice by a large margin. Look at wanted ads in those industries to see that. Overall, there are only two languages as popular as Java or more, and they are JS and Python: https://www.devjobsscanner.com/blog/top-8-most-demanded-prog...
Yeah I mean he literally works on Java at Oracle, so may just be a little biased.
Doesn't bother to disclose it of course, because what, you don't check everyone's profile in every discussion to make sure they're not biased? What, you don't just know who every user on this site works for? You dummy you :)
It's disclosed right there in my profile (I don't see your professional affiliation disclosed in your comment; or your profile, for that matter). Of course, I, like other runtime and compiler people, joined the Java team because we wanted to work on the most advanced compiler and runtime tech. I perfectly understand people who want to work on smaller, newer, potentially insurgent products, but I took the chance to work on the cutting edge of compiler and runtime engineering, and Java is where it's at these days (I'm not saying it's the only one, but it's a very small club).
GP's snark is unwarranted, but it's probably good practice to disclose your professional affiliation explicitly in comments related to it, even if you have already disclosed it in your profile.
I was reading your comments on Java, nodding my head, upvoting, without checking your profile and realizing that you're a member of the Java team. Knowing that doesn't mean I now suddenly disagree with you or anything. But while in an ideal world it doesn't matter who's saying something when evaluating it, there's some human factors at play - I'd like to turn up my internal sense of skepticism when dealing with someone, effectively, selling something their salary depends on; even if you're being entirely earnest, it's ultimately a sales pitch, and I feel bamboozled for not recognizing it - that'd make me appreciate transparency.
(FWIW, even though I prefer being coy about my place-of-work, I have no professional relation to this conversation. I've never used Java in my 9-5 and I haven't even really used it in earnest since, like, version 5 back in high school. I think it's always been underrated by the hacker crowd, though!)
I agree that it matters, but whether and how to do it depends on the standard practice in the relevant forum. On HN, it's rare for people to disclose affiliation even in their profile, so I think I'm already better than the norm here on HN in that regard.
> I'd like to turn up my internal sense of skepticism when dealing with someone, effectively, selling something their salary depends on
This is really, really silly. Java is many times beyond the position where its developers need to desperately convince people to use it. This is a person who has unique technical expertise in the area whose credentials are smack dab on their profile, not hidden from you. Their closeness to the domain at hand should make you less skeptical of what they are saying.
While Java can outperform Go in some cases, the situation is very much the opposite when it comes to Rust.
I also don't see the case for stability. Yes, if you're still on JDK 8, it would probably chug on for a couple of years. But we were talking about greenfield projects and newer JDK go EOL much faster. If you want patches, you'll have to run your app to a newer JDK, which may break a couple of things. Rust (within the same edition) or Go (within the same major version) break less than that.
As far as runtime compatibility goes, Rust and Go apps ship with the runtime. This can be better or worse for you, depending on what is your upgrade story, but I don't see a clear winner here. What I would give to Java over Rust is that you will have far fewer dependencies to take care of if you need to upgrade. But the same goes for Go.
For observability, I feel that with Rust you have a bit less that you need to observe (no GC to worry about). Tokio tracing is great, but observability requires a bit more effort. The go observability story is far worse. So Java probably has an edge here, but not something that ever felt like a game changer. My impression is that for most of the enterprise shops that love Java, observability means collecting unstructured log files through NFS and trying to find a needle in the haystack with primitive tools, but I've been out of touch with this world for a couple of years.
Productivity is something that is dead if you are AI-heavy. Sure, many shops are still wary about AI, and I totally get why, but this is a battle that's already been lost. Without AI, I would say I was about 3 to 4 times more productive in Rust than I was in Java, but ramping up that productivity took at least 1 year of practice. It's not time most companies are willing to spend. With AI, this doesn't matter anymore, for better or worse.
I'm not arguing that Java is not chosen often for greenfield projects. It's clearly extremely popular in many circles, especially outside startups and big tech. But I think the reason Java is chosen have little to do with the reasons you've mentioned above and more with organizational preferences.
> I also don't see the case for stability. Yes, if you're still on JDK 8, it would probably chug on for a couple of years. But we were talking about greenfield projects and newer JDK go EOL much faster. If you want patches, you'll have to run your app to a newer JDK, which may break a couple of things. Rust (within the same edition) or Go (within the same major version) break less than that.
Java also breaks very few things. Breaking binary compatibility is a no-go since it's a core promise of the platform. The only thing in the surface language that has ever been changed is the meaning of the underscore as an identifier, as well as the behavior of == in upcoming Project Valhalla.
> As far as runtime compatibility goes, Rust and Go apps ship with the runtime.
Java applications can also be shipped together with the runtime.
> Productivity is something that is dead if you are AI-heavy.
Nevertheless, making constructs available to express intent more clearly should also help LLMs to not go off the rails.
> the situation is very much the opposite when it comes to Rust.
It isn't, and the problem isn't Rust specifically, but all low-level languages. They can offer very good performance (often better than Java) when small. But as they evolve over time, or are very large to begin with, they become much harder to keep performant. This is for pretty fundamental constraints of low-level language that I mention in another comment here, and this performance problem with large programs written in low-level languages was well known before Java even existed. The JVM was designed, at least in part, to address it.
One of the things that drew me to Java (from years of C++, even though I still work in C++ when I work on the JVM) is precisely how it addresses those performance issues we ran into with C++ five years into a project.
> As far as runtime compatibility goes, Rust and Go apps ship with the runtime. This can be better or worse for you, depending on what is your upgrade story, but I don't see a clear winner here.
I wasn't talking about "runtime compatibility" but of overall version compatibility. Java has an unmatched compatibility record - not perfect, but better than anything else (with at least a medium-sized standard library).
> For observability, I feel that with Rust you have a bit less that you need to observe (no GC to worry about).
Memory management is very often a bigger issue without a GC than with a moving GC. Time and again we see Rust or C++ programs spend 30-50% on memory management.
> Productivity is something that is dead if you are AI-heavy.
Really? Have you had AI write a good medium-sized (say 100-500 KLOC) program or maintain one over a long period of time without very close reviews? The only people I've seen who don't know about the ticking time-bomb agents leave in the codebase are the people who don't look.
> With AI, this doesn't matter anymore, for better or worse.
You may be talking about small programs. I agree that for small programs, low-level languages can offer excellent performance, and AI can be okayish, and you can get some observability you can live with, but I'm talking about large programs.
> But I think the reason Java is chosen have little to do with the reasons you've mentioned above and more with organizational preferences.
Those organisational preferences are due to a long record of delivering on the things I mentioned. Java has an exceptionally low "regret factor", i.e. people who regret choosing it five, ten, or fifteen years into a project (which is when the problems usually start).
I don't agree. If anything these newer languages have better tooling and new projects are always built from ground up to support open standards like open telemetry
Open telemetry is about how telemetry data is reported, not how it's collected. It's hard to compete with JFR on the breadth and depth of low-overhead, in production telemetry, built into the standard library and the JVM itself.
I'm not sure what enterprise-level collaboration means. In my experience, "enterprise" usually means: "Let's use tools that are 10 years behind, buggier than average, and have lots of half-baked features, none of which we need".
I'm not sure what kind of tools you mean, but unless you're looking for something that just works exactly the way EJBs do for some mysterious reasons, I don't see why you can't do most "enterprisey" things with Rust or Go. Or Python or TypeScript for that matter.
Yes and that's exactly what modern tooling is missing. Try to develop for node.js 0.2.12 on today's update of Visual Studio Code. See? No enterprise-level collaboration for ya.
Rust is too low-level for typical enterprise app where requirements changes twice a day. You end up spending time and tokens fighting with borrow checker.
> Rust is too low-level for typical enterprise app where requirements changes twice a day. You end up spending time and tokens fighting with borrow checker.
In my experience, you do not spend tokens fighting with the borrow checker anymore, newer models are smarter. But it might not be ideal for a lot of CRUD applications.
> C# is MS product, which is no-go for some folks.
This is 2026, it's not 1996 anymore. .Net works on Linux and Microsoft is as friendly towards open source and open standards as a Big Tech company can be.
If anything, it was Oracle which more recently sued another company for using a JDK alternative. And this was a lawsuit that, if accepted, could have put the entire idea of API compatibility in danger and deal a severe blow to the Open Source movement.
Anyone who is morally bothered by MS but is unfazed by this is probably just mentally stuck in the 1990s.
> Kotlin probably would be the answer.
I love Kotlin, but I'm afraid that's not the case. The conservative organizations that choose Java out of inertia, would keep choosing Java over Kotlin, even if Kotlin is a better JVM language which is facing no downside.
For anyone who doesn't need to be on the JVM or work with JVM tooling, Kotlin doesn't cut it. It doesn't have null pointer dereference problem in theory... Only it does in practice if you're using any Java API that may return null (all these bang-decorated "Platform types"). Generic type erasure can only be overcome in inline functions with reified types. And building and deploying artifacts without docker is still a mess.
I found Kotlin extremely publishing for Java shops in the past, and I've converted multiple departments totaling over hundreds of employees to use Kotlin. But that was before AI. The rationale was simple: Java is an entrenched language that leads to bloated code, slow development cycles and way too many avoidable bugs in productions. Kotlin solves some if these issues, and it's very easy to learn for a Java engineer, while still letting you keep all of your tools and libraries. And as a language (putting ecosystem aside), I find it better than either Go or Typescript, and far more ergonomic than Rust[1].
But all of these arguments die with AI. Rust is just as ergonomic as any other popular language today if you're using an agent, and the fact that an engineer spent their lifetime writing Spring Boot programs in Java you don't have time to let them learn a new stack from scratch doesn't matter anymore.
Sure, there are many companies where letting AI write the code is still not acceptable, but most of these workplaces will accept AI agents sooner than they accept Kotlin.
I feel a bit sad since I like many ideas about Kotlin (especially how amenable it is for making DSLs) but we've lost that opportunity
--
[1] Unless you have to write highly concurrent code without any data races.
> Rust is just as ergonomic as any other popular language today if you're using an agent
Have you worked on large (>500KLOC) codebases with an agent? Not only do you have to be an expert at the language, but even if you're lucky and everything is fine, Java code is likely to be particularly fast by comparison, because the agents aren't very good at manual optimisation, especially as the code grows (they're even worse than humans at that, and humans aren't great at manual optimisation of large codebases, either, which is one of the problems the JVM set out to solve; in fact, agent-written code in a low-level language gets pretty slow well below that size). Oh, and the long build times certainly don't help.
Have you worked on large (>500KLOC) codebases with an agent?
Yes. But keep in mind KLOCs are not easily comparable across languages. Java is notoriously verbose. A 500KLOC codebase in Java would usually be half that size in Rust. If your argument is that large codebases makes life harder for agents, you should go with a less verbose language.
I'm not sure what "manual optimization" means (isn't it a bit of an oxymoron when the agent does it?), but if your agent has the proper tools (e.g. ast-grep, rg, semble) it can deal with large codebases. Would the agent create slop? Yes. But it wouldn't be worse on the slop that humans created on every moderately-sized Java project I've worked on.
> in fact, agent-written code in a low-level language gets pretty slow well below that size
I've never seen this happening. I've seen agents writing suboptimal Rust code (e.g. copies instead of Cow). But while this occassionally happens with Rust, I've never seen an agent optimizing for Java where necessary (e.g. using object pools to avoid GC churn). Java is not magic.
> is notoriously verbose. A 500KLOC codebase in Java would usually be half that size in Rust
Lol, no way. Especially that rust is pretty verbose all things together (which makes sense, given it's a low level language - ergo you have to literally express more things about the code)
The iPhone launch was met with a lot of derision and eye-rolling despite that being the result of a half decade "effort to make themselves distinct from the competition".
Those events are rare, the opportunities to really shake things up are fleeting, as everyone is driving forward with relentless, incremental progress that quickly closes up all but the biggest gaps.
What could they possibly do that wouldn't be slammed as derivative?
reply