溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

DirectByteBuffer和文件IO的作用分別是什么

發(fā)布時間:2021-06-15 11:37:31 來源:億速云 閱讀:144 作者:Leah 欄目:大數(shù)據(jù)

本篇文章為大家展示了DirectByteBuffer和文件IO的作用分別是什么,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

a. 傳統(tǒng)的IO操作(就是使用java.io包的api)訪問磁盤文件,數(shù)據(jù)需要copy的次數(shù):

    1. 磁盤文件的數(shù)據(jù) copy 內(nèi)核page cache 

    2. 內(nèi)核的數(shù)據(jù) copy  應用程序空間(即:jvm 堆外內(nèi)存)

    3. jvm堆外內(nèi)存  copy  jvm堆內(nèi) 內(nèi)存

為什么2、和3 不合并,將內(nèi)核數(shù)據(jù) copy jvm堆內(nèi)內(nèi)存。 因為jvm進行系統(tǒng)調(diào)用進行讀文件時候,此時發(fā)生gc,那么堆內(nèi)存的對應地址就會移動,所以直接copy到堆內(nèi)是有問題的。

b. 使用DirectByteBuffer訪問磁盤文件,數(shù)據(jù)需要copy的次數(shù):

   1. 磁盤文件的數(shù)據(jù) copy 內(nèi)核page cache 

    2. 內(nèi)核的數(shù)據(jù) copy  應用程序空間(即:DirectByteBuffer)

所以DirectByteBuffer減少了內(nèi)存copy次數(shù)。

1.傳統(tǒng)文件IO解析

文件讀取示例:

FileInputStream input = new FileInputStream("/data");

byte[] b = new byte[SIZE];

input.read(b);

byte數(shù)組示堆內(nèi)存對象,此處將數(shù)據(jù)copy 到jvm堆內(nèi)存。我們看一下read函數(shù)內(nèi)部實現(xiàn)

public int read(byte b[]) throws IOException { 

    return readBytes(b, 0, b.length);

}

private native int readBytes(byte b[], int off, int len) throws IOException;

我們看到 read函數(shù)最終調(diào)用 native函數(shù) readBytes。

jintreadBytes(JNIEnv *env, jobject this, jbyteArray bytes,          jint off, jint len, jfieldID fid){

    jint nread;

    char stackBuf[BUF_SIZE];

    char *buf = NULL;

    FD fd;

    if (IS_NULL(bytes)) {

        JNU_ThrowNullPointerException(env, NULL);

        return -1;

    }

    if (outOfBounds(env, off, len, bytes)) {

        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);

        return -1;

    }

    if (len == 0) {

        return 0;

    } else if (len > BUF_SIZE) {

        buf = malloc(len);

        if (buf == NULL) {

            JNU_ThrowOutOfMemoryError(env, NULL);

            return 0;

        }

    } else {

        buf = stackBuf;

    }

    fd = GET_FD(this, fid);

    if (fd == -1) {

        JNU_ThrowIOException(env, "Stream Closed");

        nread = -1;

    } else {

        nread = IO_Read(fd, buf, len);

        if (nread > 0) {

            (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);

        } else if (nread == -1) {

            JNU_ThrowIOExceptionWithLastError(env, "Read error");

        } else { /* EOF */

            nread = -1;

        }

    }

    if (buf != stackBuf) {

        free(buf);   

}

    return nread;

}

我們看到最終通過IO_Read將緩沖數(shù)據(jù)讀到buf中去,這個IO_Read其實是一個宏定義:

#define IO_Read handleRead

handleRead函數(shù)實現(xiàn)如下,這里你可以看到這里進行了read系統(tǒng)調(diào)用:

ssize_t

handleRead(FD fd, void *buf, jint len)

{

    ssize_t result;

    RESTARTABLE(read(fd, buf, len), result);

    return result;

}

buf返回之后,由SetByteArrayRegion這個JNI函數(shù)拷貝到了bytes,它的具體實現(xiàn)如下(下面定義了一個通用的宏函數(shù)來表示各種數(shù)據(jù)類型數(shù)組區(qū)域的設置,可以將Result宏替換成Byte即可理解):

JNI_ENTRY(void, \

jni_Set##Result##ArrayRegion(JNIEnv *env, ElementType##Array array, jsize start, \

             jsize len, const ElementType *buf)) \

  JNIWrapper("Set" XSTR(Result) "ArrayRegion"); \

  DTRACE_PROBE5(hotspot_jni, Set##Result##ArrayRegion__entry, env, array, start, len, buf);\

  DT_VOID_RETURN_MARK(Set##Result##ArrayRegion); \

  typeArrayOop dst = typeArrayOop(JNIHandles::resolve_non_null(array)); \

  if (start < 0 || len < 0 || ((unsigned int)start + (unsigned int)len > (unsigned int)dst->length())) { \

    THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException()); \

  } else { \

    if (len > 0) { \

      int sc = TypeArrayKlass::cast(dst->klass())->log2_element_size(); \

      memcpy((u_char*) dst->Tag##_at_addr(start), \

             (u_char*) buf, \

             len << sc);    \

    } \

  } \

JNI_END

(以上內(nèi)容部門來源:https://www.zhihu.com/question/65415926)

由此可見,nativ方法,readBytes而采用了C Heap - JVM Heap進行內(nèi)存拷貝的方式進行數(shù)據(jù)傳遞。

而readBytes 通過調(diào)用 handleRead 進行讀寫。handleRead就是讀取內(nèi)核緩存區(qū)數(shù)據(jù)。內(nèi)核數(shù)據(jù)來源文件。

2. DirectByteBuffer

DirectByteBuffer 是構(gòu)建在堆外的內(nèi)存的對象。

DirectByteBuffer是包級別可訪問的,通過 ByteBuffer.allocateDirect(int capacity) 進行構(gòu)造。

public static ByteBuffer allocateDirect(int capacity) {

return new DirectByteBuffer(capacity);

}

我們看一下DirectByteBuffer 構(gòu)造函數(shù)實現(xiàn)

DirectByteBuffer(int cap) {// package-private

    super(-1,0, cap, cap);

    boolean pa = VM.isDirectMemoryPageAligned();

    int ps = Bits.pageSize();

    long size = Math.max(1L, (long)cap + (pa ? ps :0));

    Bits.reserveMemory(size, cap);

    long base =0;

    try {

        base =unsafe.allocateMemory(size);

    }catch (OutOfMemoryError x) {

        Bits.unreserveMemory(size, cap);

        throw x;

    }

    unsafe.setMemory(base, size, (byte)0);

    if (pa && (base % ps !=0)) {

        // Round up to page boundary

        address = base + ps - (base & (ps -1));

    }else {

        address = base;

    }

    cleaner = Cleaner.create(this,new Deallocator(base, size, cap));   

    att =null;

}

這里我們主要關(guān)注這幾個地方:

1.unsafe.allocateMemory(size);

利用 unsafe 類在堆外內(nèi)存(C_HEAP)中分配了一塊空間,這是一個 native 函數(shù),轉(zhuǎn)到進行堆外內(nèi)存分配的 C/C++ 代碼

inline char* AllocateHeap( size_t size, MEMFLAGS flags, address pc = 0, AllocFailType alloc_failmode = AllocFailStrategy::EXIT_OOM){

// ... 省略

char*p=(char*)os::malloc(size, flags, pc);

// 分配在 C_HEAP 上并返回指向內(nèi)存區(qū)域的指針

// ... 省略

return p;

}

2.cleaner = Cleaner.create(this,new Deallocator(base, size, cap)); 

cleaner對象是對DirectByteBuffer占用對堆外內(nèi)存進行清理。DirectByteBuffer.cleaner().clean() 進行手動清理。我們看一下clean() 函數(shù)

public void clean() {

//....省略

this.thunk.run();

//....省略

}

其中 thunk就是我們 Cleaner.create(this,new Deallocator(base, size, cap)); 中的Deallocator??匆幌翫eallocator。

private static class Deallocator implements Runnable

{

//。。。省略

    public void run() {

        if (address ==0) {

            // Paranoia

            return;

           }

        unsafe.freeMemory(address);

        address =0;

        Bits.unreserveMemory(size,capacity);

    }

}

可以看到其是一個線程進行 堆外內(nèi)存的釋放動作。

cleaner是PhantomReference的子類。

PhantomReference它其實主要是用來跟蹤對象何時被回收的,它不能影響gc決策,但是gc過程中如果發(fā)現(xiàn)某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了,那將會把這個引用放到java.lang.ref.Reference.pending隊列里,在gc完畢的時候通知ReferenceHandler這個守護線程去執(zhí)行一些后置處理。這個處理方法中,就會判斷是否是cleaner對象,如果是,就性質(zhì)clean()函數(shù)。

因此DirectByteBuffer并不需要我們手動清理內(nèi)存。當jvm進行g(shù)c(oldgc)的時候,就會清理沒有引用的 dirctByteBuffer。

當我們一直申請DirectByteBuffer。其實占用的是堆外內(nèi)存,堆內(nèi)內(nèi)存只是占用一個引用。如果一直觸發(fā)不了gc,納悶堆外內(nèi)存就不會回收,導致jvm進程占用內(nèi)存很大。我們可以通過-XX:MaxDirectMemorySize限制DirecByteBuffer占用堆外內(nèi)存的大小

3.Bits.reserveMemory(size, cap);

static void reserveMemory(long size,int cap) {

    synchronized (Bits.class) {

        if (!memoryLimitSet && VM.isBooted()) {

            maxMemory = VM.maxDirectMemory();

            memoryLimitSet =true;

        }

        // -XX:MaxDirectMemorySize limits the total capacity rather than the

        // actual memory usage, which will differ when buffers are page

        // aligned.

        if (cap <=maxMemory -totalCapacity) {

            reservedMemory += size;

            totalCapacity += cap;

            count++;

            return;

        }

    }

    System.gc();

    try {

        Thread.sleep(100);

    }catch (InterruptedException x) {

        // Restore interrupt status

        Thread.currentThread().interrupt();

    }

    synchronized (Bits.class) {

        if (totalCapacity + cap >maxMemory)

            throw new OutOfMemoryError("Direct buffer memory");

        reservedMemory += size;

        totalCapacity += cap;

        count++;

        }

}

該函數(shù)用于統(tǒng)計DirectByteBuffer占用的大小。VM.maxDirectMemory()是jvm允許申請的最大DirectBuffer的大小(XX:MaxDirectMemorySize 通過這個參數(shù)設置)

如果發(fā)現(xiàn)當前申請的空間,大于限制的空間,就會觸發(fā)一次gc,上面說過gc會回收哪些之前不使用的directBuffer。然后再次申請。

VM.maxDirectMemory() 大小是如何設置的內(nèi),在VM類有這樣一段代碼

public static void saveAndRemoveProperties(Properties var0) {

    //....

    String var1 = (String)var0.remove("sun.nio.MaxDirectMemorySize");

    if (var1 !=null) {

        if (var1.equals("-1")) {

            directMemory = Runtime.getRuntime().maxMemory();

        }else {

            long var2 = Long.parseLong(var1);

            if (var2 > -1L) {

            directMemory = var2;

            }

    }

    //...

}

"sun.nio.MaxDirectMemorySize"  這個屬性就是通過 -XX:MaxDirectMemorySize 這個參數(shù)設置的。如果我們不指定這個jvm參數(shù),筆者在jdk8中測試了一下,默認是-1,這樣就導致directBufffer內(nèi)存限制為進程最大內(nèi)存。當然這也是一個潛在風險。

風險案例:

筆者曾在線上運行一個應用。該應用就是從消息隊列中消費數(shù)據(jù),然后將數(shù)據(jù)處理后存到Hbase中。但是應用運行每次運行2周左右,機器就會出現(xiàn)swap占用過大。經(jīng)過分析,是jvm進程占用內(nèi)存太大,但是分析jvm相關(guān)參數(shù)(堆、線程大小),并沒有設置的很大。最后發(fā)現(xiàn)原來是directBuffer占用達到了10G。后面通過-XX:MaxDirectMemorySize=2048m 限制directbuffer使用量,解決了問題。每次directBuffer占用達到2G,就會觸發(fā)一次fullgc,將之前的無用directbuffer回收掉。hbase一個坑,有時間筆者會整理這個案例。

3.DirectByteBuffer文件IO

文件讀取示例:

FileChannel filechannel=new RandomAccessFile("/data/appdatas/cat/mmm","rw").getChannel();

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(SIZE);

filechannel.read(byteBuffer)

我們看一下read函數(shù)

public int read(ByteBuffer var1)throws IOException {

//。。。。

var3 = IOUtil.read(this.fd, var1, -1L,this.nd);

//。。。。

}

主要邏輯調(diào)用IOUtil.read。我們看一下這個函數(shù)

static int read(FileDescriptor var0, ByteBuffer var1,long var2, NativeDispatcher var4)throws IOException {

    if (var1.isReadOnly()) {

        throw new IllegalArgumentException("Read-only buffer");

    }else if (var1instanceof DirectBuffer) {

        return readIntoNativeBuffer(var0, var1, var2, var4);

    }else {

        ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());

        int var7;

    try {

        int var6 = readIntoNativeBuffer(var0, var5, var2, var4);

        var5.flip();

        if (var6 >0) {

            var1.put(var5);

        }

        var7 = var6;

    }finally {

        Util.offerFirstTemporaryDirectBuffer(var5);

    }

    return var7;

    }

}

主要方法就是通過 readIntoNativeBuffer 這個函數(shù)將數(shù)據(jù)讀入 directBuffer中,其中readIntoNativeBuffer也是調(diào)用一個native方法。

通過上面的代碼,我們會看到,如果fielchannel.read(ByteBuffer) 也可以傳入一個HeapByteBuffer,這個類是堆中。如果是這個類,那么內(nèi)部讀取的時候,會把數(shù)據(jù)先讀到DirectByteBuffer中,然后在copy到HeapByteBuffer中。Util.getTemporaryDirectBuffer(var1.remaining());就是獲取一個DirectBuffer對像。因為DirectBuffer創(chuàng)建的時候,開銷比較大,所以使用的時候一般會用一個池子來管理。有興趣可以看一下Util這個類里面的實現(xiàn)。

上述內(nèi)容就是DirectByteBuffer和文件IO的作用分別是什么,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI