CloudNativeEra
  • Introduction
  • 名词解释
  • Computer Science
    • Computer Organization
      • CPU
      • 二进制、电路、加法器、乘法器
      • 编译、链接、装载
      • 存储器
      • IO
    • Operating System
      • 操作系统基础知识
      • 系统初始化
      • 进程管理
      • Everything about Memory
      • 文件系统
      • 并行编程
      • Linux
        • CPU
        • IO 多路复用
        • DMA IO and Linux Zero Copy
    • Computer Network
      • 网络相关命令
      • 评估系统的网络性能
      • 网络抓包
      • Linux 最多支撑的 TCP 连接
      • 网络虚拟化
      • DHCP 工作原理
    • Data Structure and Algorithm
      • 题目列表
      • Summarize
        • 方法总结
        • 二分思想
        • 树的演化
        • 算法思想总结
      • Data Structure
        • Data Struct - Array
        • Tree
        • Heap
        • Hash
        • 字符串
      • Algorithm
        • Sorting Algorithm
        • 查找
        • 贪心算法
        • 动态规划
        • 位运算
      • Practice Topics
        • Data Struct in SDK
        • Topic - Tree
        • Topic - Graph
        • Topic - 滑动窗口
        • 剑指 Offer 题解
    • 并发编程
      • 并发模式
      • 并发模型
  • 系统设计
    • 软件设计
      • 软件架构
      • 编程范式
      • 系统设计题
      • 设计原则
      • 计算机程序的构造和解释 SICP
    • 领域驱动设计
      • 应用:在线请假考勤管理
      • 应用: library
    • 微服务与云原生
      • Designing and deploying microservices
      • 容器技术
      • Docker
      • Etcd
      • Kubernetes
        • Kubernetes - Mapping External Services
      • Istio
      • 监控
    • 分布式系统
      • 分布式理论
      • 分布式事务
    • 后端存储设计
      • 缓存设计
      • 数据库架构设计
    • CI/CD
    • 设计最佳实践
    • 测试
    • 安全
    • 综合
      • 开发实践
      • 分布式锁
      • 分布式计数服务
      • 弹幕系统设计
      • 消息队列设计
      • 分布式ID生成算法
      • 限流设计
      • 网关设计
      • 通用的幂等设计
      • 分布式任务调度
        • Timer
        • ScheduledExecutorService
        • Spring Task
        • Quartz
      • 交易系统
      • 权限设计
  • 编程语言
    • 编程语言
    • C & C++
    • Java
      • JVM
        • JVM Bytecode
      • Java 核心技术
      • Java 8 新特性
      • Java 集合框架
      • Java NIO
      • 并发编程
        • 线程生命周期与线程中断
        • 三个线程交替打印
        • 两个线程交替打印奇偶
        • 优雅终止线程
        • 等待通知机制
        • 万能钥匙:管程
        • 限流器
        • 无锁方案 CAS
    • Java 源码阅读
      • Unsafe
      • 异步计算 Future
      • Java Queue
      • CoalescingRingBuffer 分析
      • Java Collections
        • PriorityQueue 分析
        • HashMap 分析
        • TreeMap
    • Golang
    • Python
  • 框架/组件/类库
    • Guava
      • Guava Cache
      • Guava EventBus
    • RxJava
    • Apache MINA
    • Netty
      • 网络 IO 模型
      • Netty 生产问题
    • Apache Tomcat
    • MyBatis
    • 限流框架
    • Spring Framework
      • Spring Core
      • Spring 事务管理
    • Spring Boot
    • Spring Cloud
      • Feign & OpenFeign
      • Ribbon
      • Eurake
      • Spring Cloud Config
    • FixJ
    • Metrics
    • Vert.x
  • 中间件
    • Redis
      • Redis 基础
        • Redis 数据结构设计与实现
        • Redis 高性能网络模型
      • Redis checklist
      • 应用案例 - Redis 数据结构
      • 应用案例 - Redis 缓存应用
      • 应用案例 - Redis 集群
      • Redis 客户端
      • Redis 生产案例
        • [译] 在 Redis 中存储数亿个简单键值对
    • MySQL
      • MySQL 基础
      • MySQL Index
      • MySQL Transaction
      • MySQL 优化
      • MySQL 内核
      • MySQL Command
      • MySQL Checklist
      • MySQL Analysis Tool
      • 实现 MySQL
    • State Machine
    • 数据库连接池
    • MQ
      • 高性能内存队列 Disruptor
      • Kafka
      • Pulsar
      • RocketMQ
        • Broker 的设计与实现
      • NSQ
  • 实际案例
    • 线上 Case
      • Request Aborted
      • MySQL - Specified key was too long
      • Java 应用 CPU 100% 排查优化
      • 频繁 GC 导致的 Java 服务不响应
      • 导出优化
  • 大数据
    • 流计算
    • Flink
  • 其他
    • 工具
    • 读书
      • 设计数据密集型应用
      • 实现领域驱动设计
      • 精通比特币
      • 提问的智慧
    • 论文
    • 工程博客
    • 阅读源码
    • 面试
      • 如何在最短的时间里对对方有个全面的了解
    • 分享
    • 软技能
    • Todo
  • Blog
    • #算法
      • 查找
      • 位运算
      • 树
    • #架构
      • 1- 通信
    • Design & Dev & Opt
      • High Performance Data structure Design
  • Tiny Project
    • A Simple WeChat-like Instant Messaging Platform
由 GitBook 提供支持
在本页
  • ByteBuffer
  • FileChannel
  • MappedByteBuffer
  • mmap

这有帮助吗?

  1. 编程语言
  2. Java

Java NIO

关于 Java NIO

上一页Java 集合框架下一页并发编程

最后更新于4年前

这有帮助吗?

ByteBuffer

Buffer 在 Java 里表示一块缓冲区,是一个线性的、指定原始类型的有限元素序列;Buffer 的本质是:capacity, limit, position。可以这么理解 Buffer 是在 Java 中实现的对一块连续内存的读写封装,它提供了一些列的接口来操作这块内存,这个内存在 Java 里可以抽象的理解为一个原始类型的数组,如 ByteBuffer 是对字节数组 byte[] 的封装。

要更深入的理解 Buffer,还需要下钻到操作系统层面,也就是 Buffer 如何分配内存以及该部分内存如何被回收,在抽象类 Buffer 中并没有定义,需要看具体的子类实现,也就是说 Buffer 的抽象并不关心是堆内存还是直接内存,依赖于具体的实现,Buffer 只关注它自己要做的事情(就是维护一块内存区域可读可写的范围、位置和上限等), Buffer 中重要的接口:

public abstract class Buffer {
    public final int position() {...}
    public final Buffer position(int newPosition){...}
    public final Buffer limit(int newLimit) {...}
    public final Buffer mark() {...}
    public final Buffer reset() {...}
    public final Buffer clear() {...}
    public final Buffer flip() {...}
    ...
}

ByteBuffer 是 Buffer 中的诸多实现中使用频率最高的。它同时扩展了 Buffer,提供了读写 Buffer 的接口

public abstract byte get();
public abstract byte get(int index);
public abstract ByteBuffer put(byte b);
public abstract ByteBuffer put(byte b);
...

在 ByteBuffer 的实现中,有两类实现:堆内存 Buffer 和直接内存 Buffer

  • 堆内存就不说了,直接在堆上分配,受 JVM 的垃圾回收机制管理,也同样占用堆内存的大小

  • 直接内存,也叫堆外内存,可以通过 JVM 参数 -XX:MaxDirectMemorySize 来限制,默认堆外内存大小是-Xmx减去一个Survivor区的内存量

使用堆外内存有两个点需要关注,如何分配堆外内存和如何回收对外内存

// 分配堆外内存
ByteBuffer.allocateDirect(capacity);

// 调用下面的方法
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

// DirectByteBuffer 的构造函数里
DirectByteBuffer() {
    // ....
    try {
        // 调用 unsafe.allocateMemory 直接分配内存
        // 这里调用了 OS 提供的接口,在 Linux 下是 malloc 系统调用函数
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    
    // ...

}

但是在回收堆外内存时,使用了 Cleaner,这里比较有意思,最终 Cleaner 是被 Reference Handler 线程监控,去调用 cleaner.clean() 方法,clean 方法中调用的是 trunk.run(),在 DirectByteBuffer 的场景里就是 Deallocator.run(),Deallocator 实现了 Runnable 接口

// 在 DirectByteBuffer 的构造函数里有如下定义
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

// 在 Reference.java 中启动了一个 Reference Handler 线程
public abstract class Reference<T> {
     static {
         Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
     }
}

// ReferenceHandler.java
public void run() {
    while (true) {
        tryHandlePending(true);
    }
}

static boolean tryHandlePending(boolean waitForNotify) {
    //...
    // Fast path for cleaners
    if (c != null) {
        c.clean();
        return true;
    }
    //...
}

// Cleaner.clean()
try {
    // thunk 就是创建 Cleaner 对象时的 Runnable 实例
    this.thunk.run();
} ....

// Deallocator
class Deallocator {
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        // 调用 unsafe 的 freeMemory
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
}

从整个过程来看,堆外内存的回收由 ReferenceHandler 线程来控制,当然我们也可以手动回收 directByteBuffer.getCleaner().clean() 。

总结

ByteBuffer 没有什么神秘的,不管是堆内还是堆外,对于 Buffer 本身而言只是被 JVM 管理的方式不同,以及占用的内存区域是不一样的;此外,堆外内存和堆内的一个不同是在使用时,在 read 和 write 时可能会少一些内存 copy,比如 fileChannel 的 transferTo 和 transferFrom

  1. 当需要申请大块的内存时,堆内内存会受到限制,只能分配堆外内存。

  2. 堆外内存适用于生命周期中等或较长的对象。(如果是生命周期较短的对象,在 YGC 的时候就被回收了,就不存在大内存且生命周期较长的对象在 FGC 对应用造成的性能影响)。

  3. 同时,还可以使用池 + 堆外内存 的组合方式,来对生命周期较短,但涉及到 I/O 操作的对象进行堆外内存的再使用 (Netty 中就使用了该方式)。在比赛中,尽量不要出现在频繁 new byte[] ,创建内存区域再回收也是一笔不小的开销,使用 ThreadLocal<ByteBuffer> 和 ThreadLocal<byte[]> 往往会给你带来意外的惊喜 ~

  4. 创建堆外内存的消耗要大于创建堆内内存的消耗,所以当分配了堆外内存之后,尽可能复用它。

FileChannel

MappedByteBuffer

A direct byte buffer whose content is a memory-mapped region of a file. (一个直接的字节缓冲区,其内容是文件的内存映射区域。)

MappedByteBuffer 是通过 FileChannel.map(...) 进行创建的,一个映射的字节缓冲区和它所代表的文件映射一直有效,直到缓冲区本身被垃圾回收;映射的字节缓冲区的内容可以在任何时候改变,例如,如果映射文件的相应区域的内容被这个程序或其他程序改变了。 这种变化是否会发生,以及何时发生,都是取决于操作系统的,因此没有说明。映射的字节缓冲区的全部或部分可能在任何时候变得不可访问,例如如果映射的文件被截断。 试图访问映射的字节缓冲区中不可访问的区域不会改变缓冲区的内容,但会在访问时或以后的某个时间引起一个未指明的异常。 因此,强烈建议采取适当的预防措施,避免本程序或同时运行的程序对映射文件进行操作,但读取或写入文件内容除外。映射的字节缓冲区在其他方面的表现与普通的直接字节缓冲区没有区别。

mmap

要理解 MappedByteBuffer 就需要很好的理解操作系统提供的内存映射技术 mmap,在 Linux 系统中提供了系统调用 mmap 来实现这项技术,mmap 可以将文件或者设备映射到内存中,当调用 mmap 时,在调用进程的虚拟内存空间中会分配一块内存做映射,如果不指定内存映射的起始内存地址,内核会选择一个合理的地址,同时 mmap 需要传入一个要被映射的文件的 fd,当 mmap 调用完成后,就把这个进程的这块虚拟内存映射到了磁盘上的一个具体文件上,它们之间就建立了实际的关联,这个时候就可以关闭掉 fd,因为后续对这块内存的操作就是对磁盘上文件的操作,就不会再去走文件系统的那套流程了;内存映射之间的最小操作单位也是页,映射分为几个阶段:

  1. 进程启动映射过程,并在虚拟地址空间中创建一个虚拟的文件映射区

  2. 调用内核的映射函数,实现物理文件和虚拟地址之间的映射关系

  3. 映射关系建立后,只有在进程实际访问这个虚拟的文件映射区时才会真正的读写磁盘上被映射的文件,如果发现要读写的页不在内存中,就会引发缺页异常,然后从磁盘中把该页数据加载到内存中;这个过程有可能会用到 OS 的 Buffer,因为是直接操作的块设备

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享

资料参考:

关于 unsafe 可以参考 .

堆内内存刷盘的过程中,还需要复制一份到堆外内存,这部分内容可以在 FileChannel 的实现源码中看到细节,至于 Jdk 为什么需要这么做,可以参考另外一篇文章:

Linux IO 基础
Java 中的两种 IO 模型
多种IO模型
彻底看破Java NIO
搞懂 Buffer
Java 源码阅读/Unsafe
《一文探讨堆外内存的监控与回收
mmap 是什么,怎么用
Linux man mmap
Java mmap
Java 中的内存映射
Java File mmap 总结