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内存,下面代码证明了这一点!


public MapperCore(String prefix, long size) throws IOException {

coreFile = new File(prefix + getUniqueId() + ".mem");

// This is a for testing - to avoid the disk filling up

coreFile.deleteOnExit();

// Now create the actual file

coreFileAccessor = new RandomAccessFile(coreFile, "rw");

FileChannel channelMapper = coreFileAccessor.getChannel();

long nChunks = size / TWOGIG;

if (nChunks > Integer.MAX_VALUE)

throw new ArithmeticException("Requested File Size Too Large");

length = size;

long countDown = size;

long from = 0;

while (countDown > 0) {

long len = Math.min(TWOGIG, countDown);

ByteBuffer chunk = channelMapper.map(MapMode.READ_WRITE, from, len);

chunks.add(chunk);

from += len;

countDown -= len;

}

}

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

线程

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

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

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


@Test

public void readWriteCycleThreaded() throws IOException {

final MapperCore mapper = new MapperCore("/tmp/MemoryMap", BIG_SIZE);

final AtomicInteger fails = new AtomicInteger();

final AtomicInteger done = new AtomicInteger();

Runnable r = new Runnable() {

public void run() {

try {

// Set to 0 for sequential test

long off = (long) ((BIG_SIZE - 1024) * Math.random());

System.out.println("Running new thread");

byte[] bOut = new byte[1024];

double counts = 10000000;

for (long i = 0; i < counts; ++i) {

ByteBuffer buf = ByteBuffer.wrap(bOut);

long pos = (long) (((BIG_SIZE - 1024) * (i / counts)) + off)

% (BIG_SIZE - 1024);

// Align with 8 byte boundary

pos = pos / 8;

pos = pos * 8;

for (int j = 0; j < 128; ++j) {

buf.putLong(pos + j * 8);

}

mapper.put(pos, bOut);

byte[] bIn = mapper.get(pos, 1024);

buf = ByteBuffer.wrap(bIn);

for (int j = 0; j < 128; ++j) {

long val = buf.getLong();

if (val != pos + j * 8) {

throw new RuntimeException("Error at " + (pos + j * 8) + " was " + val);

}

}

}

System.out.println("Thread Complete");

} catch (Throwable e) {

e.printStackTrace();

fails.incrementAndGet();

} finally {

done.incrementAndGet();

}

}

};

int nThreads = 128;

for (int i = 0; i < nThreads; ++i) {

new Thread(r).start();

}

while (done.intValue() != nThreads) {

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// ignore

}

}

if (fails.intValue() != 0) {

throw new RuntimeException("It failed " + fails.intValue());

}

}

  我曾尝试进行其他形式的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中,通过混合和处理单一声波,可以模拟真实世界中的音频效果。例如,创建原始音频副本是为模拟从硬表面反射的声波,并将反射回来的声波与原声波混合。这种方法需要大量的存储空间,这时就可以把音频信号放在虚拟内存中(也是这项工作的最初动机)。

最新内容请见作者的GitHub页:http://qaseven.github.io/

时间: 2024-09-20 03:17:51

Java高速、多线程虚拟内存的实现的相关文章

JAVA的多线程浅析

一 JAVA 语言的来源.及特点 在这个高速信息的时代,商家们纷纷把信息.产品做到Internet国际互连网页上.再这些不寻常网页的背后,要属功能齐全.安全可靠的编程语言,Java是当之无愧的.Java是由Sun Microsystem开发的一种功能强大的新型程序设计语言.是与平台无关的编程语言.它是一种简单的.面象对象的.分布式的.解释的.键壮的.安全的.结构的中立的.可移植的.性能很优异的.多线程的.动态的.语言. Java自问世以后,以其编程简单.代码高效.可移植性强,很快受到了广大计算机

Java的多线程

在计算机编程中,一个基本的概念就是同时对多个任务加以控制.许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程.可以通过多种途径达到这个目的.最开始的时候,那些拥有机器低级知识的程序员编写一些"中断服务例程",主进程的暂停是通过硬件级的中断实现的.尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题. 有些时候,中断对那些实时性很强的任务来说是很有必要的.但还存在其他许多问题,它们只要求将问题划分进入独立运行的程序片断中,使整个程序能

用Java实现多线程服务器程序

---- 摘要:在Java出现之前,编写多线程程序是一件烦琐且伴随许多不安全因素的事情.利用Java,编写安全高效的多线程程序变得简单,而且利用多线程和Java的网络包我们可以方便的实现多线程服务器程序. ---- Java是伴随Internet的大潮产生的,对网络及多线程具有内在的支持,具有网络时代编程语言的一切特点.从Java的当前应用看,Java主要用于在Internet或局域网上的网络编程,而且将Java作为主流的网络编程语言的趋势愈来愈明显.实际工作中,我们除了使用商品化的服务器软件外

Java开发多线程同步技巧

在编写一个类时,如果该类中的代码可能运行于多线程环境下,那么就要考虑同步的问题.在Java中内置了语言级的同步原语--synchronized,这也大大简化了Java中多线程同步的使用. 我们首先编写一个非常简单的多线程的程序,是模拟银行中的多个线程同时对同一个储蓄账户进行存款.取款操作的. 在程序中我们使用了一个简化版本的Account类,代表了一个银行账户的信息.在主程序中我们首先生成了1000个线程,然后启动它们,每一个线程都对John的账户进行存100元,然后马上又取出100元.这样,对

java用多线程数组求和

问题描述 java用多线程数组求和 有三个数组 int[] arr1={1,1,1,1,1}; int[] arr2={2,2,2,2,2}; int[] arr3={3,3,3,3,3}; 怎么用三个线程求他们的和?即得到的答案应该是30. 解决方案 你是说用 CountDownLatch 吗?

java socket多线程的时间问题

问题描述 java socket多线程的时间问题 各位大神你们好, 我正在用java写一个接收服务器数据并进行统计的程序, 我用的是多线程socket. 该程序要求能够每秒钟接收几千条数据, 数据中包含value和timestamp以及其他字段. 每个数据通过单独的连接发送, 接收之后统计每秒的钟数据的最大值与平均值(指的是数据中所含有的"value"字段的值), 并将其存储到本地. 但是使用多线程时我有个疑问, 就是怎么判断是否到了1秒. 如果使用本地时钟计算肯定会与服务器有出入,

java se 多线程资源共享问题

问题描述 java se 多线程资源共享问题 class bread{ int num = 0; public synchronized void makeBreand(){ num++; this.notify(); } public synchronized void sale(){ while(num==0){ try{ this.wait(); System.out.println("暂时无面包,等待"); }catch(InterruptedException e){e.pr

Java 并发/多线程教程(五)-相同线程

       本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!        相同线程是一并发框架模型,是一个单线程系统向外扩展成多个单线程的系统.这样的结果就是多个单线程并行运行. 为什么是单线程系统?         你也许会感到好奇,为什么当今还有人设计单线程系统.单线程系统之所以这么普及,是因为单线程系统相对于多线程并发系统更为简单.单线程系统不需要与其他线程共享任何数据.这就使得单线程系统可以使用非并发的数据结构,可以更

锁-JAVA中多线程读取成员变量的重复问题

问题描述 JAVA中多线程读取成员变量的重复问题 这是个模拟卖票的问题,使用一个对象实现Runnable接口建立四个线程,这个对象有100张票,四个进程同时卖,因为没使用锁,所以会出现负数票,但是为什么会出现相同的票呢?8号票卖了四次,是因为成员变量在if之后进栈保存了值?然后直接用这个num输出吗? 代码: class Ticket implements Runnable//extends Thread { private int num = 100 public void run()//这时