Guides

6.1 - Memory Management

jNetPcap utilizes a complex native memory managment above and beyond what java memory managment has to offer. Since java memory management exclusively manages the lifecycles of java objects and is in no way extendible to manage other types of memory, native API provides its own memory management that seamlessly blends with java memory managment, object retention through reference counting, and eventually garbage collection.

Here is a 50,000 foot (or meter if you prefer just divide by 3) view of a jNP object that also manages lifecycle of a block of memory that was allocated natively outside of java VM control and realm.

JBuffer buffer = new JBuffer(1024); // Allocate 1K buffer

We created a JBuffer object that allocates 1K of native memory as a block (suposably using C runtime malloc() function call), although the specifics of exactly how the memory is allocated is unimportant in this example and may change in the future, we need to understand that out buffer, is somehow holding a pointer to a native block of memory of size 1024 bytes. JBuffer does not contain byte[] or other type of primitive arrays for storage, this is memory that it references outside of java's control.

If we do nothing with this buffer and let it go out of local scope, it will be eventually garbage collected (GC) by java runtime. The object in its finalizer method, a method that is always called when the object is about to be removed from java's memory, performs a self cleanup, where it checks if there is any memory that it allocated and it is responsible for. In our case that is definitely true and the JBuffer object invokes a special native method that deallocates this native memory. Therefore the filecycle of the java object and the native memory block are tied together. When the java object is GCed by java, then the native memory it holds a reference to is also deallocated. This simple concept provides a mechanism in which millions of various native memory blocks and java objects can be created and everyone of them will eventually be freed. There are no memory leaks as every single byte allocated this way is accounted for and always tied to file cycle of a java object which is eventually GCed.

Accessing native memory from java

To utilize this native memory management mechanism in java is easy. All you have to do is extend JMemory class which maintains the native references and provides the native function calls to allocate and free the memory when object is GCed. It also provides several other useful methods that allow other objects to be "peered" with each other, natively transfer blocks of memory to other memory blocks, byte arrays and java nio Buffers.

Therefore native API and especially native memory managment has to take into account both memory in native land and java objects. This is probably the most difficult part of the public API to comprehend as there are somewhat unseen links between native memory blocks and structures within them and java objects and their accessor methods.

If we look at the class hierachy of our JBuffer we have been working with in our example we see that it extends JMemory class.

JMemory
 |
 +-> JBuffer

So how does JBuffer read and write into this native block of memory? It also provides native methods that read and write out of the memory block. There are quiet a few accessor and setter methods that read/write ints and longs. Read and write data supplied in java's byte[] and many other methods. Another words, all the accessor methods are provided by JBuffer class. These are java methods that have been marked as "native" which require a complementary native function to actually handle a call to such a native method.

Each of these accessor methods also needs a reference to a native memory block on which it is asked to perform some operation. This information is stored as a java property in the JMemory baseclass of the JBuffer object. This is fully utilizing java's polymorphism and thus JBuffer does not need to provide its own implementation on how to actually keep a reference to native memory in java. Note that JMemory class does not provide any individual accessor or setter methods to do anything with the native memory block. It does however provide a few utility methods that can efficiently copy native memory blocks to other native memory blocks. Even into java primitive arrays. The objects that subclass JMemory class provide their own accessor methods depending on the structure of the native block. There several memory related subclasses of JMemory<code> such as our already seen <code>JBuffer class, JStruct, JFunction and several others. These are specialized subclasses of JMemory that provide a standardized way of accessing various native structures and buffers.

Accessing native memory from a properties in JMemory class

When a accessor method in a subclass of JMemory calls on its native counter part to perform the actual work, the native accessor method, or JNI method, is passed a native representation of a java object reference. Through specific JNI calls, the accessor can retrieve java object's fields and invoke its methods. In our JNI accessor method we need a reference to a native memory location that this java object is managing.

Lets say we invoke the following method:

int i = buffer.getInt(0); // Read 4 byte integer at offset 0 in our memory block

We simply want to read an integer value out of native memory JBuffer is referencing and return it into java land. Our native accessor method is provided a JNI reference to our buffer object. Using the black box function call jmemory_as_void(java_object): void * out JNI accessor method retrieves the memory buffer is pointing at. Once we have our memory its a trivial task in C to read 4 byte integer out of that memory location and we simply return it as a return value from our JNI accessor method. This value is properly massaged by JNI library and eventually returned as a java int primitve and assigned to local java variable i.

Other memory types

We have so far looked at memory that was allocated directly and managed by a single object. There are several other modes that memory managment provides.

  1. Block memory - just like when our new JBuffer(int size) used that particular constructor to also allocate memory.
  2. Memory to memory peer - a way that one java object can reference native memory of another object.
  3. Memory to memory proxy - a way that one java object can reference native memory, but through an extra memory reference (a double reference if you will). This allows multiple objects that want to access a single memory resource be reset, when the proxy in-between reference is reset. This proxy mechanism ensures that volatile memory, one that can change or become unsuable outside of our control, can be dereferenced (not neccessarily deallocated) and all the peered objects loose there references.
  4. a java reference - special memory pointer that creates and then eventually deletes a JNI global reference. Global references are explicitly created and need to be explicitely deleted. Global references are references to java objects, and until all global or local JNI references are deleted to a particular java object, that object will never be garbage collected. The purpose of this type of memory is to allow native memory to lock down a java object, so that it never is GCed, as long as the global JNI reference exists to it.