The kind of bugs that GC addresses seem (at least to an external
observer) the kind of things that a programmer that knows well his
language, libraries, concepts, idioms, etc, wouldn't do. But I could
be wrong: is manual memory handling intrinsically complicated?
Coming from the C end which makes memory management about as manual and pronounced as possible so that we're comparing extremes (C++ mostly automates memory management without GC), I'd say "not really" in the sense of comparing to GC when it comes to leaks. A beginner and sometimes even a pro may forget to write
free for a given
malloc. It definitely does happen.
However, there are tools like
valgrind leak detection which will immediately spot, on executing the code, when/where such mistakes occur down to the exact line of code. When that's integrated into the CI, it becomes almost impossible to merge such mistakes, and easy as pie to correct them. So it's never a big deal in any team/process with reasonable standards.
Granted, there might be some exotic cases of execution that flys under the radar of testing where
free failed to be called, perhaps on encountering an obscure external input error like a corrupt file in which case maybe the system leaks 32 bytes or something. I think that can definitely happen even under pretty good testing standards and leak detection tools, but it would also not be quite so critical to leak a little bit of memory on something that almost never happens. We'll see a much bigger issue where we can leak massive resources even in common execution paths below in a way that GC can't prevent.
It's also difficult without something resembling a pseudo-form of GC (reference counting, e.g.) when the lifetime of an object needs to be extended for some form of deferred/asynchronous processing, perhaps by another thread.
The real issue with more manual forms of memory management is not leaks to me. How many native applications written in C or C++ do we know of that are really leaky? Is the Linux kernel leaky? MySQL? CryEngine 3? Digital audio workstations and synthesizers? Does Java VM leak (it's implemented in native code)? Photoshop?
If anything, I think when we look around, the leakiest applications tend to be the ones written using GC schemes. But before that's taken as a slam on garbage collection, native code has a significant issue that's not related at all to memory leaks.
The issue for me was always safety. Even when we
free memory through a pointer, if there are any other pointers to the resource, they will become dangling (invalidated) pointers.
When we try to access the pointees of those dangling pointers, we end up running into undefined behavior, though almost always a segfault/access violation leading to a hard, immediate crash.
All those native applications I listed above potentially have an obscure edge case or two which can lead to a crash primarily because of this issue, and there are definitely a fair share of shoddy applications written in native code which are very crash-heavy, and often in large part due to this issue.
... and it's because resource management is hard regardless of whether you use GC or not. The practical difference is often either leaking (GC) or crashing (without GC) in the face of a mistake leading to resource mismanagement.
Resource Management: Garbage Collection
Complex resource management is a difficult, manual process no matter what. GC can't automate anything here.
Let's take an example where we have this object, "Joe". Joe is referenced by a number of organizations to which he is a member. Every month or so they extract a membership fee from his credit card.
We also have one reference to Joe to control his lifetime. Let's say, as programmers, we no longer need Joe. He's starting to pester us and we no longer need these organizations he belongs to waste their time dealing with him. So we attempt to wipe him off the face of the earth by removing his lifeline reference.
... but wait, we're using garbage collection. Every strong reference to Joe will keep him around. So we also remove references to him from the organizations to which he belongs (unsubscribing him).
... except whoops, we forgot to cancel his magazine subscription! Now Joe remains around in memory, pestering us and using up resources, and the magazine company also ends up continuing to process Joe's membership every month.
This is the main mistake which can cause a lot of complex programs written using garbage collection schemes to leak and start using up more and more memory the longer they run, and possibly more and more processing (the recurring magazine subscription). They forgot to remove one or more of those references, making it impossible for the garbage collector to do its magic until the entire program is shut down.
The program doesn't crash, however. It's perfectly safe. It's just going to keep hogging up memory and Joe will still linger around. For many applications, this kind of leaky behavior where we just throw more and more memory/processing at the issue might be far preferable to a hard crash, especially given how much memory and processing power our machines have today.
Resource Management: Manual
Now let's consider the alternative where we use pointers to Joe and manual memory management, like so:
These blue links don't manage Joe's lifetime. If we want to remove him from the face of the earth, we manually request to destroy him, like so:
Now that would normally leave us with dangling pointers all over the place, so let's remove the pointers to Joe.
... whoops, we made the exact same mistake again and forgot to unsubscribe Joe's magazine subscription!
Except now we have a dangling pointer. When the magazine subscription tries to process Joe's monthly fee, the entire world will explode -- typically we get the hard crash instantly.
This same basic resource mismanagement mistake where the developer forgot to manually remove all pointers/references to a resource can lead to a lot of crashes in native applications. They don't hog up memory the longer they run typically because they will often outright crash in this case.
Now the above example is using a ridiculously simple diagram. A real-world application might require thousands of images stitched together to cover a full graph, with hundreds of different types of resources stored in a scene graph, GPU resources associated to some of them, accelerators tied to others, observers distributed across hundreds of plugins watching a number of entity types in the scene for changes, observers observing observers, audios synced to animations, etc. So it might seem like it's easy to avoid the mistake I described above, but it's generally nowhere near this simple in a real-world production codebase for a complex application spanning millions of lines of code.
The chance that someone, some day, will mismanage resources somewhere in that codebase tends to be quite high, and that probability is the same with or without GC. The main difference is what will happen as a result of this mistake, which also affects potentially affects how quickly this mistake will be spotted and fixed.
Crash vs. Leak
Now which one is worse? An immediate crash, or a silent memory leak where Joe just mysteriously lingers around?
Most might answer the latter, but let's say this software is designed to be run for hours on end, possibly days, and each of these Joe's and Jane's we add increases the memory usage of the software by a gigabyte. It's not a mission-critical software (crashes don't actually kill users), but a performance-critical one.
In this case, a hard crash that immediately shows up when debugging, pointing out the mistake you made, might actually be preferable to just a leaky software that might even fly under the radar of your testing procedure.
On the flip side, if it is a mission-critical software where performance isn't the goal, just not crashing by any means possible, then leaking might actually be preferable.
There is kind of a hybrid of these ideas available in GC schemes known as weak references. With weak references, we can have all these organizations weak-reference Joe but not prevent him from being removed when the strong reference (Joe's owner/lifeline) goes away. Nevertheless, we get the benefit of being able to detect when Joe is no longer around through these weak references, allowing us to get an easily-reproducible error of sorts.
Unfortunately weak references aren't used nearly as much as they probably should be used, so often a lot of complex GC applications might be susceptible to leaks even if they're potentially far less crashy than a complex C application, e.g.
In any case, whether or not GC makes your life easier or harder depends on how important it is for your software to avoid leaks, and whether or not it deals with complex resource management of this sort.
In my case, I work in a performance-critical field where resources do span hundreds of megabytes to gigabytes, and not releasing that memory when users request to unload because of a mistake like the above can actually be less preferable to a crash. Crashes are easy to spot and reproduce, making them often the programmer's favorite kind of bug, even if it's the user's least favorite, and a lot of these crashes will show up with a sane testing procedure before they even reach the user.
Anyway, those are the differences between GC and manual memory management. To answer your immediate question, I would say manual memory management is difficult, but it has very little to do with leaks, and both GC and manual forms of memory management are still very difficult when resource management is non-trivial. The GC arguably has more tricky behavior here where the program appears to be working just fine but is consuming more and more and more resources. The manual form is less tricky, but is going to crash and burn big time with mistakes like the one shown above.