我是靠谱客的博主 雪白朋友,最近开发中收集的这篇文章主要介绍Java高速、多线程虚拟内存,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

本文作者Alex已经从事Java开发15年了,最近帮助开发了COBOL和Magik语言的JVM 。当前,他正致力于Micro Focus的Java性能测试工具。在本文中,他阐述了在标准硬件中实现高速、多线程虚拟内存的可能性及方案。原文内容如下。 

你想在标准硬件上运行TB级甚至PB级内存的JVM吗?你想与内存交互一样读写文件,且无需关心文件的打开、关闭、读、写吗? 

JVM的64位地址空间使这些成为可能。 

首先,不要在观念上将内存和磁盘进行区分,而是统一处理为内存映射文件。在32位地址空间时,内存映射文件只是为了高速访问磁盘;因为受限于虚拟机的有限地址空间,并不支持大规模的虚拟内存或大文件。如今JVM已经发展为64位,而且可以在64位操作系统上运行。在一个进程的地址空间中,内存映射文件大小就可以达到TB甚至PB。 

进程无需关心内存是在RAM或是磁盘上。操作系统会负责处理,而且处理得非常高效。 

我们可以使用Java的MappedByteBuffer类访问内存映射文件。该类的实例对象与普通的ByteBuffer一样,但包含的内存是虚拟的——可能是在磁盘上,也可能是在RAM中。但无论哪种方式,都是由操作系统负责处理。因为的ByteBuffer的大小上限是Intger.MAX_VALUE,我们需要若干个ByteBuffer来映射大量内存。在这个示例中,我映射了40GB。 

这是因为我的Mac只有16GB内存,下面代码证明了这一点! 

Java代码 
  1. public MapperCore(String prefix, long size) throws IOException {  
  2.     coreFile = new File(prefix + getUniqueId() + ".mem");  
  3.     // This is a for testing - to avoid the disk filling up  
  4.     coreFile.deleteOnExit();  
  5.     // Now create the actual file  
  6.     coreFileAccessor = new RandomAccessFile(coreFile, "rw");  
  7.     FileChannel channelMapper = coreFileAccessor.getChannel();  
  8.     long nChunks = size / TWOGIG;  
  9.     if (nChunks > Integer.MAX_VALUE)  
  10.         throw new ArithmeticException("Requested File Size Too Large");  
  11.     length = size;  
  12.     long countDown = size;  
  13.     long from = 0;  
  14.     while (countDown > 0) {  
  15.         long len = Math.min(TWOGIG, countDown);  
  16.         ByteBuffer chunk = channelMapper.map(MapMode.READ_WRITE, from, len);  
  17.         chunks.add(chunk);  
  18.         from += len;  
  19.         countDown -= len;  
  20.     }  
  21. }  


上面的代码在虚拟内存创建了40GB MappedByteBuffer对象列表。读取和写入时只需要注意处理两个内存模块的跨越访问。完整代码的可以在 这里找到。 

线程 

一个极其强大且简单易用的方法就是线程。但是普通的Java IO简直就是线程的噩梦。两个线程无法在不引起冲突的情况下同时访问相同的数据流或RandomAccessFile 。虽然可以使用非阻塞IO,但是这样做会增加代码的复杂性并对原有的代码造成侵入。 

与其他的内存线程一样,内存映射文件也是由操作系统来处理。可以根据读写需要,在同一时刻尽可能多的使用线程。我的测试代码有128个线程,而且工作得很好(虽然机器发热比较大)。唯一重要的技巧是复用MappedByteBuffer对象,避免自身位置状态引发问题。 

现在可以执行下面的测试: 

Java代码 
  1. @Test  
  2. public void readWriteCycleThreaded() throws IOException {  
  3. final MapperCore mapper = new MapperCore("/tmp/MemoryMap", BIG_SIZE);  
  4. final AtomicInteger fails = new AtomicInteger();  
  5. final AtomicInteger done = new AtomicInteger();  
  6. Runnable r = new Runnable() {  
  7.     public void run() {  
  8.         try {  
  9.             // Set to 0 for sequential test  
  10.             long off = (long) ((BIG_SIZE - 1024) * Math.random());  
  11.             System.out.println("Running new thread");  
  12.             byte[] bOut = new byte[1024];  
  13.             double counts = 10000000;  
  14.             for (long i = 0; i < counts; ++i) {  
  15.                 ByteBuffer buf = ByteBuffer.wrap(bOut);  
  16.                 long pos = (long) (((BIG_SIZE - 1024) * (i / counts)) + off)  
  17.                         % (BIG_SIZE - 1024);  
  18.                 // Align with 8 byte boundary  
  19.                 pos = pos / 8;  
  20.                 pos = pos * 8;  
  21.                 for (int j = 0; j < 128; ++j) {  
  22.                     buf.putLong(pos + j * 8);  
  23.                 }  
  24.                 mapper.put(pos, bOut);  
  25.                 byte[] bIn = mapper.get(pos, 1024);  
  26.                 buf = ByteBuffer.wrap(bIn);  
  27.                 for (int j = 0; j < 128; ++j) {  
  28.                     long val = buf.getLong();  
  29.                     if (val != pos + j * 8) {  
  30.                         throw new RuntimeException("Error at " + (pos + j * 8) + " was " + val);  
  31.                     }  
  32.                 }  
  33.             }  
  34.             System.out.println("Thread Complete");  
  35.         } catch (Throwable e) {  
  36.             e.printStackTrace();  
  37.             fails.incrementAndGet();  
  38.         } finally {  
  39.             done.incrementAndGet();  
  40.         }  
  41.     }  
  42. };  
  43. int nThreads = 128;  
  44. for (int i = 0; i < nThreads; ++i) {  
  45.     new Thread(r).start();  
  46.     }  
  47. while (done.intValue() != nThreads) {  
  48.     try {  
  49.         Thread.sleep(1000);  
  50.     } catch (InterruptedException e) {  
  51.         // ignore  
  52.     }  
  53.     }  
  54. if (fails.intValue() != 0) {  
  55.     throw new RuntimeException("It failed " + fails.intValue());  
  56.     }  
  57. }  


我曾尝试进行其他形式的IO,但是只要像上面那样运行128个线程,性能都不如上面的方法。我在四核、超线程I7 Retina MacBook Pro上尝试过。代码运行时会启动128个线程,超出CPU的最大负载(800%),直到操作系统检测到该进程的内存不足。在这个时候,系统开始对内存映射文件的读写进行分页。为实现这一目标,内核会占用一定的CPU,Java进程的性能会下降到650~750%。Java无需关心读取、写入、同步或类似的东西——操作系统会负责处理。 

结果会有所不同 

如果读取和写入点不是连续而是随机的,性能下降有所区别(带有交换时会达到750%,否则会达到250%)。我相信这种方式可能更适合处理少量的大数据对象,而不适用于大量的小数据对象。对于后者,可能的处理办法是预先将大量小数据对象加载到缓存中,再将其映射到虚拟内存。 

应用程序 

到目前为止,我使用的技术都是虚拟内存系统。在示例中,一旦与虚拟内存交互完成,就会删除底层文件。但是,这种方法可以很容易地进行数据持久化。 

例如,视频编辑是一个非常具有挑战性的工程问题。一般来说,有两个有效的方法:无损耗存储整个视频,并编辑存储的信息;或根据需要重新生成视频。因为RAM的制约,后一种方法越来越普遍。然而,视频是线性的——这是一种理想的数据类型,可用来存储非常大的映射虚拟内存。由于在视频算法上取得的进步,可以将它作为原始字节数组访问。操作系统会根据需要将磁盘到虚拟内存的缓冲区进行分页处理。 

另一个同样有效的应用场景是替代文档服务中过度设计的RAM缓存解决方案。想想看,我们有一个几TB的中等规模的文档库。它可能包含图片、短片和PDF文件。有一种常见的快速访问磁盘的方法,使用文件的RAM缓存弱引用或软引用。但是,这会对JVM垃圾收集器产生重大影响,并且增加操作难度。如果将整个文档映射到虚拟内存,可以更加简单地完成同样的工作。操作系统会根据需要将数据读入内存。更重要的是,操作系统将尽量保持RAM中最近被访问的内存页。这意味着内存映射文件就像RAM缓存一样,不会对Java或JVM垃圾收集器产生任何影响。 

最后,内存映射文件在科学计算和建模等应用中非常有效。在用来处理代表真实世界系统的计算模型时,经常需要大量的数据才能正常工作。在我的音频处理系统 Sonic Field中,通过混合和处理单一声波,可以模拟真实世界中的音频效果。例如,创建原始音频副本是为模拟从硬表面反射的声波,并将反射回来的声波与原声波混合。这种方法需要大量的存储空间,这时就可以把音频信号放在虚拟内存中(也是这项工作的最初动机)。 

原文链接:  jaxenter 翻译:  ImportNew.com -  MarkGZ 
译文链接:  http://www.importnew.com/9270.html

最后

以上就是雪白朋友为你收集整理的Java高速、多线程虚拟内存的全部内容,希望文章能够帮你解决Java高速、多线程虚拟内存所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(35)

评论列表共有 0 条评论

立即
投稿
返回
顶部