Buffer源碼深刻分析

博客園對MarkDown顯示的層次感不是很好,你們能夠看這裏:Buffeerjava

本機環境:
Linux 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linuxlinux

Buffer

Buffer的類圖以下:git

Buffer類圖

除了Boolean,其餘基本數據類型都有對應的Buffer,可是隻有ByteBuffer才能和Channel交互。只有ByteBuffer才能產生Direct的buffer,其餘數據類型的Buffer只能產生Heap類型的Buffer。ByteBuffer能夠產生其餘數據類型的視圖Buffer,若是ByteBuffer自己是Direct的,則產生的各視圖Buffer也是Direct的github

Direct和Heap類型Buffer的本質

首選說說JVM是怎麼進行IO操做的。數組

JVM在須要經過操做系統調用完成IO操做,好比能夠經過read系統調用完成文件的讀取。read的原型是:ssize_t read(int fd,void *buf,size_t nbytes),和其餘的IO系統調用相似,通常須要緩衝區做爲其中一個參數,該緩衝區要求是連續的。app

Buffer分爲Direct和Heap兩類,下面分別說明這兩類buffer。ide

Heap

Heap類型的Buffer存在於JVM的堆上,這部份內存的回收與整理和普通的對象同樣。Heap類型的Buffer對象都包含一個對應基本數據類型的數組屬性(好比:final **[] hb),數組纔是Heap類型Buffer的底層緩衝區。
可是Heap類型的Buffer不能做爲緩衝區參數直接進行系統調用,主要由於下面兩個緣由。測試

  • JVM在GC時可能會移動緩衝區(複製-整理),緩衝區的地址不固定。
  • 系統調用時,緩衝區須要是連續的,可是數組可能不是連續的(JVM的實現沒要求連續)。

因此使用Heap類型的Buffer進行IO時,JVM須要產生一個臨時Direct類型的Buffer,而後進行數據複製,再使用臨時Direct的Buffer做爲參數進行操做系統調用。這形成很低的效率,主要是由於兩個緣由:優化

  • 須要把數據從Heap類型的Buffer裏面複製到臨時建立的Direct的Buffer裏面。
  • 可能產生大量的Buffer對象,從而提升GC的頻率。因此在IO操做時,能夠經過重複利用Buffer進行優化。

Direct

Direct類型的buffer,不存在於堆上,而是JVM經過malloc直接分配的一段連續的內存,這部份內存成爲直接內存,JVM進行IO系統調用時使用的是直接內存做爲緩衝區。
-XX:MaxDirectMemorySize,經過這個配置能夠設置容許分配的最大直接內存的大小(MappedByteBuffer分配的內存不受此配置影響)。
直接內存的回收和堆內存的回收不一樣,若是直接內存使用不當,很容易形成OutOfMemoryError。JAVA沒有提供顯示的方法去主動釋放直接內存,sun.misc.Unsafe類能夠進行直接的底層內存操做,經過該類能夠主動釋放和管理直接內存。同理,也應該重複利用直接內存以提升效率。ui

MappedByteBuffer和DirectByteBuffer之間的關係

This is a little bit backwards: By rights MappedByteBuffer should be a subclass of DirectByteBuffer, but to keep the spec clear and simple, and for optimization purposes, it's easier to do it the other way around.This works because DirectByteBuffer is a package-private class.(本段話摘自MappedByteBuffer的源碼)

實際上,MappedByteBuffer屬於映射buffer(本身看看虛擬內存),可是DirectByteBuffer只是說明該部份內存是JVM在直接內存區分配的連續緩衝區,並不一是映射的。也就是說MappedByteBuffer應該是DirectByteBuffer的子類,可是爲了方便和優化,把MappedByteBuffer做爲了DirectByteBuffer的父類。另外,雖然MappedByteBuffer在邏輯上應該是DirectByteBuffer的子類,並且MappedByteBuffer的內存的GC和直接內存的GC相似(和堆GC不一樣),可是分配的MappedByteBuffer的大小不受-XX:MaxDirectMemorySize參數影響。
MappedByteBuffer封裝的是內存映射文件操做,也就是隻能進行文件IO操做。MappedByteBuffer是根據mmap產生的映射緩衝區,這部分緩衝區被映射到對應的文件頁上,屬於直接內存在用戶態,經過MappedByteBuffer能夠直接操做映射緩衝區,而這部分緩衝區又被映射到文件頁上,操做系統經過對應內存頁的調入和調出完成文件的寫入和寫出。

MappedByteBuffer

經過FileChannel.map(MapMode mode,long position, long size)獲得MappedByteBuffer,下面結合源碼說明MappedByteBuffer的產生過程。

FileChannel.map的源碼:

public MappedByteBuffer map(MapMode mode, long position, long size)
        throws IOException
    {
        ensureOpen();
        if (position < 0L)
            throw new IllegalArgumentException("Negative position");
        if (size < 0L)
            throw new IllegalArgumentException("Negative size");
        if (position + size < 0)
            throw new IllegalArgumentException("Position + size overflow");
        //最大2G
        if (size > Integer.MAX_VALUE)
            throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE");
        int imode = -1;
        if (mode == MapMode.READ_ONLY)
            imode = MAP_RO;
        else if (mode == MapMode.READ_WRITE)
            imode = MAP_RW;
        else if (mode == MapMode.PRIVATE)
            imode = MAP_PV;
        assert (imode >= 0);
        if ((mode != MapMode.READ_ONLY) && !writable)
            throw new NonWritableChannelException();
        if (!readable)
            throw new NonReadableChannelException();

        long addr = -1;
        int ti = -1;
        try {
            begin();
            ti = threads.add();
            if (!isOpen())
                return null;
            //size()返回實際的文件大小
            //若是實際文件大小不符合,則增大文件的大小,文件的大小被改變,文件增大的部分默認設置爲0。
            if (size() < position + size) { // Extend file size
                if (!writable) {
                    throw new IOException("Channel not open for writing " +
                        "- cannot extend file to required size");
                }
                int rv;
                do {
                   //增大文件的大小
                    rv = nd.truncate(fd, position + size);
                } while ((rv == IOStatus.INTERRUPTED) && isOpen());
            }
            //若是要求映射的文件大小爲0,則不調用操做系統的mmap調用,只是生成一個空間容量爲0的DirectByteBuffer
            //並返回
            if (size == 0) {
                addr = 0;
                // a valid file descriptor is not required
                FileDescriptor dummy = new FileDescriptor();
                if ((!writable) || (imode == MAP_RO))
                    return Util.newMappedByteBufferR(0, 0, dummy, null);
                else
                    return Util.newMappedByteBuffer(0, 0, dummy, null);
            }
            //allocationGranularity的大小在個人系統上是4K
            //頁對齊,pagePosition爲第多少頁
            int pagePosition = (int)(position % allocationGranularity);
            //從頁的最開始映射
            long mapPosition = position - pagePosition;
            //由於從頁的最開始映射,增大映射空間
            long mapSize = size + pagePosition;
            try {
                // If no exception was thrown from map0, the address is valid
                //native方法,源代碼在openjdk/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c,
                //參見下面的說明
                addr = map0(imode, mapPosition, mapSize);
            } catch (OutOfMemoryError x) {
                // An OutOfMemoryError may indicate that we've exhausted memory
                // so force gc and re-attempt map
                System.gc();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException y) {
                    Thread.currentThread().interrupt();
                }
                try {
                    addr = map0(imode, mapPosition, mapSize);
                } catch (OutOfMemoryError y) {
                    // After a second OOME, fail
                    throw new IOException("Map failed", y);
                }
            }

            // On Windows, and potentially other platforms, we need an open
            // file descriptor for some mapping operations.
            FileDescriptor mfd;
            try {
                mfd = nd.duplicateForMapping(fd);
            } catch (IOException ioe) {
                unmap0(addr, mapSize);
                throw ioe;
            }

            assert (IOStatus.checkAll(addr));
            assert (addr % allocationGranularity == 0);
            int isize = (int)size;
            Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
            if ((!writable) || (imode == MAP_RO)) {
                return Util.newMappedByteBufferR(isize,
                                                 addr + pagePosition,
                                                 mfd,
                                                 um);
            } else {
                return Util.newMappedByteBuffer(isize,
                                                addr + pagePosition,
                                                mfd,
                                                um);
            }
        } finally {
            threads.remove(ti);
            end(IOStatus.checkAll(addr));
        }
    }

map0的源碼實現:

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);
    //linux系統調用是經過整型的文件id引用文件的,這裏獲得文件id
    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;
    }
    //這裏就是操做系統調用了,mmap64是宏定義,實際最後調用的是mmap
    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) {
            //若是沒有映射成功,直接拋出OutOfMemoryError
            JNU_ThrowOutOfMemoryError(env, "Map failed");
            return IOS_THROWN;
        }
        return handle(env, -1, "Map failed");
    }

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

雖然FileChannel.map()的zise參數是long,可是size的大小最大爲Integer.MAX_VALUE,也就是最大隻能映射最大2G大小的空間。實際上操做系統提供的MMAP能夠分配更大的空間,可是JAVA限制在2G,ByteBuffer等Buffer也最大隻能分配2G大小的緩衝區。
MappedByteBuffer是經過mmap產生獲得的緩衝區,這部分緩衝區是由操做系統直接建立和管理的,最後JVM經過unmmap讓操做系統直接釋放這部份內存。

Haep****Buffer

下面以ByteBuffer爲例,說明Heap類型Buffer的細節。
該類型的Buffer能夠經過下面方式產生:

  • ByteBuffer.allocate(int capacity)
  • ByteBuffer.wrap(byte[] array)
    使用傳入的數組做爲底層緩衝區,變動數組會影響緩衝區,變動緩衝區也會影響數組。
  • ByteBuffer.wrap(byte[] array,int offset, int length)
    使用傳入的數組的一部分做爲底層緩衝區,變動數組的對應部分會影響緩衝區,變動緩衝區也會影響數組。

DirectByteBuffer

DirectByteBuffer只能經過ByteBuffer.allocateDirect(int capacity) 產生。
ByteBuffer.allocateDirect()源碼以下:

public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

DirectByteBuffer()源碼以下:

DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        //直接內存是否要頁對齊,我本機測試的不用
        boolean pa = VM.isDirectMemoryPageAligned();
        //頁的大小,本機測試的是4K
        int ps = Bits.pageSize();
        //若是頁對齊,則size的大小是ps+cap,ps是一頁,cap也是重新的一頁開始,也就是頁對齊了
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        //JVM維護全部直接內存的大小,若是已分配的直接內存加上本次要分配的大小超過容許分配的直接內存的最大值會
        //引發GC,不然容許分配並把已分配的直接內存總量加上本次分配的大小。若是GC以後,仍是超過所容許的最大值,
        //則throw new OutOfMemoryError("Direct buffer memory");
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
           //是吧,unsafe能夠直接操做底層內存
            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;
    }

unsafe.allocateMemory()的源碼在openjdk/src/openjdk/hotspot/src/share/vm/prims/unsafe.cpp中。具體的源碼以下:

UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
  UnsafeWrapper("Unsafe_AllocateMemory");
  size_t sz = (size_t)size;
  if (sz != (julong)size || size < 0) {
    THROW_0(vmSymbols::java_lang_IllegalArgumentException());
  }
  if (sz == 0) {
    return 0;
  }
  sz = round_to(sz, HeapWordSize);
  //最後調用的是 u_char* ptr = (u_char*)::malloc(size + space_before + space_after),也就是malloc。
  void* x = os::malloc(sz, mtInternal);
  if (x == NULL) {
    THROW_0(vmSymbols::java_lang_OutOfMemoryError());
  }
  //Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
  return addr_to_java(x);
UNSAFE_END

JVM經過malloc分配獲得連續的緩衝區,這部分緩衝區能夠直接做爲緩衝區參數進行操做系統調用。

相關文章
相關標籤/搜索