上篇:Java NIO核心三大组件Channel、Buffer和Selector(一)
一、直接缓冲区map创建 内存管理在深入MappedByteBuffer之前,先看看计算机内存管理的几个术语:
- MMC:CPU的内存管理单元。
- 物理内存:即内存条的内存空间。
- 虚拟内存:计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
- 页面文件:操作系统反映构建并使用虚拟内存的硬盘空间大小而创建的文件,在windows下,即pagefile.sys文件,其存在意味着物理内存被占满后,将暂时不用的数据移动到硬盘上。
- 缺页中断:当程序试图访问已映射在虚拟地址空间中但未被加载至物理内存的一个分页时,由MMC发出的中断。如果操作系统判断此次访问是有效的,则尝试将相关的页从虚拟内存文件中载入物理内存。
1、为什么会有虚拟内存和物理内存的区别?
如果正在运行的一个进程,它所需的内存是有可能大于内存条容量之和的,如内存条是256M,程序却要创建一个2G的数据区,那么所有数据不可能都加载到内存(物理内存),必然有数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,再调度进入物理内存。虚拟内存技术就派上用场了。
2、什么是虚拟内存地址和物理内存地址?
虚拟内存地址:虚拟内存地址范围的大小由CPU的位数决定,假设你的计算机是32位,那么它的地址总线是32位的,它的地址范围是0~0xFFFFFFFF (4G),这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。
物理内存地址:与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集。举个例子,对于一台内存(物理内存)为256M的32bit x86主机来说,它的虚拟地址空间范围是0~0xFFFFFFFF(4G),而物理地址空间范围是0x00000000 ~ 0x0FFFFFFF(256M)。
3、内存分页机制
大多数使用虚拟内存的系统都使用一种称为分页(paging)机制。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页帧(frame)。页和页帧的大小必须相同。
举个例子,我们有一台可以生成32位地址的机器,它的虚拟地址范围从0~0xFFFFFFFF(4G),而这台机器只有256M的物理地址,因此他可以运行4G的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放4G程序的外部存储器(例如磁盘),以保证程序片段在需要时可以被调用。在这个例子中,页的大小为4K,页帧大小与页相同——这点是必须保证的,因为内存和外围存储器之间的传输总是以页为单位的。对应4G的虚拟地址和256M的物理存储器,他们分别包含了1M个页和64K个页帧。
4、虚拟内存与物理内存之间的联系是通过分页表来建立
页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB(进程管理块)表中有指针指向页表。
虚拟内存页的个数 > 物理内存页帧的个数,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?不是的,操作系统是这样处理的。操作系统有个页面失效(page fault)功能。操作系统找到一个使用的最少的页帧,使之失效,并把它写入磁盘,随后把需要访问的页放到这个页帧中来,并修改页表中的映射,保证了所有的页都会有对应的物流内存地址空间。
虚拟内存地址组成:由页号(与页表中的页号关联)和偏移量(页的小大,即这个页能存了多少数据)组成。
举个例子,有一个虚拟地址它的页号是4,偏移量是20,那么他的寻址过程是这样的:首先到页表中找到页号4对应的页帧号(比如为8),如果这个页号没有与之对应的页帧号,则用失效机制调入页,接着把页帧号和偏移量传给MMU组成一个物理上真正存在的地址,最后就是访问物理内存的数据了。
直接缓冲区map创建
虚拟内存技术可以让硬盘上文件的位置(物理地址)与进程逻辑地址空间(虚拟内存地址空间)中一块大小相同的区域之间的一一对应,这种对应关系纯属是逻辑上的概念,物理上是不存在的,原因是进程的逻辑地址空间本身就是不存在的。
在内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存。
在JVM内存外开辟内存,在每次调用操作系统的一个本机I/O之前或者之后,数据传递会少一次复制过程。如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。虽然直接缓冲区使JVM可以进行高效的I/O操作,但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销。所以,针对大数据处理时,直接缓冲区可以提升效率。
1、直接缓冲区的创建
1)ByteBuffer.allocateDirect()方法直接内存DirectMemory的大小,默认为 -Xmx 的JVM堆的最大值,但是并不受其限制,而是由JVM参数 MaxDirectMemorySize单独控制。
2)通道的map方法创建MappedByteBuffer,MappedByteBuffer处理大文件,一次最多只能读2G内容,推荐使用
abstract MappedByteBuffer
map(FileChannel.MapMode mode, long position, long size)
将此通道的文件区域直接映射到内存中。
FileChannel中的几个变量:
- MapMode mode:内存映像文件访问的方式,共三种:
- MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出异常。
- MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的。
- MapMode.PRIVATE:私用,可读可写,但是修改的内容不会写入文件,只是buffer自身的改变,这种能力称之为”copy on write”。
- position:文件映射时的起始位置。映射区域从此位置开始;必须为非负数。
- size:要映射的区域大小;必须为非负数且不大于Integer.MAX_VALUE(2G)
2、MappedByteBuffer的使用:它不需要翻转
简单的文件复制,注意:这里没有正确的释放MappedByteBuffer,项目中记得正确释放。
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
// 1.创建FileChannel
FileChannel readChannel = new RandomAccessFile("D:/E/NIO/text.txt", "r").getChannel();
FileChannel writeChannel = new RandomAccessFile("D:/E/NIO/text_copy.txt", "rw").getChannel();
// FileChannel readChannel = new RandomAccessFile("D:/E/NIO/textBig.txt", "r").getChannel();
// FileChannel writeChannel = new RandomAccessFile("D:/E/NIO/textBig_copy.txt", "rw").getChannel();
// 2.缓冲区
MappedByteBuffer mappedByteBuffer = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
while (mappedByteBuffer.remaining() > 0) {
writeChannel.write(mappedByteBuffer);
}
readChannel.close();
writeChannel.close();
long end = System.currentTimeMillis();
System.out.println("所需时间为:" + (end - start));
}
直接缓冲区操作时间:35M--834ms, 1.56G--11983ms
3、MappedByteBuffer释放
1)文件小于2G时
MappedByteBuffer没有unmap方法,使得这个文件会一直被程序的资源句柄占用着无法释放。测试如下
public static void main(String[] args) throws Exception {
File srcfile = new File("D:/E/NIO/text.txt");
FileChannel readChannel = new RandomAccessFile(srcfile, "r").getChannel();
FileChannel writeChannel = new RandomAccessFile("D:/E/NIO/text_copy.txt", "rw").getChannel();
MappedByteBuffer mappedByteBuffer = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
while (mappedByteBuffer.remaining() > 0) {
writeChannel.write(mappedByteBuffer);
}
readChannel.close();
writeChannel.close();
// 文件的资源句柄有没有被释放,可以通过修改文件名来测试
// 名字被改了,证明资源句柄释放了,没改表示没有被释放
srcfile.renameTo(new File("D:/E/NIO/text_2.txt"));
}
// 程序正常结束,但是文件没有改名
解决方案:使用 Cleaner类
// 断开连接
Cleaner cleaner = ((DirectBuffer)mappedByteBuffer).cleaner();
if (cleaner != null) {
cleaner.clean();
}
// 文件的资源句柄被释放,修改成功
srcfile.renameTo(new File("D:/E/NIO/text_2.txt"));
2)文件大于2G时:分块循环读取
MappedByteBuffer处理大文件,一次最多只能读2G内容到内存中,为了读取大文件,需要分块循环读取处理:
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
File srcfile = new File("D:/E/NIO/textBig2.txt");
// 1.创建FileChannel
FileChannel readChannel = new RandomAccessFile(srcfile, "r").getChannel();
FileChannel writeChannel = new RandomAccessFile("D:/E/NIO/textBig2_copy.txt", "rw").getChannel();
long fileLength = readChannel.size();
long current = 0;
long mapSize = 1L 0){
sinkChannel.write(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// B线程取数据
class ThreadB implements Runnable{
private Pipe pipe;
public ThreadB(Pipe pipe) {
this.pipe = pipe;
}
@Override
public void run() {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Pipe.SourceChannel sourceChannel = pipe.source();
sourceChannel.read(buffer);
buffer.flip();
System.out.println("ThreadB received:" + new String(buffer.array()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
参考文章:
Java NIO之Selector(选择器)
操作系统-内存管理
深入浅出MappedByteBuffer
—— Stay Hungry. Stay Foolish. 求知若饥,虚心若愚。