Java开发分布式系统的编码技巧
PageCache 调优和 Direct IO
应用程序读取文件,会经过应用缓存、PageCache、DISK(硬盘)三层。
Linux内核读取到文件数据后,会把它缓存一段时间,这个文件缓存就是PageCache。它会进行适当的预读,比如用户当前只需要读取1kb的文件,但是它的算法觉得读取16k或者更多更合适,那么它就会读取16kb,加载到PageCache中,下次读取先去PageCache中查找。
但是以下三种情况没法使用PageCache:
使用 FIleChannel 读写时,底层可能走 Direct IO,不走页缓存。
在内存有限或者不够用的时候,频繁换页,导致缓存命中率低。
大量随机读的场景,导致页缓存的数据无法命中。
一种解决思路是:通过使用Direct IO 来模拟实现PageCahce的效果。原先PageCache的底层实现,是由操作系统实现的,比如说数据加载,缓存命中,换页,刷盘等,我们无法控制。我们可以通过自定义 Cache + Direct IO 来实现自己可控的操作。
FileChannel 和 mmap
Java 原生的 IO 主要可以分为普通 IO、FileChannel(文件通道)、mmap(内存映射)三种。
java.io 包中的 FileWriter 和 FileReader 属于普通 IO,java.nio 包中的 FileChannel 属于 NIO 的一种,mmap 是调用 FileChannel.map() 实例出来的一种特殊读写文件的方式,被称为内存映射。
FileChannel
FileChannel 大多数时候是和 ByteBuffer 打交道的,ByteBiffer是byte[]的一个封装类,ByteBuffer 是在应用内存中的,它和硬盘之间还隔着一层 PageCache。从使用上看,我们通过 filechannel.write 写入数据时,会将数据从应用内存写入到 PageCache,此时便认为完成了落盘操作。但实际上,操作系统最终帮我们将 PageCache 的数据自动刷到了硬盘。
mmap
mmap 是一个把文件映射到内存的操作,因此可以像读写内存一样读写文件。它省去了用户空间到内核空间的数据复制过程,从而提高了读写性能。
从经验来看,mmap 在内存充足、数据文件较小且相对固定的场景下,性能比 FileChannel 高。但它有这样几个缺点:
使用时必须先指定好内存映射的大小,并且一次 Map 的大小限制在 1.5G 左右。
是由操作系统来刷盘的,手动刷盘时间不好掌握。
回收非常复杂,需要手动释放,并且代码和实现很复杂。
在消息队列数据文件分段的场景下,因为每个段文件的大小是固定的,且大小还是可配置的,所以是可以使用 mmap 来提高性能的。
直接内存(堆外)和堆内内存
堆内和堆外的堆是指 JVM 堆,堆内内存就是指 JVM 堆内部的内存空间,堆外就是指除了 JVM 堆以外的内存空间。堆内内存加上堆外内存等于总内存。

如何选择堆外内存和堆内内存:
- 当需要申请大块的内存时,堆内内存会受到限制,可以尝试分配堆外内存
- 堆外内存适用于生命周期中等或较长的对象
- 堆内内存刷盘的过程中,还需要复制一份到堆外内存,多了一步,会降低性能
- 创建堆外内存的消耗要大于创建堆内内存的消耗,所以当分配了堆外内存之后,要尽可能复用它
- 可以使用池化 + 堆外内存的组合方式,比如代码中如果需要频繁 new byte[],就可以研究一下 ThreadLocal 和 ThreadLocal<byte[]> 的使用机制。