关于港口码头发展建设的网站,应用开发工程师干什么,专业手表网站,网站开发的技术流程背景#xff1a;
在尼恩视频版本里#xff0c;从架构师视角#xff0c;尼恩为大家彻底介绍 rocketmq 高可用、高并发中间件的原理与实操。 给大家底层的解读清楚 rocketmq 架构设计、源码设计、工业级高可用实操#xff0c;含好多复杂度非常高、又非常核心的概念#xff…背景
在尼恩视频版本里从架构师视角尼恩为大家彻底介绍 rocketmq 高可用、高并发中间件的原理与实操。 给大家底层的解读清楚 rocketmq 架构设计、源码设计、工业级高可用实操含好多复杂度非常高、又非常核心的概念比如 零复制、延迟容错、工业级RPC框架 以横扫全网和史无前例的方式帮助大家彻底掌握、深入骨髓的掌握 rocketmq 成为明年3月份征服面试官的神器 问题why 高可用、高并发中间件的原理、源码与实操 实际的开发过程中很多小伙伴聚焦crud开发环境出了问题都不能启动。 作为开发人员未来的高级开发、架构师或者未来想走向高端开发必须掌握高可用、高并发中间件的原理掌握其实操。 MappedByteBuffer图解秒懂史上最全 这里 作为 rocketmq 高可用、高并发中间件的原理、源码与实操的前置知识以博文的方式 给大家介绍一下 MappedByteBuffer java nio中引入了一种基于MappedByteBuffer操作大文件的方式其读写性能极高本文会介绍其性能如此高的内部实现原理。 注本文以 PDF 持续更新最新尼恩 架构笔记、面试题 的PDF文件请从下面的链接获取码云 内存管理
在深入MappedByteBuffer之前先看看计算机内存管理的几个术语
MMUCPU的内存管理单元。物理内存即内存条的内存空间。虚拟内存计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存一个连续完整的地址空间而实际上它通常是被分隔成多个物理内存碎片还有部分暂时存储在外部磁盘存储器上在需要时进行数据交换。交换空间操作系统反映构建并使用虚拟内存的硬盘空间大小而创建的文件在windows下即pagefile.sys文件其存在意味着物理内存被占满后将暂时不用的数据移动到硬盘上。缺页中断当程序试图访问已映射在虚拟地址空间中但未被加载至物理内存的一个分页时由MMC发出的中断。如果操作系统判断此次访问是有效的则尝试将相关的页从虚拟内存文件中载入物理内存。
为什么会有虚拟内存和物理内存的区别
如果正在运行的一个进程它所需的内存是有可能大于内存条容量之和的如内存条是256M程序却要创建一个2G的数据区那么所有数据不可能都加载到内存物理内存必然有数据要放到其他介质中比如硬盘待进程需要访问那部分数据时再调度进入物理内存。
什么是虚拟内存地址和物理内存地址
假设你的计算机是32位那么它的地址总线是32位的也就是它可以寻址00xFFFFFFFF4G的地址空间但如果你的计算机只有256M的物理内存0x0x0FFFFFFF256M同时你的进程产生了一个不在这256M地址空间中的地址那么计算机该如何处理呢
回答这个问题前先说明计算机的内存分页机制。
计算机会对虚拟内存地址空间32位为4G进行分页page对物理内存地址空间假设256M进行分帧page frame页和页帧的大小一样所以虚拟内存页的个数势必要大于物理内存页帧的个数。
在计算机上有一个页表page table就是映射虚拟内存页到物理内存页的更确切的说是页号到页帧号的映射而且是一对一的映射。 问题来了虚拟内存页的个数 物理内存页帧的个数岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间
不是的操作系统是这样处理的。操作系统有个页面失效page fault功能。操作系统找到一个最少使用的页帧使之失效并把它写入磁盘随后把需要访问的页放到页帧中并修改页表中的映射保证了所有的页都会被调度。
现在来看看什么是虚拟内存地址和物理内存地址
虚拟内存区域由页号与页表中的页号关联和偏移量页的小大即这个页能存多少数据组成。
举个例子有一个虚拟地址它的页号是4偏移量是20那么他的寻址过程是这样的
首先到页表中找到页号4对应的页帧号比如为8如果页不在内存中则用失效机制调入页接着把页帧号和偏移量传给MMC组成一个物理上真正存在的地址最后就是访问物理内存的数据了。
Java中基础MMap的使用 MappedByteBuffer是什么从继承结构上看MappedByteBuffer继承自ByteBuffer内部维护了一个逻辑地址address。 将共享内存和磁盘文件建立联系的是文件通道类FileChannel。
该类的加入是JDK为了统一对外部设备文件、网络接口等的访问方法并且加强了多线程对同一文件进行存取的安全性。
这里只是用它来建立共享内存用它建立了共享内存和磁盘文件之间的一个通道。
FileChannel提供了map方法把文件映射到虚拟内存通常情况可以映射整个文件如果文件比较大可以进行分段映射。
大致的步骤
首先通过 RandomAccessFile获取文件通道。然后通过channel进行内存映射获取一个虚拟内存区域VMA
//通过RandomAccessFile获取FileChannel。
try (FileChannel channel new RandomAccessFile(decodePath, rw).getChannel();) {//通过channel进行内存映射获取一个虚拟内存区域VMAMappedByteBuffer mapBuffer channel.map(FileChannel.MapMode.PRIVATE, 0, length);....channel.map方法的参数
映射类型
MapMode mode内存映像文件访问的方式FileChannel中的几个常量定义共三种
MapMode.READ_ONLY只读试图修改得到的缓冲区将导致抛出异常。MapMode.READ_WRITE读/写对得到的缓冲区的更改最终将写入文件但该更改对映射到同一文件的其他程序不一定是可见的。MapMode.PRIVATE私用可读可写,但是修改的内容不会写入文件只是buffer自身的改变这种能力称之为”copy on write”。
position文件映射时的起始位置。length映射区的长度。长度单位为字节。长度单位为字节
示例1通过MappedByteBuffer读取文件
package com.crazymakercircle.iodemo.fileDemos;import com.crazymakercircle.NioDemoConfig;
import com.crazymakercircle.util.IOUtil;
import com.crazymakercircle.util.Logger;import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;/*** Created by 尼恩 疯创客圈*/
public class FileMmapDemo {/*** 演示程序的入口函数** param args*/public static void main(String[] args) {doMmapDemo();}/*** 读取*/public static void doMmapDemo() {String sourcePath NioDemoConfig.MMAP_FILE_RESOURCE_SRC_PATH;String decodePath IOUtil.getResourcePath(sourcePath);Logger.debug(decodePath decodePath);mmapWriteFile(decodePath);}/*** 读取文件内容并输出** param fileName 文件名*/public static void mmapWriteFile(String fileName) {//向文件中存1M的数据int length 1024;//try (FileChannel channel new RandomAccessFile(fileName, rw).getChannel();) {//一个整数4个字节MappedByteBuffer mapBuffer channel.map(FileChannel.MapMode.READ_WRITE, 0, length);for (int i 0; i length; i) {mapBuffer.put((byte) (Integer.valueOf(a) i % 26));}for (int i 0; i length; i) {if (i % 50 0) System.out.println();//像数组一样访问System.out.print((char) mapBuffer.get(i));}mapBuffer.force();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}
}输出的结果
decodePath/E:/refer/crazydemo/netty_redis_zookeeper_source_code/NioDemos/target/classes//mmap.demo.log abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx
yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst
uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr
stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnop
qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmn
opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl
mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij
klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh
ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef
ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd
efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzab
cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx
yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst
uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr
stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnop
qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmn
opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl
mnopqrstuvwxyzabcdefghijDisconnected from the target VM, address: 127.0.0.1:50970, transport: socketProcess finished with exit code 0示例2通过MappedByteBuffer读取私用映射
私用可读可写,但是修改的内容不会写入文件只是buffer自身的改变这种能力称之为”copy on write”。
/*** 读取文件内容并输出**/
public static void mmapPrivate() {String sourcePath NioDemoConfig.MMAP_FILE_RESOURCE_SRC_PATH;String decodePath IOUtil.getResourcePath(sourcePath);Logger.debug(decodePath decodePath);//向文件中存1M的数据int length 1024;//try (FileChannel channel new RandomAccessFile(decodePath, rw).getChannel();) {//一个整数4个字节MappedByteBuffer mapBuffer channel.map(FileChannel.MapMode.PRIVATE, 0, length);for (int i 0; i length; i) {mapBuffer.put((byte) (Integer.valueOf(a) i % 26));}for (int i 0; i length; i) {if (i % 50 0) System.out.println();//像数组一样访问System.out.print((char) mapBuffer.get(i));}mapBuffer.force();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}执行程序可以看到文件并没有写入的内容。
实例3通过MMap共享内存
共享内存对应应用开发的意义
对熟知UNIX系统应用开发的程序员来说IPCInterProcess Communication机制是非常熟悉的
IPC基本包括共享内存、信号灯操作、消息队列、信号处理等部分是开发应用中非常重要的必不可少的工具。
在所有的IPC中 其中共享内存是关键对于数据共享、系统快速查询、动态配置、减少资源耗费等均有独到的优点。
对应UNIX系统来说共享内存分为一般普通共享内存和文件映射共享内存两种而对应 Windows实际上只有映像文件共享内存一种。
所以java应用中也是只能创建映像文件共享内存。
Java中的共享内存场景
在java语言中基本上没有提及共享内存这个概念但是在某一些应用中共享内存确实非常有用。
例如采用java语言的分布式应用系统中存在着大量的分布式共享对象很多时候需要查询这些对象的状态以查看系统是否运行正常或者了解这些对象的目前的一些统计数据和状态。
如果采用网络通信的方式显然会增加应用的额外负担也增加了一些不必要的应用编程。
而如果采用共享内存的方式则可以直接通过共享内存查看对象的状态数据和统计数据从而减少了一些不必要的麻烦。
共享内存的使用有如下几个特点
可以被多个进程打开访问读写操作的进程在执行读写操作时其他进程不能进行写操作多个进程可以交替对某一共享内存执行写操作一个进程执行了内存的写操作后不影响其他进程对该内存的访问。同时其他进程对更新后的内存具有可见性。在进程执行写操作时如果异常退出对其他进程写操作禁止应自动解除。相对共享文件数据访问的方便性和效率有
共享内存在java中的实现
在jdk1.4中提供的类MappedByteBuffer为我们实现共享内存提供了较好的方法。
该缓冲区实际上是一个磁盘文件的内存映像。二者的变化将保持同步即内存数据发生变化会立刻反映到磁盘文件中这样会有效的保证共享内存的实现。
将共享内存和磁盘文件建立联系的是文件通道类FileChannel。
该类的加入是JDK为了统一对外部设备文件、网络接口等的访问方法并且加强了多线程对同一文件进行存取的安全性。
这里只是用它来建立共享内存用它建立了共享内存和磁盘文件之间的一个通道。
打开一个文件建立一个文件通道可以用RandomAccessFile类中的方法getChannel。
该方法将直接返回一个文件通道。
该文件通道由于对应的文件设为随机存取文件一方面可以进行读写两种操作另一方面使用它不会破坏映像文件的内容如果用FileOutputStream直接打开一个映像文件会将该文件的大小置为0当然数据会全部丢失。 为什么用 FileOutputStream和FileInputStream则不能理想的实现共享内存的要求呢 因为这两个类同时实现自由的读写操作要困难得多。 如何保障写入的互斥性
由于只有一个文件能拥有写的权限可以通过分布式锁的方式保障排他性。
如果在同一个机器上有一种简单的互斥方式
采用文件锁的方式。
共享内存在java中的应用的参考代码
package com.crazymakercircle.iodemo.sharemem;import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Properties;import com.crazymakercircle.NioDemoConfig;
import com.crazymakercircle.util.IOUtil;/*** 共享内存操作类*/
public class ShareMemory {String sourcePath NioDemoConfig.MEM_SHARE_RESOURCE_SRC_PATH;String decodePath IOUtil.getResourcePath(sourcePath);int fsize 1024; //文件的实际大小 MappedByteBuffer mapBuf null; //定义共享内存缓冲区FileChannel fc null; //定义相应的文件通道 FileLock fl null; //定义文件区域锁定的标记。 Properties p null;RandomAccessFile randomAccessFile null; //定义一个随机存取文件对象public ShareMemory() {try {// 获得一个只读的随机存取文件对象 rw 打开以便读取和写入。如果该文件尚不存在则尝试创建该文件。 randomAccessFile new RandomAccessFile(decodePath, rw);//获取相应的文件通道 fc randomAccessFile.getChannel();//将此通道的文件区域直接映射到内存中。mapBuf fc.map(FileChannel.MapMode.READ_WRITE, 0, fsize);} catch (IOException e) {e.printStackTrace();}}/*** param pos 锁定区域开始的位置必须为非负数* param len 锁定区域的大小必须为非负数* param buff 写入的数据* return*/public synchronized int write(int pos, int len, byte[] buff) {if (pos fsize || pos len fsize) {return 0;}//定义文件区域锁定的标记。 FileLock fl null;try {//获取此通道的文件给定区域上的锁定。 fl fc.lock(pos, len, false);if (fl ! null) {mapBuf.position(pos);ByteBuffer bf1 ByteBuffer.wrap(buff);mapBuf.put(bf1);//释放此锁定。 fl.release();return len;}} catch (Exception e) {if (fl ! null) {try {fl.release();} catch (IOException e1) {System.out.println(e1.toString());}}return 0;}return 0;}/*** param pos 锁定区域开始的位置必须为非负数* param len 锁定区域的大小必须为非负数* param buff 要取的数据* return*/public synchronized int read(int pos, int len, byte[] buff) {if (pos fsize) {return 0;}//定义文件区域锁定的标记。 FileLock fl null;try {fl fc.lock(pos, len, false);if (fl ! null) {//System.out.println( pospos ); mapBuf.position(pos);if (mapBuf.remaining() len) {len mapBuf.remaining();}if (len 0) {mapBuf.get(buff, 0, len);}fl.release();return len;}} catch (Exception e) {if (fl ! null) {try {fl.release();} catch (IOException e1) {System.out.println(e1.toString());}}return 0;}return 0;}/*** 完成关闭相关操作*/protected void finalize() throws Throwable {if (fc ! null) {try {fc.close();} catch (IOException e) {System.out.println(e.toString());}fc null;}if (randomAccessFile ! null) {try {randomAccessFile.close();} catch (IOException e) {System.out.println(e.toString());}randomAccessFile null;}mapBuf null;}/*** 关闭共享内存操作*/public synchronized void closeSMFile() {if (fc ! null) {try {fc.close();} catch (IOException e) {System.out.println(e.toString());}fc null;}if (randomAccessFile ! null) {try {randomAccessFile.close();} catch (IOException e) {System.out.println(e.toString());}randomAccessFile null;}mapBuf null;}
} map过程核心原理
接下去通过分析源码了解一下map过程的内部实现。
通过RandomAccessFile获取FileChannel。
public final FileChannel getChannel() {synchronized (this) {if (channel null) {channel FileChannelImpl.open(fd, path, true, rw, this);}return channel;}
}上述实现可以看出由于synchronized 只有一个线程能够初始化FileChannel。
通过FileChannel.map方法把文件映射到虚拟内存并返回逻辑地址address实现如下
/**只保留了核心代码**/
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {int pagePosition (int)(position % allocationGranularity);long mapPosition position - pagePosition;long mapSize size pagePosition;try {addr map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError x) {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, failthrow new IOException(Map failed, y);}}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);}
}上述代码可以看出最终map通过native函数map0完成文件的映射工作。
如果第一次文件映射导致OOM则手动触发垃圾回收休眠100ms后再次尝试映射如果失败则抛出异常。通过newMappedByteBuffer方法初始化MappedByteBuffer实例不过其最终返回的是DirectByteBuffer的实例实现如下
static MappedByteBuffer newMappedByteBuffer(int size, long addr, FileDescriptor fd, Runnable unmapper) {MappedByteBuffer dbb;if (directByteBufferConstructor null)initDBBConstructor();dbb (MappedByteBuffer)directByteBufferConstructor.newInstance(new Object[] { new Integer(size),new Long(addr),fd,unmapper }return dbb;
}
// 访问权限
private static void initDBBConstructor() {AccessController.doPrivileged(new PrivilegedActionVoid() {public Void run() {Class? cl Class.forName(java.nio.DirectByteBuffer);Constructor? ctor cl.getDeclaredConstructor(new Class?[] { int.class,long.class,FileDescriptor.class,Runnable.class });ctor.setAccessible(true);directByteBufferConstructor ctor;}});
}由于FileChannelImpl和DirectByteBuffer不在同一个包中所以有权限访问问题通过AccessController类获取DirectByteBuffer的构造器进行实例化。
DirectByteBuffer是MappedByteBuffer的一个子类其实现了对内存的直接操作。
get过程
MappedByteBuffer的get方法最终通过DirectByteBuffer.get方法实现的。
public byte get() {return ((unsafe.getByte(ix(nextGetIndex()))));
}
public byte get(int i) {return ((unsafe.getByte(ix(checkIndex(i)))));
}
private long ix(int i) {return address (i 0);
}map0()函数返回一个地址address这样就无需调用read或write方法对文件进行读写通过address就能够操作文件。底层采用unsafe.getByte方法通过address 偏移量获取指定内存的数据。
第一次访问address所指向的内存区域导致缺页中断中断响应函数会在交换区中查找相对应的页面如果找不到也就是该文件从来没有被读入内存的情况则从硬盘上将文件指定页读取到物理内存中非jvm堆内存。如果在拷贝数据时发现物理内存不够用则会通过虚拟内存机制swap将暂时不用的物理页面交换到硬盘的虚拟内存中。
性能分析
从代码层面上看从硬盘上将文件读入内存都要经过文件系统进行数据拷贝并且数据拷贝操作是由文件系统和硬件驱动实现的理论上来说拷贝数据的效率是一样的。 但是通过内存映射的方法访问硬盘上的文件效率要比read和write系统调用高这是为什么
read()是系统调用首先将文件从硬盘拷贝到内核空间的一个缓冲区再将这些数据拷贝到用户空间实际上进行了两次数据拷贝map()也是系统调用但没有进行数据拷贝当缺页中断发生时直接将文件从硬盘拷贝到用户空间只进行了一次数据拷贝。
所以采用内存映射的读写效率要比传统的read/write性能高。
总结
MappedByteBuffer使用虚拟内存因此分配(map)的内存大小不受JVM的-Xmx参数限制但是也是有大小限制的。如果当文件超出1.5G限制时可以通过position参数重新map文件后面的内容。MappedByteBuffer在处理大文件时的确性能很高但也存在一些问题如内存占用、文件关闭不确定被其打开的文件只有在垃圾回收的才会被关闭而且这个时间点是不确定的。 javadoc中也提到A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.
技术自由的实现路径
实现你的 架构自由
《吃透8图1模板人人可以做架构》
《10Wqps评论中台如何架构B站是这么做的》
《阿里二面千万级、亿级数据如何性能优化 教科书级 答案来了》
《峰值21WQps、亿级DAU小游戏《羊了个羊》是怎么架构的》
《100亿级订单怎么调度来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章正在添加中
实现你的 响应式 自由
《响应式圣经10W字实现Spring响应式编程自由》
这是老版本 《Flux、Mono、Reactor 实战史上最全》
实现你的 spring cloud 自由
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战史上最全》
《一文搞定SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系史上最全》
实现你的 linux 自由
《Linux命令大全2W多字一次实现Linux自由》
实现你的 网络 自由
《TCP协议详解 (史上最全)》
《网络三张表ARP表, MAC表, 路由表实现你的网络自由》
实现你的 分布式锁 自由
《Redis分布式锁图解 - 秒懂 - 史上最全》
《Zookeeper 分布式锁 - 图解 - 秒懂》
实现你的 王者组件 自由
《队列之王 Disruptor 原理、架构、源码 一文穿透》
《缓存之王Caffeine 源码、架构、原理史上最全10W字 超级长文》
《缓存之王Caffeine 的使用史上最全》
《Java Agent 探针、字节码增强 ByteBuddy史上最全》
实现你的 面试题 自由
4000页《尼恩Java面试宝典 》 40个专题
以上尼恩 架构笔记、面试题 的PDF文件更新请到《技术自由圈》公号获取↓↓↓