【IT168 技术文章】
概念--什么是MMF?
从现在开始,MMF一词将在本文中大量出现。所以,我在此先对MMF做一个简单的描述。MMF,全称Memory Mapped Files,从宏观上看,它是一种数据内存映射的技术或者说管理动态内存的一种方法,Randy Kath这样定义到MMF:Memory-mapped files(MMFs) offer a unique memory management feature that allows applications to access files on disk in the same way they access dynamic memory-through pointers。从微观的角度,它主要具有以下几个特性:
概念:MMF是一个Windows对象,你可以通过Windows API创建和访问它。
本质:你可以把MMF当成一个普通的文件,只不过它贮存在系统内存中。
图一:MMF在各个进程间实现共享(来自MSDN Online)

特性:MMF可以被任何进程、线程所访问,这说明MMF具有可在进程间共享的特性,这也正是它的最大"魅力"所在。当然,因为所有的存取操作都在内存中进行,它也同时具备快速的特点。
实现原理:MMF是基于现代操作系统都普遍采用的虚拟内存(virtual memory)技术,而虚拟内存是基于一种被称作Paging的机制之上的(2)。所以可以这样认为,只要某个操作系统采用了基于Page的虚拟内存管理系统,它就可以实现MMF这种功能特性。
生存周期:MMF一直存在直到对它的最后一个引用被断开。
MMF其实是Windows平台下的一个基本特性,所有关于它的操作都可以通过Windows API获得,它使得DNA架构下COM跨进程访问数据成为可能。利用它,可以将数据库端的业务数据缓存到应用服务器端或者客户端的MMFs中,省去频繁访问数据库的开销,极大地提高系统访问性能。对于Java,我们也在Jdk1.4的NIO规范下找到了利用MMF的类集合,虽然在Jdk1.4的API文档中并没有明确地提出这样一个概念,但是我们在FileChannel和ByteBuffer类的文档中了解到FileChannel对象具有映射文件至内存的功能,从上面的介绍中我们可以看出这实际上就是创建了MMF。
背景--我们遇到什么困难?
我们希望对某个购买系统进行升级开发,自然就会涉及到平台的选型。原来的系统是基于微软的DNA架构,我们现在倾向于将之移植到J2EE平台。在此之前自然要进行必要的可行性分析,除去其他方面的考虑之外,我们最关心的自然落到关键技术的可行性上面,因为我们希望最大限度地利用原有系统的架构设计。
由于该系统基于微软的DNA架构,采用DCOM远程访问组件的方式,系统性能自然成为一个非常重要的考虑。所以,在原有系统中最大的亮丽之处在于花费大量工作来提高整个系统的性能指标,使得整个系统无论在系统响应速度,还是大数据量并发操作方面都有很杰出的表现。在这其中尤以数据缓存技术MMF的应用最为关键,通过服务器端和客户端的数据缓存,有效地提升了整个系统的性能。
图二:应用MMF后的系统图

图示说明:
图中的"Server Cache(Business Rules)"部分即为利用MMF进行的数据缓存;
另外,在客户端也大量利用到MMF,在图中并未标出。
整个系统沿着这样一个思路来利用MMF:每次系统启动的时候,程序访问数据库,获取表中数据,通过一系列步骤将之缓存至应用服务器端MMFs,见下图中黑线所示。以后客户端每次请求数据,将直接访问应用服务器端MMFs,见图中红线所示,并且同时将数据缓存到客户端。此后,如果有任何配置数据的改变,可以重新装载数据到MMFs。当然,与之配套的还有一套比较合理的定时数据比较机制。
图三、系统与MMF的交互图

以上这些就是我们所要实现的MMF缓存机制,简单地说,我们就是要在Java中找出与之对应的缓存机制解决方案。
解决方案--我们想到了什么?
明确了目标之后,我们就开始了在Java中寻找的征程。可以说几乎涵盖了现有的所有可行的方案,下面就是我们探索和思考的点点滴滴。
一、 利用JNI
我们首先想到的就是JNI(Java Native Interface,Java本地接口),毕竟这是最直观和最省事的解决方案。在Java中利用JNI直接调用已有的VC或者VB代码,不需要重新编写这些代码,节省了时间,而且程序执行效率也相当不错。但是,利用JNI也存在着诸多的问题:不同程序代码之间的兼容性和可协调性,不易维护性。总之,对于这种夹生饭可以作为一时的权宜之计,在项目时间紧迫的情况下可以考虑使用,但是从长远考虑还是不宜采用。(3)
二、 利用XML
这其中我们也想到了利用XML,作为时下非常流行和实用的一门技术,Jdk1.4中提供了一整套比较完整的XML API,使得产生以及解析XML文件变得非常的容易。但是,个人觉得XML最大的优势在于为不同系统间的数据交换提供一种通用的格式,在于数据存储、解析和转换方面,作为数据缓存的候选虽然也未尝不可,但是从最优系统性能和充分继承原有系统架构考虑,还不是非常受欢迎的解决方案。
三、 利用MMF
因为原有系统是使用的MMF,所以我们也自然而然想到了JAVA中是否也存在MMF。经过对Jdk1.4的仔细研究,我们也如愿找到了我们希望的功能。经过各方面的讨论,我们决定在新系统中采用该技术。
解决方案--我们做了些什么?
在做出决定之后,我们就需要对Java中的MMF做一个详细的研究。在Jdk1.4中,关于MMF的API主要位于java.nio和java.nio.channels包下。在新的JAVA NIO中着重提到两个概念Buffer和Channel,MMF其实是作为它们的一个附属品被提出来的。其中的FileChannel类的map方法能够完成这样一种功能"Maps a region of this channel's file directly into memory",返回一个MappedByteBuffer对象。由此我们可见在Jdk1.4中,MMF的表现形式为MappedByteBuffer类及其父类ByteBuffer,你可以通过这些类提供的一些方法来操纵MMF对象,而创建MMF的功能主要由FileChannel类来完成。(4)
在使用类MappedByteBuffer之前,你必须弄清楚这样几个概念:capacity, limit, position,这在所有Buffer类中都是非常关键的。这里我直接引用Jdk1.4文档中的解释:
A buffer's capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.
A buffer's limit is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.
A buffer's position is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.
也许这样一个数学公式更加直观:0 <= position <= limit <= capacity。
在进行大规模的系统应用之前,我们建立个简单的应用模型。今天,我们介绍一下这其中关于MMF最简单的一些操作。
1、 创建MMF
上面我们已经提到,调用FileChannel类的map()方法可以创建MMF,详细的方法说明如下:
2
通过设置不同的MapMode类型,可以分别得到只读的、可读写的和私有的MMF,因此可以视情况而定创建不同的MMF。同时通过设置参数position和size可以指定文件的某一部分映射至内存,该特点对于大文件是非常有用的。
2 try
3 {
4 File file = new File("filename");
5
6 // 创建一个只读的memory-mapped file
7 FileChannel roChannel = new RandomAccessFile(file, "r").getChannel();
8 ByteBuffer roBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int)roChannel.size());
9
10 // 创建一个可读写的 memory-mapped file
11 FileChannel rwChannel = new RandomAccessFile(file, "rw").getChannel();
12 ByteBuffer wrBuf = rwChannel.map(FileChannel.MapMode.READ_WRITE, 0, (int)rwChannel.size());
13
14 // 创建一个私有的 (copy-on-write) memory-mapped file.
15 // Any write to this channel results in a private copy of the data.
16 FileChannel pvChannel = new RandomAccessFile(file, "rw").getChannel();
17 ByteBuffer pvBuf = roChannel.map(FileChannel.MapMode.PRIVATE, 0, (int)rwChannel.size());
18 }
19 catch (IOException e)
20 {}
21
2、 向MMF中插入数据
你可以利用类MappedByteBuffer的capacity来得到它里面包含的字节数,这是个常量。你可以利用方法put()来向MMF中插入数据,它有两种不同的版本:绝对位置插入put(int index, byte b),为此你必须指定index(0<=index<=capacity-1);相对位置插入put(byte b),它是利用了position和limit属性。利用相对位置插入数值后,position也相应地加1,直至达到limit的限制。而且,针对不同的数据类型,有各自相对应的put方法,比如putChar, putDouble之类。
2 ByteBuffer bbuf = MappedByteBuffer.allocate(10);
3
4 // Get the buffer's capacity
5 int capacity = bbuf.capacity(); // 10
6
7 // Use the absolute put().
8 // This method does not affect the position.
9 bbuf.put(1,(byte)0xFF); // position=0
10
11 // Set the position
12 bbuf.position(5);
13
14 // Use the relative put()
15 bbuf.put((byte)0xFF);
16
17 // Get the new position
18 int pos = bbuf.position(); // 6
19
20 // Get remaining byte count
21 int rem = bbuf.remaining(); // 4
22
23 // Set the limit
24 bbuf.limit(7); // remaining=1
25
26 // This convenience method sets the position to 0
27 bbuf.rewind(); // remaining=7
28
3、 从MMF中获得数据
与上述的过程相反,你可以通过不同的get方法来从MMF中获得数据。
2 ByteBuffer bbuf = MappedByteBuffer.allocate(10);
3
4 // Get the MappedByteBuffer's capacity
5 int capacity = bbuf.capacity(); // 10
6
7 // Use the absolute get().
8 // This method does not affect the position.
9 byte b = bbuf.get(5); // position=0
10
11 // Set the position
12 bbuf.position(5);
13
14 // Use the relative get()
15 b = bbuf.get();
16
17 // Get the new position
18 int pos = bbuf.position(); // 6
19
20 // Get remaining byte count
21 int rem = bbuf.remaining(); // 4
22
23 // Set the limit
24 bbuf.limit(7); // remaining=1
25
26 // This convenience method sets the position to 0
27 bbuf.rewind(); // remaining=7
28
解决方案--需要注意的地方
上面我们给出的只是一个非常简单的读写MMF的例子,在实际的使用过程中会复杂得多,下面几个因素可能是你要好好考虑的:
1、 数据与MMF的对应关系
既然是要将数据缓存到MMF中,那我们就必须确立数据库表与MMF的对应关系。我们推荐使用的方式是每一张表对应一个MMF文件。
2、 MMF文件长度的设计
确立了对应关系之后,我们需要分析一下如何设定MMF文件的初始长度。文件长度不能太小,否则就不能容纳所有的数据,同时文件也不能太长,那样一来浪费系统内存,二来也会使创建MMF的开销急剧增大。那刚好能容纳所有的记录呢?听起来是个不错的主意,但是如果这个时候需要添加一条记录呢?麻烦就来啦,由于原有长度不够。系统需要重新re-map MMF文件,造成系统内频繁地创建MMF,反而使性能下降。经过我们研究后得出,这个比例在1.1-1.3之间比较合适,也就是MMF文件略大于表中现有记录的总和。
3、 针对不同性质的数据进行不同的处理
明确以上两点,我们还需要对数据本身做一番研究。有些数据趋于固化,一般不会有什么改变,比如国家、省份等,而有些数据则会经常变化,比如产品等,对于这两种不同类型的数据,你可以采取不同的处理方式,以达到最优的系统性能。
可能存在的问题--我们需要预防些什么?
1、 MMF不是功能较多灵药
千万不要以为有了MMF,你就可以高枕无忧,可以轻轻松松搞定系统的缓存机制。事实远非如此,MMF只不过是一把利刃,更重要的是你自己要仔细认真地设计好系统的缓存机制。要知道,解决交通堵塞问题的关键不是把路修得多么宽,而是要合理地规划整个交通路线。要知道在某些操作系统中,使用MMF的代价是非常昂贵的,失去好的规划,你可能会适得其反,系统反而会更加的拥挤不堪。况且,使用MMF还会带来很多的副作用。
2、 性能与数据差错容忍度之间的平衡
我们知道,随着数据缓存的大量使用,不可避免地会产生某种程度上的数据不一致,也就可能会产生某些数据差错。所以说,数据缓存使用的力度决定于系统客户对这些错误的容忍程度有多大。在某些非常关键的业务数据应用数据缓存技术时,必须格外地小心。
3、 需要额外的MMF支持代码
如上所述,为了最大限度地减少数据的不统一,我们必须提供一套非常合理和有效的数据同步机制,某种程度上甚至可以认为数据同步机制的好坏决定了数据缓存技术的成败。而这些是我们在使用MMF的过程中需要额外提供的代码。
4、 MMF与平台的相关性
现在大部分编程语言中使用MMF的方法都是,提供相应的接口创建和操作MMF或者系统API,而底层的具体MMF细节则由相应的操作系统去决定。这样每种操作系统中MMF不同的实现细节也在某种程度上影响着我们对MMF的使用。
5、 使用MMF必须十分的小心
既然MMF是贮存在系统内存中,所以对于某些错误必须时刻警惕,比如"Array Out of Bound"等。要是您的系统没有很好地捕获这些错误,您的系统可能会彻底崩溃。每当你编写这些MMF代码的时候,你必须时刻牢记在心:我是在与系统内存打交道,这家伙可是娇贵的很。
6、 由于Jdkl1.4的推出时间不长,基于MMF的现有应用几乎没有,所以没有真正能够在现实环境中检验MMF的使用情况,可能会存在一些不可预知的风险。
总结
通过以上的介绍,相信大家对MMF在Java中的应用都有了一个初步的印象。实际上,提高应用系统的性能一直是所有应用系统开发人员追求的目标。除去本文谈到的缓存技术之外,在J2EE中,你还可以通过各种池技术的应用,EJB组件的优化来提高系统性能