/nalloc

Off-heap memory allocators for Java

Primary LanguageJavaApache License 2.0Apache-2.0

nalloc - off-heap memory allocators for Java

Build Status

Nalloc contains factories for allocating Java objects from native heap and memory mapped files. Basically void* and mmap in Java.

Off-heap storage is usually required with huge datasets (eg. caches and data grids) or with systems with hard latency requirements (eg. stock trading). Instantiating tens of millions of data entries from JVM heap is slow and likely to cause GC trashing. Off-heap storage for N entries can be instantiated in O(1) and data can be handled in pre-allocated arrays with direct pointer access.

Nalloc uses almost no JVM heap once initialized and is suitable for systems requiring "garbage free" design.

Download

Nalloc is available on Maven Central.

<dependency>
    <groupId>com.github.alaisi.nalloc</groupId>
    <artifactId>nalloc</artifactId>
    <version>0.1.Alpha1</version>
</dependency>

Usage

Hello world

NativeHeapAllocator allocator = NativeHeapAllocator.Factory.create(Message.class);
Pointer<Message> ptr = allocator.malloc(Message.class);

Message message = ptr.deref();
message.content("Hello world!");
System.out.println(message.content());

ptr.free();

Native heap

Access to native heap is provided by com.github.nalloc.NativeHeapAllocator. Pointers to native memory are eligible for GC, but the data referred to is not.

Allocating a single instance:

NativeHeapAllocator allocator = NativeHeapAllocator.Factory.create(MyStruct.class);
Pointer<MyStruct> ptr = allocator.malloc(MyStruct.class);

Allocating 90 million instances initialized to zero:

Array<MyStruct> array = allocator.calloc(90000000, MyStruct.class);

Resizing an array to 1 million:

Array<MyStruct> array = ...
array = allocator.realloc(array, 1000000);

Memory mapped files

Access to memory mapped files is provided by com.github.nalloc.MmapAllocator. Pointers to mmapped files provide access to objects that are transparently written to a file. As this is equivalent of POSIX MAP_SHARED, MmapAllocator can also be used as an IPC channel to interact with programs written in C/Python/Ruby/etc.

Mapping with MAP_ANOYMOUS without a backing file and mapping a buffer with MAP_PRIVATE are also implemented.

Mapping a file to memory with preallocated space for 1 million instances of MyStruct:

MmapAllocator allocator = MmapAllocator.Factory.create(MyStruct.class);
Array<MyStruct> messages = allocator.mmap(new File("/tmp/my-index"), 1000000, MyStruct.class);
// write to array index 5000
MyStruct my = messages.get(5000);
my.myAge('X');

Mapping a java.nio.ByteBuffer:

ByteBuffer buffer = ByteBuffer.allocateDirect(4096).order(ByteOrder.nativeOrder());
Array<MyStruct> array = allocator.mmap(buffer, MyStruct.class);

Anonymous mapping without a file for 1000 instances:

Array<MyStruct> array = allocator.mmap(1000, MyStruct.class);

Mutating an mmapped array with java.nio.ByteBuffer:

Array<MyStruct> array = ...
ByteBuffer buffer = allocator.toBytes(array); 
java.nio.channels.SocketChannel channel = ...
channel.read(buffer);

Pointers

Objects returned by the allocators are com.github.nalloc.Pointer and its subclass com.github.nalloc.Array. These are pointers to native memory that must be freed after use, otherwise memory (and possibly file descriptors) are leaked.

Freeing a pointer:

Pointer<MyStruct> ptr = allocator.malloc(MyStruct.class);
ptr.free();

Pointer extends java.lang.AutoCloseable, so it can be used in a try-with-resources block (mimicking C++ RAII):

try(Array<MyStruct> ptr = allocator.calloc(16, MyStruct.class)) {
    // do something with ptr
}
// ptr is now freed

Pointers provide all the type safety and bound checking of native C pointers: none. Bugs in handling pointers can (and most likely will) segfault the JVM.

Structs

Off-heap objects are modelled as c-style structs that have state but no behavior. Java representation is an interface annotated with @com.github.nalloc.Struct. Implementation for the interface is generated by the allocator and is based only on the Struct annotation value.

@Struct({
    @Field(name="id", type=Type.LONG) 
    @Field(name="type", type=Type.String, len=10) })
public interface Message {

    long id();
    void id(long id);

    String name();
    void name(String name);

}

Strings and arrays have a fixed length specified in Field.len.

@Struct({
    @Field(name="history", type=Type.LONG, len=10) 
    @Field(name="header", type=Type.String, len=10) })
public interface Request {

    long[] history();
    void history(long[] id);

    char[] header();
    void header(char[] header);

}

Struct.c creates C-compatible memory layout and Struct.pad pads memory length.

@Struct(c=true, pad=8, value={
    @Field(name="type", type=Type.CHAR) 
    @Field(name="body", type=Type.String, len=10) })
public interface Response {

    char type(); // single byte char
    void type(char type);

    String body(); // max 9 single byte chars and null terminator
    void body(String body);

}

Nested struct and struct arrays are supported with Type.STRUCT.

@Struct({
    @Field(name="headerType", type=Type.LONG),
    @Field(name="content", type=Type.STRING, len=16) })
public interface Header {
    long headerType();
    void headerType(long type);

    String content();
    void content(String content);
}

@Struct({
    @Field(name="header", type=Type.STRUCT, struct=Header.class),
    @Field(name="headers", type=Type.STRUCT, struct=Header.class, len=32) })
public interface Message {
    Header header();
    Array<Header> headers();
}