Hacker News new | ask | show | jobs
by amit-bansil 4094 days ago
I've done a fair bit of work interfacing memory managed languages like Java & Python with "unsafe" native code and found that this problem actually crops up often.

The first issue is that finalizers should NEVER be relied upon to clean up system resources the way that you might in a language with deterministic garbage collection like C++. Instead, expect clients of interfaces that use system resources to manually call some 'close' like method. Use finalizers to warn clients that they are leaking. Josh Bloch has an excellent writeup on this[†]:

http://www.informit.com/articles/article.aspx?p=1216151&seqN...

The second thing, as I believe @btown is saying, is that the child object seems to depend on memory that it doesn't own a strong reference to. I bet the whole memory graph here is just a tangled mess. I used to think that that was just the nature of graphs. When I first started working with GC that couldn't handle these kind of cyclical structures without being explicitly told I thought that it was an absurd premature optimization. Then, this wise old hacker peered at my code, smacked me on the back of the head, and grumbled that if I couldn't clearly explain the reference hierarchy it wasn't just the software that would be befuddled, it was too complicated for his old brain as well.

I'm sure that this can't be true, but I am starting to suspect that all the tremendous work that has gone into fast pause-less concurrent GC has been for naught. I wish that when GC encountered a cycle it would just tell the programmer that they now have a leak, maybe even suggest where it could be fixed with one of those neat little IDE quick fixes, and continue chugging along.

tl;dr When you have a hard problem let someone else deal with it.

[†] Granted, Bloch does make an exception for 'native peers'. My understanding of that pattern, however, is that the key is not letting some other object walk off with a reference to your peer, which is exactly what has gone wrong here.

(Aside: Josh Bloch's Effective Java & Java Puzzlers are fantastic. They are worth reading for programmers who use any language.)

4 comments

> the child object seems to depend on memory that it doesn't own a strong reference to

Exactly. Having made similar types of robotics interfaces myself, the memory graph for OP probably isn't as complicated/cyclic as you might think: multiple types of frames (at various stages of visualization processing) look into each others' memory buffers for efficiency. It's probably best modeled as an acyclic directed graph where frame objects reference (many-to-many) memory buffers. The problem is that unless you abstract out those buffers into garbage-collectible objects themselves, it's very tempting just to pass a weak reference (like a pointer from JNI) to the buffer owned by an EarlyStageFrame to the processing code in a LaterStageFrame. Thus the segfault when the EarlyStageFrame is garbage-collected.

That is wonderfully clear. I haven't touched much Java post nio but could the app just use something like java.nio.ByteBuffer to model the raw memory at the bottom of the graph, then have EarlyStageFrames muck with those using static native methods that take the ByteBuffers where needed and then LaterStageFrames playing with the EarlyStageFrames and reaching inside them to get at their ByteBuffers when absolutely needed for performance?

> It's probably best modeled as an acyclic directed graph

My pet theory is that a good representation for most systems should maybe always be an acyclic directed graph?

Poof! There goes finite automata, and therefore regular expressions. :-)
Fantastic examples! I would also add möbius strips, toroidal spaces, and of course, the venerated circle. Sorry, my thoughts above are poorly worded. Perhaps what I am trying to say is that cycles should be created with intentionality, instead of willy nilly spilled throughout your code base. My complaint is really with all the mad spills of Java that I created when I thought that GC was some magic wand that freed me from having to consider the structure of the memory I was manipulating.

Someone should go around sabotaging regular expression parsers and replacing them with PEGs:

http://en.wikipedia.org/wiki/Parsing_expression_grammar

It is a crime against honest words to try to cram so much logic into so few characters.

I once was told that regular expressions were originally designed for creating AIs. The image I have in my mind is that of graduate students in a dungeon somewhere banging on type writers creating some sort of God box.

Maybe if we get rid of regular expressions we'll have one fewer problem. :-)
>I wish that when GC encountered a cycle it would just tell the programmer that they now have a leak, maybe even suggest where it could be fixed with one of those neat little IDE quick fixes, and continue chugging along.

Are you saying that cyclic data structures are always a bad idea? So no one should ever make a doubly-linked list?

Or, to take a case from a game I'm working on: I'm writing a set of GUI controls for the game's UI. There will almost always be at least a few controls on screen, so I have a WindowManager object that lives for the entire duration of the application and does, among other things, tasks like telling all of the controls to draw themselves, determining which control has focus, etc. The controls all have a reference to the WindowManager object (or whatever their direct parent is, once I start supporting more complex controls) so they can implement methods like onClick() { parent->removeGroupOfControls(); }

Even though the WindowManager is persistent, the individual controls come and go quite often. So if I was using a GC (I happen to be doing this in C++) that simply threw up its hands whenever it encountered a cycle, none of the controls would ever get garbage collected, leading to a huge memory leak over time.

Is this a case where you think cycles are appropriate? Or is there a better way to refactor this so it doesn't use cycles? (If someone has ideas for a better design, I would absolutely love to know.)

I cannot imagine creating any sort of complex system that does not, at some conceptual level, contain cycles. What I am suggesting is that the author of a complex system should, whenever possible, untangle as many of these cycles as possible by differentiating between the different classes of references. Think of it like the difference between your directory tree and your symbolic links in unix.

Reg. your example, I have generally found it effective to model UI hierarchies where the top level components refer to their children 'strongly' while the children refer 'weakly' to their parents. I am not familiar with the recent smart pointer stuff in c++ but I believe that these concepts have been cleanly modeled with abstractions such as this:

http://en.cppreference.com/w/cpp/memory/weak_ptr

N.B. A more difficult conundrum that I have encountered when dealing with UI hierarchies is whether or not child components should require a constant reference to their parent passed upon creation or you should be able to 'reparent' or even be in an initial parentless case. My current thinking is that this is a mistake and interactions such as drag and drop are better handled by sending a gesture up to your data layer which then regenerates the component in its new place. This might get ugly, however, if you have a heavily direct manipulation based UI which doesn't really need a data layer on top.

By the way, what has lead you down the road of UI toolkits? They are a fetish of mine. Feel free to shoot me an email if you'd like to connect.

"I wish that when GC encountered a cycle it would just tell the programmer that they now have a leak..."

Wow. Overreact much? :-)

Garbage collection works great for one use case: managing heap memory. Please, let's not return to a universe where you need to free each chunk of memory piecewise. That makes freeing memory O(n) in the number of chunks freed, where modern collectors can free unlimited numbers of objects in O(1) time. Compared to this, malloc and free are a little like keeping score in soccer by tracking every action that does not result in a ball entering a net.

The trouble is finalizers. Finalizers suck. They tie the management of one resource (say native memory or file handles) to an unrelated resource (heap memory). This has the terrible consequences highlighted by this story, plus others. It keeps the native resource tied up until the collector decides to run, for one thing. It also harms performance in secondary ways even when the finalizer isn't running. For example, finalizers typically defeat escape analysis, meaning objects with finalizers can't be stack-allocated.

It's a good idea to use finalizers mostly for detecting invalid final states of objects (like leaking resources), as long as you stay aware that finalizers can slow down your program in unexpected ways.

[These are my personal opinions, not those of my employer.]

>That makes freeing memory O(n) in the number of chunks freed

A GC can't outdo that. For everything the GC frees, it has to do some work to answer the question, "Should I free this or not?" Even if that question can magically be answered with a single CPU instruction, that's still O(n) answers in the number of chunks freed.

I think you're referring to a mark and sweep collector. Java's garbage collector uses better algorithms. The collector never even looks at objects that are not referenced from the root set, and an allocation operation is nothing but a pointer bump. Even Cheney's algorithm from the early 1970s works this way, and collectors have only improved since then.
Except that the OS zeros memory when it's released...
The JVM doesn't release the memory to the OS when garbage is collected; only when the heap shrinks. Any zeroing the OS might do is proportional to the size change in the heap, not to the number of objects freed.
Good point.

However, that brings up a related issue - namely that the JVM requires all (or close enough to all) object memory to be initialized. And given that the amount of memory required to be initialized is at minimum linear w.r.t. the number of objects required to be initialized...

Work done is still O(n) minimum w.r.t. the number of objects allocated regardless.

Yeah, managing memory with malloc and free directly was a nightmare. Back when I worked with Java, however, GC still suffered from unpredictable performance as well as a requirement to have much larger heaps. Are these now mostly solved problems? If not, what do you think of something like this (syntax aside):

http://en.wikipedia.org/wiki/Smart_pointer#C.2B.2B_smart_poi...

My personal experience with similar such systems was that even when performance was not a concern the cost of having to untangle and document my reference structure was well worth the time.

Has any language ever gotten the balance of tradeoffs for finalizers right for strong GC?

> finalizers typically defeat escape analysis

Why is that? I would have thought they would work really well together - if an object such as a file stream doesn't escape you could inline and run the finaliser after the last use of the object goes out of scope.

Or do you mean they defeat current implementations of escape analysis?

It's hard to say. The finalization spec is subtle.

To make this work, the JIT would need to perform escape analysis on the finalizer too (since the finalizer can make an object escape). If that analysis succeeds, then I suppose the object could be stack-allocated and the finalizer called directly. Even then, though, you would have to think carefully about all the subtle ramifications, like the consequences running a finalizer on an application thread instead of the finalizer thread. (What if the finalizer grabs a lock in an attempt to protect itself from the application code? Now this becomes a recursive lock by one thread, and so both the application code and the finalizer can enter it at the same time!)

If this ever became important enough, it's possible the JIT developers could get this right in every case with enough effort invested. I wouldn't count on that happening any time soon.

[These are my personal opinions, not those of my employer.]

If this stuff isn't escaping then the JIT can prove that it isn't using a lock.
Actually, synchronization is not an escape. Objects can be stack-allocated in methods that do synchronization.
Yes I know - so why did you bring it up as an example of something that can prevent EA?
Hi, author here.

As I recall, the memory hierarchy was actually simple--the issue was that it called some structure_free(parent) in C++, which freed the entirety of that part of the hierarchy--the parent and the children; which would obviously cause problems if the children weren't done working.

That said, I'm looking over my backups from that era; I can't quite figure out what real code corresponds to the pseudo-code in the post (I didn't have the real code when I wrote the post).