NIO笔记(三)之Buffer

参考资料

  1. 《Java IO,NIO and NIO2》 英文版
  2. http://tutorials.jenkov.com/java-nio/index.html
  3. 《JAVA NIO》

Buffer

  1. Bufferjava NIO操作的基础,Java NIO的Buffer用于与Channel进行交互,数据是从Channel读入Buffer(缓冲区),从Buffer写入到Channel中的。从本质上来说,NIO主要是将数据从Buffer中移入和移出。
    • 第一步:进程对操作系统发出read()调用
    • 第二步:操作系统请求磁盘控制器(DMA)将这些字节直接读入操作系统缓冲区
    • 第三步: 操作系统将字节复制到JVM进程的缓冲区中
  2. 为什么不让DMA控制器直接复制到进程缓存区,这样岂不是更好?这样会有两个问题
    • DMA控制器通常不能直接与运行JVM进程的用户空间通信
    • DMA控制器这种面向块的设备,与固定大小的数据块一起工作,JVM进程可能请求的数据大小不是块大小的倍数或者未对齐
    • 由于以上的问题,操作系统充当中间角色,在JVM进程DMA控制器之间切换时,操作系统会撕裂和重新组合数据
  3. Buffer从两方面提高IO的效率
    • 减少实际的物理读写次数
    • 缓冲区在创建时被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存区域的次数
  4. 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO的Buffer对象,并提供一组方法用来方便的访问这块内存
  5. 使用Buffer读写数据一般遵循以下四个步骤
    • 写入数据到Buffer
    • 调用flip(),切换读写模式
    • Buffer中读取数据
    • 调用clear()方法或者compact()
  6. Buffer的一些特征
    • Buffer类不是线程安全的,避免多线程同时读写同一个Buffer
    • 所有的Buffer都是可读的,但是并非所有的都可写,可通过isReadOnly()判断,对只读Buffer的修改会导致ReadOnlyBufferException
    • 传统流是单向的(输入和输出流是独立的),而Buffer是双向的

Buffer的属性

  1. capacity(容量)
    • Buffer能容纳的数据元素的最大数量(不能为负值),在缓冲区创建时指定,并且永远不能更改。
    • capacity(容量)指的是具体实现类中写入的数据对象的数量,比如实现类IntBuffer初始d额capacity是10,则最多可以写入10个int类型的数据。
  2. position(位置)
    • 当写数据到Buffer中时,position表示当前的位置,当一个数据被写入到Buffer后,position移动到下一个可插入数据的Buffer单元。position初始值为0,最大值为capacity-1
    • 当读数据时,也是从某个特定的位置开始读。当将Buffer从写模式切换到读模式,position会被重置为0,当从Bufferposition处读取数据时,position向前移动到下一个可读的位置
  3. limit(界限)
    • 第一个不应该被读取或者写入的Buffer位置索引,位于limit后的数据既不能读也不能写
    • 在写模式下,Bufferlimit表示你最多能向Buffer写入多少数据,写模式下,limit等于capacity
    • 当切换到读模式下,limit表示你最多能从Buffer读取多少数据,因此,当Buffer从写模式切换到读模式下,此时limit被设置为写模式下的position。即能读取到之前写入的所有数据
  4. mark(标记)
    • 初始时没有定义值。调用mark()来设定mark=position,调用reset()设定postion=mark。作用就是临时保存position的值,当需要恢复时可以通过reset()恢复
  5. Buffer属性的关系
    • 0 <= mark <= position <= limit <= capacity
  6. 创建一个ByteBuffer
    • ByteBuffer buf = ByteBuffer.allocate(7)
    • 初始Buffer属性的关系图

Buffer实现类以及用法

  1. Buffer是一个抽象类,它具有7个直接抽象子类(即缓冲区中存储的数据类型并不像I/O流只能存储byte或char数据类型),继承关系图如下,从下图可以看出没有BooleanBuffer这个子类。java.lang.StringBuffer是在lang包下,在nio包下没有提供java.nio.StringBuffer缓冲区,在NIO中存储字符的缓冲区使用CharBuffer

  2. Buffer创建

    • allocate()或者allocateDirect()

      //分配一个captacity为48字节的HeapByteBuffer
      ByteBuffer byBuffer = ByteBuffer.allocate(48);
      //分配一个captacity为48字节的DirectByteBuffer
      ByteBuffer byBufferDirect = ByteBuffer.allocateDirect(48);
      //分配一个可存储1024个字符的CharBuffer
      CharBuffer charBuffer = CharBuffer.allocate(1024);	
      
    • wrap():包装已有数组,wrap()类似于静态工厂方法

       @Test
       public void testWrapArray() {
           int[] bytes = new int[]{1, 2, 3, 4};
           //包装一个已有的数组
           IntBuffer ib = IntBuffer.wrap(bytes);
           /**
            * capacity:4,limit:4,position:0
             * 通过wrap创建的Buffer,容量与数组的length一样
             */
            logger.info("capacity:{},limit:{},position:{}", ib.capacity(), ib.limit(), ib.position());
        
            for (int i = 0; i < bytes.length; i++) {
              //依次输出1234
                logger.info("{}", ib.get(i));
            }
            for (int i = 0; i < bytes.length; i++) {
                if (i == 0) {
                    //改变数组,同时会改变缓冲区中的值
                    bytes[i] = 5;
                }
                //5234
                logger.info("{}", ib.get(i));
            }
        
            int[] intArray = new int[]{1, 2, 3, 4, 5};
            /**
             * wrap(array,offset,length)
             * 1. 创建一个capacity等于inArray.length的buffer
             * 2. 并且position等于offset,此处是1
             * 3. limit为length,此时是2
             */
            IntBuffer intBuffer = IntBuffer.wrap(intArray, 1, 2);
            //capacity:5,limit:3,position:1
            logger.info("capacity:{},limit:{},position:{}",
                    intBuffer.capacity(),
                    intBuffer.limit(),
                    intBuffer.position());
        
        
            while (intBuffer.hasRemaining()) {
                //23
                logger.info("{}",intBuffer.get());
           }
        }
      
    • map():内存映射方式

        String filePath = BufferTest.class.getClassLoader().getResource("book.txt").getFile();
       File file = new File(filePath);
        FileChannel fc = new RandomAccessFile(filePath, "rw").getChannel();
        MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
      
  3. Buffer中写入数据

    • Channel中写入Buffer

      //只是演示,所以不做异常处理
      @Test
      public void testBuffer() throws IOException {
         String filePath = BufferTest.class.getClassLoader().getResource("book.txt").getFile();
         RandomAccessFile accessFile = new RandomAccessFile(filePath, "rw");
         FileChannel channel = accessFile.getChannel();
         ByteBuffer buf = ByteBuffer.allocate(1024);
         /**
          * 将FileChannel通道中的数据读取到Buffer中
          * 返回值表示有多少字节被读到Buffer中,返回-1表示读到文件末尾
          */
         int readByte = channel.read(buf);
         while (readByte != -1) {
             //将写模式切换到读模式
             buf.flip();
             //hasRemaining 判断是否已经达到缓冲区的limit
             while (buf.hasRemaining()) {
                 //remaining从position到limit还剩余的元素个数
                 int count = buf.remaining();
                 System.out.println("----" + count + "------");
                 System.out.println(new String(new byte[]{buf.get()}, "utf-8"));
             }
             //读完清空缓冲区,让缓冲区继续可写
             buf.clear();
             readByte = channel.read(buf);
         }
         accessFile.close();
      }
      
    • 通过Buffer的put方法

      @Test
      public void testWriteBuffer() {
         CharBuffer charBuffer = CharBuffer.allocate(10);
         charBuffer.put('a');
         charBuffer.put('b');
         charBuffer.put('c');
         charBuffer.flip();
         while (charBuffer.hasRemaining()) {
             //abc
             System.out.print(charBuffer.get());
         }
      }
      
  4. Buffer中读取数据

    • Buffer读取数据到Channel

         int bytesWritten = inChannel.write(buf);
      
    • 使用get()

      System.out.print(charBuffer.get());
      
  5. flip():翻转,可以理解为模式切换(比如写模式切换到读模式)

    • JDK中Buffer抽象类,可以byteBuffer.limit(byteBuffer.position()).position(0);limit指明了Buffer有效内容的末端,将limit设置为当前位置,position设置为0

       public final Buffer flip() {
      	      limit = position;
              position = 0;
              mark = -1;
              return this;
        }
      
    • 案例

         @Test
          public void testFlip() {
              IntBuffer intBuffer = IntBuffer.allocate(20);
              //position:0,limit:20,capacity:20
              logger.info("position:{},limit:{},capacity:{}",
                      intBuffer.position(),
                      intBuffer.limit(),
                      intBuffer.capacity());
              Stream.of(1, 2, 3, 4, 5).forEach(intBuffer::put);
              //position:5,limit:20,capacity:20
              logger.info("position:{},limit:{},capacity:{}",
                      intBuffer.position(),
                      intBuffer.limit(),
                      intBuffer.capacity());
              intBuffer.flip();
              //position:0,limit:5,capacity:20
              logger.info("position:{},limit:{},capacity:{}",
                      intBuffer.position(),
                      intBuffer.limit(),
                      intBuffer.capacity());
              while (intBuffer.hasRemaining()) {
                  System.out.println(intBuffer.get());
              }
          
              //将读模式切换为写模式
              intBuffer.clear();
              //position:0,limit:20,capacity:20
              logger.info("position:{},limit:{},capacity:{}",
                      intBuffer.position(),
                      intBuffer.limit(),
                      intBuffer.capacity());
          
          }
      
  6. rewind():倒带(重新播放),已经读完的数据如果需要再读一遍,可以调用此方法。rewindflip类似,但是rewind不影响limit(表示仍然能读多少元素),它只是将position设置为0,mark标记被清理

    1. rewind源码
    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    
    2. 测试rewind
    @Test
    public void testRewind() {
        IntBuffer intBuffer = IntBuffer.allocate(20);
        //写入数据
        Stream.of(1, 2, 3, 4, 5).forEach(intBuffer::put);
        //切换到读模式
        intBuffer.flip();
        //position:0,limit:5,capacity:20
        logger.info("position:{},limit:{},capacity:{}",
                intBuffer.position(),
                intBuffer.limit(),
                intBuffer.capacity());
        while (intBuffer.hasRemaining()) {
            System.out.print(intBuffer.get());
        }
        System.out.println();
        //position:5,limit:5,capacity:20
        logger.info("position:{},limit:{},capacity:{}",
                intBuffer.position(),
                intBuffer.limit(),
                intBuffer.capacity());
    
        intBuffer.rewind();
        //position:0,limit:5,capacity:20
        logger.info("position:{},limit:{},capacity:{}",
                intBuffer.position(),
                intBuffer.limit(),
                intBuffer.capacity());
        while (intBuffer.hasRemaining()) {
            System.out.print(intBuffer.get());
        }
    }
    
  7. clear() : 一旦缓冲区完成填充并释放,它就可以被重新使用了,clear()函数将缓冲区重置为空状态,即切换到写模式。它不改变缓冲区中的任何元素(不清空数据),而是仅仅将limit设为capacity,并把position设置为0

    public class BufferClearTest {
        // 释放缓冲区
        public static void drainBuffer(CharBuffer buffer) {
            // hasRemaining会在释放缓冲区时告诉您是否已经达到缓冲区的limit
            while (buffer.hasRemaining()) {
                System.out.println(buffer.get());
            }
            System.out.println("=====释放缓冲区结束=====");
        }
    
        // 释放缓冲区 第二种方式,此方法比较高效,因为limit不会在每次循环重复时都被检查
        public static void drain2Buffer(CharBuffer buffer) {
            // remaining()将返回从当前position到limit还剩余的元素数目。
            int count = buffer.remaining();
            for (int i = 0; i < count; i++) {
                System.out.println(buffer.get());
            }
        }
    
        // 填充缓冲区
        private static void fillBuffer(CharBuffer charBuffer) {
            String str = "this is buffer data";
            for (int i = 0; i < str.length(); i++) {
                charBuffer.put(str.charAt(i));
            }
            System.out.println("=====填充缓冲区结束=====");
        }
    
        public static void main(String[] args) {
            CharBuffer buffer = CharBuffer.allocate(100);
            // 写入数据
            fillBuffer(buffer);
            // 翻转(写模式转读模式)
            buffer.flip();
            // 读取数据
            drainBuffer(buffer);
            // 重置缓冲区,让缓冲区可以写入
            buffer.clear();
            //此时缓冲区中还是有内容的
            System.out.println("clear缓冲区后,缓冲区中的内容是" + buffer.get());
        }
    
    }
    
  8. compact():有时只想从缓冲区中释放一部分数据,而不是全部,然后重新填充,为了实现这一点,未读的数据元素需要下移以使第一个元素索引为 0,尽管重复这样做效率很低,但有时非常必要,API有一个compact()方法,此方法在复制数据的时候要比使用get()put()函数高效的多.(读取一部分Buffer,将剩下的部分整体移动到Buffer的头部)

    • 源码

      HeapByteBuffer 的compact()源码
      public ByteBuffer compact() {
          System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
          position(remaining());
          limit(capacity());
          discardMark();
          return this;
      }
      
    • 过程图

  9. mark():使Buffer能够记住一个位置并在之后将其返回。缓冲区的标记在mark()函数被调用之前都是未定义的,调用时标记被设为当前位置的值。reset()将位置设为当前的标记值。如果标记值未定义,调用reset()将导致InvalidMarkException异常。一些Buffer函数会抛弃已经设定的标记(rewind() clear flip 总是抛弃标记).如果新设置的值比当前的标记小,调用limit()position()带有索引参数的版本会抛弃标记。

  10. equals():所有的缓冲区都提供了equals来测试两个缓冲区是否相等。如果每个缓冲区中剩余的内容相同,则equals返回true,否则返回false。两个缓冲区被认为相等的充要条件

    • 两个对象类型相同
    • 两个对象都剩余相同数量的元素。Buffer的容量不需要相同,缓冲区剩余数据的索引也不必相同,但是每个缓冲区中剩余元素的数目(从positionlimit)必须相同
    • 在每个缓冲区中应被get()函数返回的剩余数据元素序列必须一致
  11. duplicate()与Slice():复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。slice()方法获取的是原ByteBuffer的position-limit之间的内容,和原内容相互影响,原内容的position和limit不受影响。duplicate()方法获取的是原ByteBuffer所有的内容,包括原ByteBuffer的mark,position,limit,capacity值,和原内容相互影响,源内容的position和limit不受影响

    public class BufferDuplicateTest {
        private static final Logger logger = LoggerFactory.getLogger(BufferDuplicateTest.class);
    
        @Test
        public void testDuplicate() {
            CharBuffer buffer = CharBuffer.allocate(10);
            buffer.put("abcde");
            CharBuffer buffer1 = buffer.duplicate();
            buffer1.clear();
            buffer1.put("efghijk");
            //position=5, limit=10, capacity=10,content=efghijk...
            showBuffer(buffer);
            //position=7, limit=10, capacity=10,content=efghijk...
            showBuffer(buffer1);
        }
    
        @Test
        public void testSlice() {
            CharBuffer buffer = CharBuffer.allocate(10);
            buffer.put("abcde");
            CharBuffer buffer1 = buffer.slice();
            buffer1.clear();
            //通过slice创建的新缓冲区只能操作原始缓冲区中数组剩余的数据
            //即索引为调用slice方法时原始缓冲区的position到limit索引之间的数据,
            // 超出这个范围的数据通过slice创建的新缓冲区无法操作到。
            buffer1.put("efghi");
            //position=5, limit=10, capacity=10,content=abcdeefghi
            showBuffer(buffer);
            //position=5, limit=5, capacity=5,content=efghi
            showBuffer(buffer1);
    
        }
    
        private static void showBuffer(CharBuffer buffer) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < buffer.limit(); i++) {
                char c = buffer.get(i);
                if (c == 0) {
                    c = '.';
                }
                sb.append(c);
            }
            System.out.printf("position=%d, limit=%d, capacity=%d,content=%s\n",
                    buffer.position(), buffer.limit(), buffer.capacity(), sb.toString());
        }
    
    }
    

Scatter/Gather

  1. 分散(scatter)Channel中读取:在读操作时将读取的数据写入多个Buffer
  2. 聚集(gather)写入Channel:在写操作时将多个Buffer的数据写入同一个Channel
  3. 通过让JVM进程在单个系统调用中向操作系统传递一个缓冲区地址列表,可以使数据汇编/反汇编任务更加高效。然后,操作系统按顺序填充或排出(fills or drains),在读取操作期间将数据分散(scattering)到多个缓存区,或者写入操作期间从多个缓冲区收集(gathering)数据。这种分散(scatter)/收集(gather)活动减少了JVM进程必须进行的(可能昂贵的)系统调用的数量,并允许操作系统优化数据处理,因为它知道缓冲区空间的总量。此外当多个处理器或核可用时,操作系统可以允许缓冲区同时填充(filled)排出(drained)

ByteBuffer

  1. 继承图

FileChannel

  1. FileChannel的write 和 read 方法均是 线程安全 的。一般情况下FileChannel 只有在一次写入 4kb 的整数倍时,才能发挥出实际的性能。
    • filechannel.write仅仅是将数据写入到PageCache中,由操作系统完成真正的磁盘写入
    • filechannel.read是从磁盘->PageCache-> 应用内存
    • FileChannel.force()用于通知操作系统进行及时刷盘

MappedByteBuffer

内存映射原理

  1. 术语

    • MMC:CPU的内存管理单元。
    • 基址:程序所在区域的起始地址称为(程序)基址
    • 物理内存:即内存条的内存空间。
    • 虚拟内存:计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
    • 页面文件:操作系统反映构建并使用虚拟内存的硬盘空间大小而创建的文件,在windows下,即pagefile.sys文件,其存在意味着物理内存被占满后,将暂时不用的数据移动到硬盘上。
    • 缺页中断:当程序试图访问已映射在虚拟地址空间中但未被加载至物理内存的一个分页时,由MMC发出的中断。如果操作系统判断此次访问是有效的,则尝试将相关的页从虚拟内存文件中载入物理内存。
  2. 虚拟内存与物理内存

    • 如果正在运行的一个进程,它所需的内存是有可能大于内存条容量之和的,如内存条是256M,程序却要创建一个2G的数据区,那么所有数据不可能都加载到内存(物理内存),必然有数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,再调度进入物理内存。
  3. 什么是虚拟内存地址和物理内存地址?

    • 假设你的计算机是32位,那么它的地址总线是32位的,也就是它可以寻址00xFFFFFFFF(4G)的地址空间,但如果你的计算机只有256M的物理内存0x0x0FFFFFFF(256M),同时你的进程产生了一个不在这256M地址空间中的地址,那么计算机该如何处理呢?回答这个问题前,先说明计算机的内存分页机制。

    • 计算机会对虚拟内存地址空间(32位为4G)进行分页产生页(page),对物理内存地址空间(假设256M)进行分页产生页帧(page frame),页和页帧的大小一样,所以虚拟内存页的个数势必要大于物理内存页帧的个数。在计算机上有一个页表(page table),就是映射虚拟内存页到物理内存页的,更确切的说是页号到页帧号的映射,而且是一对一的映射。问题来了,虚拟内存页的个数 > 物理内存页帧的个数,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?不是的,操作系统是这样处理的。操作系统有个页面失效(page fault 也叫页面中断)功能。操作系统找到一个最少使用的页帧,使之失效,并把它写入磁盘,随后把需要访问的页放到页帧中,并修改页表中的映射,保证了所有的页都会被调度。

  4. 现在来看看什么是虚拟内存地址和物理内存地址:

    • 虚拟内存地址:由页号(与页表中的页号关联)和偏移量(页的小大,即这个页能存多少数据)组成。

    • 举个例子,有一个虚拟地址它的页号是4,偏移量是20,那么他的寻址过程是这样的:首先到页表中找到页号4对应的页帧号(比如为8),如果页不在内存中,则用失效机制调入页,接着把页帧号和偏移量传给MMC组成一个物理上真正存在的地址,最后就是访问物理内存的数据了。

简介

  1. MappedByteBuffer 将文件直接映射到内存(这里的内存指的是虚拟内存,并不是物理内存)

  2. FileChannel提供了map()方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。当通过map()方法建立映射关系之后,就不依赖于用于创建映射的文件通道(Channel)。 特别是,关闭通道(Channel)对映射的有效性没有影响(map方法的文档说明: A mapping, once established, is not dependent upon the file channel that was used to create it. Closing the channel, in particular, has no effect upon the validity of the mapping. )。即映射之后MappedByteBuffer访问的是一块内存,什么时候从将内存的修改同步到磁盘上是不确定的

  3. mmap出来的MappedByteBuffer会作为page cache的一部分,MappedByteBuffer本质是一个抽象类,map()方法返回的是一个DirectByteBuffer实例

  4. MapMode mode:内存映像文件访问的方式:

    • MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出ReadOnlyBufferException异常。
    • MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的。
    • MapMode.PRIVATE:私用,可读可写,所做的任何修改都会产生一个私有的数据副本并且只有当前MappedByteBuffer实例才能看到,不会对底层文件作任何修改,这种能力称之为copy on write(写时复制)
  5. FileChannel没有提供公开的unmap()方法(私有)释放内存,如果想要释放内存需要如下方式:

    // 第一种方式: 在关闭资源时执行以下代码释放内存
    Method m = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);
    m.setAccessible(true);
    m.invoke(FileChannelImpl.class, buffer);
    
    //第二种方式:让MappedByteBuffer自己释放本身持有的内存
    AccessController.doPrivileged(new PrivilegedAction() {
        public Object run() {
          try {
            Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);
            getCleanerMethod.setAccessible(true);
            sun.misc.Cleaner cleaner = (sun.misc.Cleaner)
            getCleanerMethod.invoke(byteBuffer, new Object[0]);
            cleaner.clean();
          } catch (Exception e) {
            e.printStackTrace();
          }
          return null;
        }
    });
    
    两种方式的本质都是调用
    private static void unmap(MappedByteBuffer bb) {
        Cleaner cl = ((DirectBuffer)bb).cleaner();
        if (cl != null)
            cl.clean();
    }
    
    
    

DirectByteBuffer

  1. DirectByteBuffer是Java用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁

  2. DirectByteBuffer的父类Buffer有个address属性。address只会被直接缓存给使用到。之所以将address属性升级放在Buffer中,是为了在JNI调用GetDirectBufferAddress时提升它调用的速率,
    address表示分配的堆外内存的地址。java通过Unsafe类native allocateMemory()本地方法创建直接缓冲区,此方法会返回堆外内存基地址(long),并赋值给address

    //ByteBuffer类
    
    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;
    
    public static ByteBuffer allocateDirect(int capacity) {
          return new DirectByteBuffer(capacity);
      }
      new DirectByteBuffer(capacity)通过 Unsafe类的本地方法 public native long allocateMemory(long bytes)创建直接内存缓冲区
    
  3. 直接内存不属于GC管辖范围,DirectByteBuffer属于java类,适当的时候会被GC回收,当它回收前会调用native方法把直接内存释放,即本地内存可以随DirectByteBuffer对象被GC回收而自动被操作系统回收。但是如果不断分配本地内存,堆内存很少使用,此时JVM并不会执行GC,此时DirectByteBuffer对象就不会被GC回收,此时会出现堆内存充足,但是本地内存不足的情况,会导致OutOfMemoryError

  4. DirectByteBuffer私有构造

    // Primary constructor
    //
    DirectByteBuffer(int cap) {                   // package-private
    
        // mark, pos, lim, cap
        super(-1, 0, cap, cap);
        // 内存是否按页分配对齐
        boolean pa = VM.isDirectMemoryPageAligned();
        // 获取每页内存大小
        int ps = Bits.pageSize();
        // 分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        // 用Bits类保存总分配内存(按页分配)的大小和实际内存的大小
        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内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的线程对象(Runnable thunk),回收操作是发生在Cleaner的clean()中。当DirectByteBuffer被回收时,堆外内存也会被释放
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
    
  5. Clean的clean方法

    /**
     * Runs this cleaner, if it has not been run before.
     */
    public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
    
  6. DirectByteBuffer通过full gc来回收内存的,DirectByteBuffer会自己检测情况而调用system.gc(),但是如果参数中使用了-XX:+DisableExplicitGC那么就无法回收该块内存了,-XX:+DisableExplicitGC标志自动将System.gc()调用转换成一个空操作,就是应用中调用System.gc()会变成一个空操作。那么如果设置了就需要我们手动来回收内存了。还有一种情况,CMS GC会回收DirectByteBuffer的内存,CMS主要是针对old space空间的垃圾回收

    @Test
    public void testAllocateDirector() throws Exception{
        ByteBuffer buffer=ByteBuffer.allocateDirect(1024);
        Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
        cleanerField.setAccessible(true);
        Cleaner cleaner = (Cleaner) cleanerField.get(buffer);
        cleaner.clean();
    }
    

使用google-perftools

  1. mac安装与使用

     # brew install google-perftools
     在项目的启动之前配置两个环境变量
     # export  DYLD_INSERT_LIBRARIES=/usr/local/Cellar/gperftools/2.7/lib//usr/local/Cellar/gperftools/2.7/lib/libtcmalloc.dylib 指定动态链接库(mac环境的)
     # export HEAPPROFILE=/Volumes/O/a.log 指定导出的文件目录
     # pprof -text $JAVA_HOME/bin/java a.log.0001.heap >> result.txt
    
  2. Centos安装

    # yum install autoconf automake libtool libunwind -y
    # wget https://github.com/gperftools/gperftools/releases/download/gperftools-2.7/gperftools-2.7.tar.gz
    # tar -zxvf gperftools-2.7.tar.gz -C /usr/local
    # c -2.7
    # ./configure --prefix=/usr/local/gperftools/ && make && make install
    # rpm -qa| grep libunwind
        libunwind-1.2-2.el7.x86_64
    # rpm -ql libunwind-1.2-2.el7.x86_64     
        /usr/lib64/libunwind-coredump.so.0
        /usr/lib64/libunwind-coredump.so.0.0.0
        /usr/lib64/libunwind-x86_64.so.8
        /usr/lib64/libunwind-x86_64.so.8.0.1
        /usr/lib64/libunwind.so.8
        /usr/lib64/libunwind.so.8.0.1
        /usr/share/doc/libunwind-1.2
        /usr/share/doc/libunwind-1.2/COPYING
        /usr/share/doc/libunwind-1.2/NEWS
        /usr/share/doc/libunwind-1.2/README 
    # vim /etc/ld.so.conf.d/usr_local_lib.conf  加入/usr/lib64/(libunwind的lib所在目录)
    # sudo /sbin/ldconfig   使libunwind生效
    
    # export LD_PRELOAD=/usr/local/gperftools/lib/libtcmalloc.so
    # export HEAPPROFILE=/root/Desktop/a.log
    # pprof -text $JAVA_HOME/bin/java a.log.0001.heap >> result.txt  
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值