That's... not really normal, though, and sounds like the exception that proves the rule. For the vast majority of applications, Java will perform better, be easier to develop, and be safer to run, than an equivalent server written in C or C++.
At my previous job we used to run realtime audio through a Java server (RTP streaming). I do remember in the Java 6 days that the GC would need tuning to ensure that GC pauses wouldn't delay audio to the degree that there would be dropouts or perceptual delays. But with Java 8 (and later releases), which came with better GC implementations, those problems just went away. Sure, realtime audio is usually fine with even up to 100ms pauses (or even 200ms, sometimes) -- so this is much more tolerant than your sub-ms example -- but we rarely saw anything even remotely that long with the more modern JVM GC implementations, without really having to tune behavior that much, or at all. Meanwhile, P99 stats for most JVM services were in the low to mid tens of milliseconds, and anything longer was always due to calling out to external services, like relational DBs.
For the rare case like Google's, sure, it's absolutely expected and appropriate to need to use a non-GC'd language instead, at least for some things. For pretty much anyone else, the JVM is more than adequate, or can be made adequate with some reasonably simple GC tuning.
> Many of these "GC is faster!" arguments come from that, but it just ain't true most of the time.
I don't agree. I think it is true most of the time. But I think many people don't think about what they mean by "faster". Faster as in throughput? Sure, a modern, performance-oriented GC (like the JVM's) can very easily beat manual memory management there. Latency? Well, ok, that can be a bit harder, so you need to evaluate things on a case by case basis, and possibly do some GC tuning to get the latencies you need. But even then, you can usually do just as well (or better) on the latency axis as well. Just not always. But I don't subscribe to the "But sometimes..." school of objections. Yes, sometimes some technologies don't work for certain use cases. That's fine. Choose your technology wisely. But in Java's case, it really is just "sometimes". Not most of the time.
Let me reiterate, though: Google is not the common case! By a long shot! It is an outlier, and it's expected that a company like Google will have to deviate from the mainstream to reach its performance targets sometimes. But also consider that (from what I understand) even Google has a ton of services written in Java, and they... work just fine, no?
> In complex distributed systems or other software that manages external resources, GC languages tend to be ill-equipped for the job because they lack explicit resource-management tools like RAII.
Eh. I initially thought of this as a problem, but in practice, I've rarely run into an issue with this sort of thing. Maybe it's because there's still vestiges of the C programmer in me that will always think about memory ownership and lifecycle, even when writing in a GC'd language, but I've rarely had my own issues (or seen issues written by others) where someone has forgotten a `.close()` or `.dispose()` on something. The "try with resources" syntax can help here too, even though IMO it can be kinda cumbersome.
And as much as the Java docs tell you to essentially never override `finalize()`, it can be a useful "last ditch" tool to ensure that any "manual dispose" owned references get cleaned up, and you can also add logging there; I'll often do something like "Got to finalize() without disposing of Foo instances: BUG!". I also appreciate when third-party authors who write `dispose()` methods also do this. It's not perfect by any means, but IMO the convenience of relying on garbage collector rather than manual memory management far outweighs this downside.
Lately I've been writing a lot of Rust, and I'm enjoying the sort of "middle ground" approach, where I don't have to think about memory management as much, but don't have to worry about GC performance, either. Certainly Rust doesn't eliminate these concerns; I still need to think about ownership and object lifetimes, but it's never "oh crap, I forgot to free() something and there's a memory leak", or "oh crap, I tried to use something after free()ing it and crashed", it's more like "ugh, rustc doesn't agree with me that this object lives long enough and refuses to compile it". Annoying, but I'd rather find this out at compile-time than runtime.
But then I'll go back to writing Java or Scala after being in a Rust project for a while, and remember how nice it is to just not have to think about these things.
> ...but a poor fit for systems engineering.
Absolutely agreed. But I would not call writing distributed network servers "systems engineering", even though I do agree that the boundary between systems and applications engineering is indeed fuzzy.