시작하며..

자바언어에서는 RandomAccessFile 을 이용해서, native heap 영역의 메모리를 얻어올 수 있다. jvm 내의 heap 영역이 아니기 때문에 따로 잡은 메모리 공간을 이용한다. 이를 통해서 자바는 속도를 향상시켜 read/write를 빨리 할 수 있다. java 1.6 update 22 내부 소스를 바탕으로 어떤 구조로 되어 있는지 살펴본다.

 

본론

간단하게 RandomAccessFile 클래스를 이용해서 Channel을 얻어온 후 READ only용으로 메모리 mapping 하는 메서드를 이용해서 MappedByteBuffer 클래스 타입의 인스턴스를 리턴하는 메서드이다.


 

private static MappedByteBuffer mmap(String filename, long start, int size) throws IOException
{
    RandomAccessFile raf;
    try
    {
        raf = new RandomAccessFile(filename, "r");
    }
    catch (FileNotFoundException e)
    {
        throw new IOError(e);
    }

    try
    {
        return raf.getChannel().map(FileChannel.MapMode.READ_ONLY, start, size);
    }
    finally
    {
        raf.close();
    }
}

 


RandomAccess의 getChannel() 메서드는 sun.nio.ch.FileChannelImpl 클래스를 리턴한다.  FileChannelImpl 클래스의 원형은 다음과 같다.

class FileChannelImpl  {

public MappedByteBuffer map(MapMode mode, long position, long size)
    throws IOException {


addr = map0(imode, mapPosition, mapSize);

Unmapper um = new Unmapper(addr, size + pagePosition); // 큰 의미없는 data structure
return Util.newMappedByteBufferr(isize, addr + pagePosition, um);

}

}


 

FileChannelImpl 클래스의 map 메서드에서는 map0() 메서드를 호출하여 주소 번지를 얻어오고, ByteBuffer로 만들어 리턴한다.

map0() 메서드의 원형은 native 로 연결되어 있다.

// Creates a new mapping
private native long map0(int prot, long position, long length)
    throws IOException;

 

이 메서드는 jni 코드인 FileChannelImpl.c 파일로 연결되어 있다. 역시 mmap64 메서드를 사용하고 그 주소값을 리턴하고 있다. 디폴트는 MAP_SHARED이다.

 


JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                     jint prot, jlong off, jlong len)
{
    void *mapAddress = 0;
    jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
    jint fd = fdval(env, fdo);
    int protections = 0;
    int flags = 0;

    if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
        protections = PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
        protections = PROT_WRITE | PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
        protections =  PROT_WRITE | PROT_READ;
        flags = MAP_PRIVATE;
    }

    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */

    if (mapAddress == MAP_FAILED) {
    if (errno == ENOMEM) {
        JNU_ThrowOutOfMemoryError(env, "Map failed");
        return IOS_THROWN;
    }
    return handle(env, -1, "Map failed");
    }

    return ((jlong) (unsigned long) mapAddress); 
}

 


 

참고로..

다른 함수를 살펴보면, postion0 () 메서드에서는 lseek64를, size0() 메서드에서는 fstat64를, close0() 메서드에서는 close를, release에서는 fcntl를, forces0 메서드에서는 fsync를, unmap0 메서드는 munmap를 쓰는등 파일 관련된 시스템 콜을 사용하고 있다. 

특별히 transfer0은 sendfile 또는 sendfile64 (동적 라이브러리를 이용한 function pointer의 형태)를 사용한다. sendfile64 구현을 안한 리눅스가 있나 보다…

 

주소번지를 받고 나서 메모리 매핑을 하는Util.newMappedByteBufferr(isize, addr + pagePosition, um) 메서드를 살펴본다. Reflection의 Constructor인 directByteBufferConstructor를 이용해서 MappedByteBuffer 클래스를 생성한다.

static MappedByteBuffer newMappedByteBuffer(int size, long addr, Runnable unmapper)


MappedByteBuffer dbb;
if (directByteBufferConstructor == null)
     initDBBConstructor();

dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
new Object[] { new Integer(size), new Long(addr), unmapper });

}

private static void initDBBRConstructor() {

Class th = Class.forName("java.nio.DirectByteBufferR"); // java api에는 나오지 않는 녀석
directByteBufferRConstructor = th.getDeclaredConstructor(
                    new Class[] { int.class, long.class,
                              Runnable.class });
directByteBufferRConstructor.setAccessible(true);

}

 



설명도 잘 나와 있다.

class DirectByteBuffer extends DirectByteBuffer implements DirectBuffer  {
..

// For memory-mapped buffers -- invoked by FileChannelImpl via reflection
    //
    protected DirectByteBufferR(int cap, long addr, FileDescriptor fd, Runnable unmapper)   {
        super(cap, addr, fd, unmapper);
   }


// For memory-mapped buffers -- invoked by FileChannelImpl via reflection
//
protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper){
    super(-1, 0, cap, cap, fd);
    address = addr;
    viewedBuffer = null;
    cleaner = Cleaner.create(this, unmapper);

}
..
}


 

DirectByteBufferR 클래스는 래퍼류로서 DirectByteBuffer를 상속받았기 때문에 생성자 레벨에서 편하게 가공할 수 있다. DirectByteBuffer클래스는 MappedByteBuffer를 상속받은 클래스이기 때문에 MappedByteBuffer Casting 이 가능하다.

 

참고로.. Cleaner 클래스는 PhantomReference 를 상속해서 쓰고 있다. GC를 편리하게 하기 위함으로 추가할 때마다 내부적으로 linked list로 객체를 만들어 정리될 때 Runnable로 들어온 객체의 run() 메서드를 호출한다.

public class sun.misc.Cleaner extends java.lang.ref.PhantomReference ..

 

private static class Unmapper
    implements Runnable
{

    private long address;
    private long size;

    private Unmapper(long address, long size) {
        assert (address != 0);
        this.address = address;
        this.size = size;
    }

    public void run() { // 메모리가 부족할 때 종료되게 함.. PhantomReference 임.
        if (address == 0)
            return;
        unmap0(address, size);
        address = 0;
    }

}



unmap0 메서드를 호출하여 항상 gc가 되게 한다.

 

 

마치며..

자바에서 nio map 사용시 리눅스 mmap 시스템 콜에 의해서 매핑된 데이터를 처리 하는 로직을 살펴보았다. 자연스럽게 생명주기도 살펴보았다.

다음에는 Cassandra의 Linux swap 이슈에 대해서 살펴봐야지..  mmap이슈가 있었던 내용을 쓰면서 정리하고자 한다.

Posted by '김용환'
,