With Great Garbage Collection, Comes Great Responsibility
Recently, I’ve been doing a lot of C and C++ thanks to my coursework. Having come from very high-level languages like Java and R, I was unprepared to deal with some of the low-level processes like memory management. Over this past month, I’ve realized how much I had taken for granted when it came to Java. I never had to worry about memory addresses and dereferencing, or allocating and freeing memory. But a few months back, I encountered a rather niche problem that made me reconsider my unwavering love for Java’s garbage collection.
Over the summer, I had the amazing opportunity to work with Soar, a cognitive architecture, and its markup language (SML) for Java. (To find out more about Soar and what I did, check out my projects page!) In order to use SML, a couple dynamic libraries have to be loaded in using the JNI. On Mac, you have to point the library path VM argument to the location of the libraries. At that point, I didn’t know much of anything about shared libraries or C/C++, so I simply did what made it work without really knowing why it worked. As you could imagine, this led to some issues down the line.
In my restaurant classifier project, I needed to create a Soar agent for my model. This agent needed to be initialized once, with rules being loaded in upon initialization. To create an agent, you must use SML’s Kernel class. See below for a demonstration. Logically, I included this process in my constructor. Here’s an idea of what my original code looked like:
class Model implements IModel {
private final Agent agent;
... other fields ...
public Model() {
Kernel kernel = Kernel.CreateKernelInCurrentThread(true);
agent = kernel.CreateAgent("name");
... other code ...
}
}
Pretty straightforward right? I thought so too, until I ran the code. I kept getting a fatal error (which I now know probably translates from a seg fault in the native code) due to a reason I’ve never seen before. It was essentially breaking whenever I tried to call a method on the agent. The only time I saw a similar error in the past was due to inefficiency, so I spent hours trying to optimize to no avail.
Conveniently enough, I had a scheduled meeting with my supervisor/professor relatively soon after discovering this weird bug. We talked about it and tried a few things: the IDE debugger was no use; we tried running the main method from the terminal with only the classpath, but no luck; removing certain method calls that seemed to be trouble spots in the core dump had no effect either. It seemed hopeless until he suggested the following change:
class Model implements IModel {
private final Kernel kernel;
private final Agent agent;
... other fields ...
public Model() {
kernel = Kernel.CreateKernelInCurrentThread(true);
agent = kernel.CreateAgent("name");
... other code ...
}
}
We added one line of code - specifically, the kernel became a field of the model. With a change this tiny, you can imagine my disbelief when everything ran perfectly fine. Although we’re still not completely sure why this worked, we’re convinced that it had something to do with Java getting rid of the Kernel object once we exited the constructor, perhaps in the form of garbage collection. At a high level, the kernel’s job in Soar is to keep track of all the agents, and unsurprisingly, if the kernel magically disappears (or gets garbage collected), everything falls apart. The underlying library code was probably trying to access a kernel that no longer existed, hence the seg fault/fatal error.
Although I’m not a fan of mallocing and freeing and deleting, I know how important memory is for all programs. Thanks to this experience, I understand how relevant these topics are, even while working in higher level languages; this also addresses the underlying importance of truly understanding what’s happening in your code. So as painful as low-level languages can be, I’m now a firm believer that everyone should study them at some point.