java nio—buffer的简单介绍以及堆外内存的分析

作用

NIO提供了一系列buffer类,用作缓存。可以直接从channel中读数据到buffer,也可以从buffer中写数据到channel。缓冲区本质上是一块固定大小的内存,其作用是一个存储器或运输器。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

类图

在这里插入图片描述

Buffer的四个属性

  • 容量(capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变
  • 上界(limit):缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数
  • 位置(position):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新
  • 标记(mark):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。标记在设定前是未定义的(undefined)。这四个属性之间总是遵循以下关系:0 <= mark <= position <= limit <= capacity

Buffer的基本用法

使用Buffer读写数据一般遵循以下四个步骤:

  1. 写入数据到Buffer
  2. 调用flip()方法
  3. 从Buffer中读取数据
  4. 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
举个栗子:
定义一个容量是10的buffer,填入hello:
在这里插入图片描述
这时候,limit代表,最多可以写多少,position代表,即将写入的位置
然后进行flip:
在这里插入图片描述
flip之后,limit代表最多可以读多少,position,代表开始读的位置。

三种类型buffer

java nio提供了三种不同的buffer,HeapByteBuffer、DirectByteBuffer、MappedByteBuffer。
一些不同:

  • HeapByteBuffer是在jvm堆上申请的内存,而DirectByteBuffer、MappedByteBuffer是在堆外申请的内存。
  • MappedByteBuffer借助了mmap(内存映射文件),提高了文件读取效率

transferTo:适用于应用程序无需对文件数据进行任何操作的场景;
map:适用于应用程序需要操作文件数据的场景;

HeapByteBuffer

在java堆上申请的内存。通过ByteBuffer.allocate方法申请jvm堆上内存。
看下ByteBuffer.allocate这个方法:

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

直接new了一个HeapByteBuffer对象,下面看下这个构造方法:

    HeapByteBuffer(int cap, int lim) {            // package-private

        super(-1, 0, lim, cap, new byte[cap], 0);
        /*
        hb = new byte[cap];
        offset = 0;
        */
    }

构造方法中调用了父类,ByteBuffer的构造方法

super(-1, 0, lim, cap, new byte[cap], 0);

其中new byte[cap],这里就能够确定,HeapByteBuffer申请的内存,确实是在jvm堆上。

DirectByteBuffer

在jvm堆外(OS堆上)申请的内存。通过ByteBuffer的allocateDirect申请。
看下ByteBuffer的allocateDirect方法:

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

直接new 了一个DirectByteBuffer对象,看下DirectByteBuffer的构造方法

    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;
    }

其中申请内存的操作是通过unsafe类进行的

base = unsafe.allocateMemory(size);

Unsafe的allocateMemory方法通过c的malloc方法【底层依赖两个系统调用,一个brk,一个mmap】进行进程堆上内存的分配。下面,我们验证下这个想法:
这里用到了几个涉及到jvm监控和linux内存监控的知识:

  • Native Memory Tracking(NMT) jdk7提供的内存工具,跟踪JVM内部的内存使用
  • linux的/proc/pid/maps 文件,可以查看进程的虚拟地址空间是如何使用的。

这里不对这两块知识进行再补充,只注重分析内存情况。

测试代码:

    public static void main(String[] args) throws Exception {
        ByteBuffer b = ByteBuffer.allocateDirect(1024*1024*50);
        //反射获取Buffer中 的address属性
        Field field = b.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("address");
        //打开私有访问
        field.setAccessible(true);
        System.out.println(field.get(b).toString());
        while (true) {
            Thread.sleep(10000);
        }
    }

代码中申请了50M的堆外内存。而且会打印出native代码申请的内存的地址起始地址。
java命令参数如下:注意下-Xms20m -Xmx100m

java -Djava.rmi.server.hostname=49.234.60.90 -Dcom.sun.management.jmxremote.port=2990 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:NativeMemoryTracking=detail -Xms20m -Xmx100m  NonBlockServer

在这里插入图片描述
这里打印出了申请的地址的虚拟内存地址起始地址:140260761661472,转换成16进制是,7f9100dff020

下面看下NMT的情况:
着重分析下jvm堆内存情况:
在这里插入图片描述
这里,说明java堆内存申请了100M,当前使用的为20M。
下面看具体的堆内存信息:
在这里插入图片描述
这里堆内存的虚拟内存地址为: 0x00000000f9c00000 - 0x0000000100000000

下面看下,linux的java进程的内存情况(通过查看/proc/pid/maps 文件):
在这里插入图片描述
上文中,我们打印的申请的内存的起始地址为7f9100dff020。
乍一看,maps文件并没有这个起始地址,但是注意这样一个内存块:7f9100dff000-7f9104000000。7f9100dff020刚好在这个区间中。但是7f9100dff000-7f9100dff020这块多申请的内存,不知道是做什么用的。

至此,我们验证了,ByteBuffer的allocateDirect申请的内存并不在jvm堆上,而是在进程堆内存中。

MappedByteBuffer

java对内存文件映射的支持。
内存文件映射的原理:
普通的read io操作原理:
在这里插入图片描述
mmap操作的原理:
在这里插入图片描述
mmap的核心是,通过使得内核空间和用户空间的虚拟地址映射到同一块物理内存上,进而减少了文件在内核空间和用户空间的拷贝。

下面用一个例子,简单看下,MappedByteBuffer在内存文件映射操作时的java进程内存和jvm内存情况.

   public static void main(String[] args) throws Exception {
        String path = ModelDubboService.class.getClassLoader().getResource("test.txt").getPath();
        File file = new File(path);
        FileChannel fileChannel = new FileInputStream(file).getChannel();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
        //反射获取Buffer中 的address属性
        Field field = mappedByteBuffer.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("address");
        //打开私有访问
        field.setAccessible(true);
        System.out.println(field.get(mappedByteBuffer).toString());

        String path2 = ModelDubboService.class.getClassLoader().getResource("test2.txt").getPath();
        FileInputStream fileInputStream = new FileInputStream(file);
        byte[] bytes = new byte[1024*2014*40];
        fileInputStream.read(bytes);

        while(true){
            Thread.sleep(10000);
        }
    }

主要是看下,通过普通的读文件操作和mmap方式读文件有什么区别。test.txt使用内存映射的方式来读,test2.txt使用普通的read方法来读。
java命令参数如下

java -Djava.rmi.server.hostname=49.234.60.90 -Dcom.sun.management.jmxremote.port=2990 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:NativeMemoryTracking=detail -Xms20m -Xmx100m  TestMMap

获取打印的内存地址:
在这里插入图片描述
地址139907108438016,转换成16进制,7f3ea9800000

下面看下jvm堆内存情况:
在这里插入图片描述
jvm最大内存是200M,使用了166M左右。
在这里插入图片描述
获取jvm堆内存的虚拟地址:0x00000000f3800000 - 0x0000000100000000

下面看下linux进程的内存(/proc/pid/maps):
在这里插入图片描述
为了再验证下,到底是不是通过mmap读取的文件,我们再看下/proc/pid/map_files这个文件:
在这里插入图片描述
这里就可以确认,test.txt确实是通过mmap读取的,而text2.txt通过一般的read来读取的,已经从pageCache拷贝到jvm堆上了。

这里总结几个java通过mmap方式相比于普通read方式的有点:

  • 不使用jvm堆内存,使用堆外内存,不会对jvm内存造成很大影响
  • mmap方式相比read,减少了一次拷贝操作(内核空间->用户空间),速度更加快

同样也有一些需要注意的地方:

  • mmap使用的堆外内存的回收问题
  • mmap适用于不修改文件的场景,如果需要修改文件,则还是要把文件拷贝到jvm堆内存中去操作
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值