6.1.1 - Memory structures

Printer-friendly

Lets talk about what exactly does the java object hold reference to when we say that it has a reference to a memory block. In a sense that is true, the address that is stored within a java JMemory based object is an address of a memory location. In most cases though, the address is not the start of memory the user can use for its purposes.

The address java object points to is a native memory descriptor or memory header. Every memory block allocated is prefixed with a memory header (not to be confused with protocol headers) that describes that particular memory block. When in this example we allocate 1024 byte of memory:

JBuffer buffer = new JBuffer(1024); // Allocate 1024 bytes

Native memory manager actually allocates the buffer slightly larger to accommodate a special header that is stored at the start of the buffer. This header contains information about the memory block such as its size (so accessors can make sure requests are within boundaries) and certain flags. For example access to memory can be restricted such as making an entire memory block READ_ONLY or not WRITABLE.

Another black box method is provided by the native API, to retrieve the actual memory address of where our object can read and write data to jmemory_jni(java_object): char* where given our java object, it returns the block of memory it references as a C char *. If memory which it references is invalid, not assigned yet, or simply access permissions don't allow (jmemory_jni assumes read-write permission to memory block), the this method returns a NULL indicating something is not right.

There are numerous native API memory management functions which allocate memory and assign it to java objects, change allocated memory size or resize the memory block, or free and cleanup the java object. There are also functions which allow additional memory allocations to take place, but tie that new memory with the lifecycle of a single java object.

The there are memory management functions which allocate other types of memory headers such as peers, proxies and jreferences which reference java objects from native land. For clarity's sake, we will at least for now focus on the all important block memory type. First, we take 1 tiny step back and look at jmemory_t structure which is the basis of all memory model types.

The jmemory_t structure

Every allocated block, peer, proxy or jreference, starts with a jmemory_t structure. This common structure provides a mechanism by which memory blocks can be handled generically. Here is the structure's definition in detail:


typedef struct jmemory_t {
uint16_t type; // memory type
uint16_t flags; // User specific flags
struct jmemory_t *next; // Memory segment chain
} jmemory_t;

The type field holds an integer type value. Possible values are BLOCK, PROXY, PEER, JREF or UNUSABLE. It defines the type of structure this is. The flags field holds memory specific flags such as read-only permission, and few others. The next field allows additional memory blocks to be daisy chained with this main block. These memory chains are freed at the same time the main block is freed. Since the main block is tied to the lifecycle of a java object, the chained memory objects are also tied to that same lifecycle.

Since the jmemory_t structure is fairly opaque (non specific) and always marks the start of an allocated memory block, this provides a mechanism by which many different memory management functions can work with allocated memory generically.

The block_t structure

The memory block is always preceded by a block_t structure. The first member in this structure is a jmemory_t structure and the only field that this structure adds is a size field:


typedef struct block_t {
jmemory_t mem;
size_t size;
} block_t;

The size field holds the number of bytes that were allocated, not including the size of the block_t header. As you might imagine, there are specific black box functions provided for calculating the exact location where user memory starts and where the header ends. The functions work with all types of memories and calculate using specific memory type algorithms where the data resides.

Here is typical JNI accessor method that gets a native data pointer using a java object:

jint Java_org_jnetpcap_nio_JBuffer_getInt(JNIEnv *env, jobject obj, jint offset) {
  jmemory_t *jmem;
  char *buf = jmemory_jni(env, obj, &jmem);
  if (buf == NULL) {
    throwNullPtrException(env);
    return 0;
  }

  if (jmemory_check(jmem, offset, sizeof(jint)) {
    throwException(env, INDEX_OUT_OF_BOUNDS_EXCEPTION, NULL);
    return 0;
  }
  
  return *((jint *)(buf + offset));
}

So step by step. Line 1 is the JNI accessor method for org.jnetpcap.nio.JBuffer class. It returns an int. Line 2 we create a local variable pointer to a jmemory_t structure where out java object is pointing to. Line 3 is a little bit busy. It does 2 things. Extracts a reference to a jmemory_t header and then does a lookup through the structure to arrive at a memory location where the buffer starts. At the same time, it also extracted the jmemory_t structure and store a reference to it into the jmem pointer. Another thing that the method checked for us, that is not so obvious, is that it also checked weather we have READ/WRITE permissions into the memory block. If we didn't it would return a NULL, but hopefully all is well and we get the actual memory pointer of our native buffer.

Lines 4 - 12, are fairly obvious that they check if we have a valid memory pointer and the offset is within memory bounds.

Lastly through C pointer magic, we dereference our memory pointers and cast the return value to be of type jint which is exactly what our jni accessor method expects us to return.

Last thing I would like to say under this section is that although through our examples, we are fairly certain that memory we point at is block_t based, our code above will work with any type of memory object, even if this is a direct PEER or PEER through a PROXY node.