OpenJDK 和 HashMap,大量数据处理时,避免垃圾回收延迟的技巧(off-heap)
从Java 6开始,要求标准化非堆存储(off-heap)作为Java内部API的提议就已经在JDK强化提案(JEP)中被提出。这种方式的处理能力和堆存储(on-heap)一样高效,并且没有堆存储使用中的一些局限问题。堆存储在百万数量级瞬时使用的对象/值下工作的相当好,但是一旦你试图存储十亿数量级的对象/值时,你就要想办法去避免垃圾回收带来的持续增加的延迟。并且有时系统会要求同时保证大量数据处理和低延迟。非堆存储就是有这样一种能力:独立管理内存空间而不产生垃圾回收压力。Java中管理集合的两个类”Queue“和"HashMap"使用起来相当方便,如果使用这两个已有接口再加上我们自己的垃圾回收机制实现起来应该不是很难。这样既能实现大量数据存储并且能大大减少延迟,相比而言,原有的堆存储方式很容易产生内存不足错误,随之就要重启服务了。
这篇文章将会研究 JEP所带来的影响,将使得我们获悉类似于Java HashMap和新的off-heap的性能。简言之,JEP可能就有“指导”HashMap这个可爱的老家伙的一些新特性的魔法。 JEP所述的特性,在OpenJDK的发布来看,相对于传统的Java平台优先级做了许多重大的改变。
1、关于安全性的重构,这一sun.misc.Unsafe上的有用的部分,被放入了 新的API包。
2、提倡使用新的API包,直接影响高性能的本地内存操作(在off-heap上的本地内存操作对象上)。
3、(通过新的API)提供一个 外部函数接口(FFI)桥 针对Java直接操作系统资源和系统调用。
4、许可了Java运行时能辅助 硬件事务性内存(Hardware Transactional Memory)的提供者能把焦点集中在重写低并发字节码到高并发的 speculatively branched机器码。
5、移除了FUD(坦率的讲这是一种技术偏见),这与使用off-heap编程策略来提升Java的执行性能有关。总的来讲,JEP有几点是很清楚的,在OpenJDK平台上,相对于曾经的 dark craft, secret society of off-heap practitioners,现在的主流对开放是拥抱的。
本文力求(用普遍而温和的方式)让所有对此感兴趣的 Java 开发者都能有所收获。作者希望即使新手也能跟上本文节奏,而不会有看不懂的“磕磕绊绊”;因此不要气馁,耐心坐下来读完吧。本文努力介绍一些历史背景,为以下问题提供思路:
-
堆存储 HashMap 的问题是怎么产生的?
-
在解决这个问题上面,有过哪些经验/教训?
-
在堆存储 HashMap 的应用情景中,有哪些仍未解决的问题?
-
新的 JEP 提供的功能(将 HashMap 非堆存储)能带来哪些好处?
-
未来的 JEP 在解决现在尚未解决的问题上面,有哪些值得期待之处?
那就让我们一起开始这段旅程吧。值得记住的是,在 Java 出现之前,hash 表是实现在原生内存堆中的,如C 和 C++ 都是如此。某种意义上来说,重新引入非堆存储是重新介绍一些古老的技巧,这些技巧当代的开发者往往不曾了解。各种意义上来说,这都是一次“回到未来”的旅程。旅途愉快!
OpenJDK的非堆存储(Off-Heap)的强化提案(JEP)
已经有一些非堆存储(Off-Heap)的强化提案(JEP)被提出来。下面描绘了一个提供非堆存储(Off-Heap)内存的最低要求。方案试图替代现在sun.misc.Unsafe所提供的内容,不仅如此,这些方案还提供了另外一些有用的功能。
提案总结:总的来说就是为sun.misc.Unsafe创建了一个替代的部分,这样就可以不用直接使用那个库。
直接目标:移除需要直接访问的内部类。
间接目标:不提供那些不推荐的方法,也不实现那些不安全(Unsafe)的方法。
成功标准:提供一种方式去实现那些重要的功能,并且达到与那些不安全(Unsafe)和 FileDispatcherImpl的方式一样的性能。
提案动机:当前不安全(Unsafe)的方式就意味着就需要构建更大的,线程上更安全的非堆存储(Off-Heap)结构。这对于最小化垃圾处理器(GC)的开销有益。这对于在进程和内嵌数据库之间的内存共享可以不用C语言和JNI,这也就有可能提供更快更多的移动计算性能。当前的FileDispatcherImpl方式用于实现任意大小内存的映射。(标准API被限制在2GB以内。)
描述:为非堆存储(off-heap)提供一个包装类(类似于 ByteBuffer) ,还需要下面的增强。
-
64位的大小和偏移量
-
对于易失(volatile)的和有序的访问以及比较和交换的操作上有线程安全的结构。
-
JVM优化边界检查,开发者控制边界检查。(允许提供安全性设置)
-
有能力在同一缓冲区的不同记录复用一份缓冲。
-
有能力去映射一个非堆存储(off-heap)数据结构,让缓冲区在优化过的方式下进行边界检查。
保留关键功能
-
支持内存映射文件
-
支持NIO
-
支持把写操作提交到磁盘
候选方案:直接使用sun.misc.Unsafe
测试:sun.misc.Unsafe和内存映射文件有同样的测试需求。附加的测试应该工作在同样的方式下,要求展示的线程安全的操作为AtomicXxxx类。AtomicXxxx类应该被重写并且单独使用公共的API。
风险: 当一群开发者使用了Unsafe之后,他们可能一致认为没有更适合的替代品。这意味着JEP的范围很广,或者创建了新的JEP覆盖了Unsafe中的其他功能。
其他JDK : NIO
兼容性: 提供了向后兼容的库。它兼容java7,如果你有足够的兴趣去研究的话,也有可能兼容java6。(截止到这篇文章,Java 7是当前的版本)。
安全性: 在理想情况下,安全的风险性不能超过ByteBuffer太多。
性能和可扩展性: 优化边界检查是困难的。为了添加更多的普通操作,则需要把功能添加到新的缓冲区,以减少开销,例如读写UTF。
HashMap简史
“Hash Code”这个概念第一次出现是在1953年1月的《Computing literature》中,H. P. Luhn (1896-1964) 在一篇 IBM 的内部备忘录中提出了这个术语。当时 Luhn 是要解决这个问题:“给出组成一本教科书的一系列单词,要得出 100% 完整的(单词,出现页码集)对应关系,最好的算法和数据结构是什么?”
H.P. Luhn (1896-1964) | Luhn 写道, “hashcode” 是基本的运算符。 Luhn 写道, “Associative Array” 是基本的运算数。 由此, ‘HashMap’ (也称为 HashTable) 就这样产生了。 注: HashMap 是由 1896 年出生的计算机科学家提出来的。HashMap 可是个老 家伙啦! |
从 HashMap 的诞生讲到它的早期应用场景,我们从1950年代跳到1970年代
Niklaus Wirth 在他1976年编写的经典著作《算法 + 数据结构 = 程序》中,谈到对于所有的程序,都可以将“算法”视为基本的运算符,将“数据结构”视为基本的运算“数”。 从那时起,数据结构(HashMap,Heap等)发展缓慢。1987年有一个重大突破, Tarjan 提出了非常著名的 F-Heap ;但除此之外,乏善可陈。要知道,HashMap 是1953年第一次提出的,已经过去60余年啦! 与此同时,算法方面 (Karmakar 1984, NegaMax 1989, AKS Primality 2002, Map-Reduce 2006, Grover’s Quantum search - 2011) 则进展迅速,为计算的基础建设带来了崭新的、强大的运算符。 然而,现在到了2014,也许又轮到数据结构来取得重大进展了。从 OpenJDK 平台来看, 非堆 HashMap 就是一个正在发展的数据结构。 HashMap 的历史就介绍到这。下面我们来探索今天的 HashMap 吧。具体来说,我们先来看一看这个老家伙在 Java 中现存的 3 种实现。 |
N. Wirth 1934- |
java.util.HashMap (非线程安全)
对于任何真正的多线程并发用例,它会立即失败,而且是每次都会失败。所有用到它的代码必须使用 Java 内存模型(JMM)的内存屏障(memory barrier)策略(如 synchronized 或 volatile) 来保证顺序执行。
一个简单的失败样例如下: - synchronized 的写入 - 没加 synchronized 的读取 - 真正并发 (2 个 CPU/L1)
我们来看看为什么会失败... |
假设线程1写入 HashMap,那么它做出的改动只会保存在 CPU 1的1级缓存中。然后线程2,在几秒钟后开始在 CPU 2上运行;它读取 HashMap,是从 CPU 2的1级缓存中读出来的——它看不到线程1做出的改动,因为在读和写的线程中都没有读、写间的内存屏障,虽然 Java 内存模型要求线程共享 HashMap 的情形下必须要有。即使线程1的写操作加了 synchronize 也会失败,这样虽然能把它做出的改动写入到主内存中,但线程2仍然看不到这些改动,因为线程2只会从 CPU 2的1级缓存中读取。所以在写操作上加 synchronized 只能避免写操作的冲突。要对于所有的线程都添加必要的内存屏障,你必须也要 synchronize 读操作。
thrSafeHM = Collections.synchronizedMap(hm) ; (粗粒度锁定)
使用“同步”时实现高性能要求低竞争率。这是很常见的,而且在很多情况下这并不像听起来那么坏。然而,一旦你引入任何竞争(多个线程试图同时操作同一集合),性能就会受到影响。在最坏的情况下,如具有很高的锁争用,你可能会得到多个线程比单个线程(操作没有锁定或任何种类的争夺)的性能表现更差的结论。
Collections.synchronizedMap() 返回一个 MT-Safe HashMap.
这是一个通过粗粒度的锁来实现所有关键部分的mutate()和access()操作,这样可以让多个线程操作整个Map。这个结果在Zero MT-concurrency中,意味着一个时刻仅有一个线程可以访问。另一个后果就是作为高锁争用(High Lock Contention)的粗粒度锁,锁住的途径是一种非常不受欢迎的已知条件。关于高锁争用(High Lock Contention)(请看在左边的图片,N个线程争用一个锁,但是迫于阻塞只好等待着,锁已经给了正在运行的线程)。
幸好这是完全同步的,不会真正的同步,隔离(isolation)=序列化(SERIALIZABLE)(总体上这是令人失望的)HashMap陷阱,我们期待的OpenJDK非堆存储(off-heap)JEP已经有一个 值得推荐的期待:硬件事务性内存(Hardware Transactional Memory (HTM))。关于HTM,粗粒度的同步写操作在Java中将会再一次变得很酷!就让HTM通过代码上的零并发和在硬件的零并发来帮助我们,实现真正的并发并且100%的多线程安全。这很酷,对吧?
java.util.concurrent.ConcurrentHashMap (线程安全、智能锁,但并非完美)
在jdk1.5的核心API中,终于发布了java程序员梦寐以求的java.util.concurrent.ConcurrentHashMap。虽然ConcurrentHashMap不能广泛替代HashMap(ConcurrentHashMap消耗更多的资源,在低竞争条件下可能不太适合。),但是它解决了其它类型的HashMap解决不了的问题:提供既有真正的多线程安全,又有真正的多线程并发的能力。让我们画一幅画来准确地描述ConcurrentHashMap为什么(原文是how)这么有用的(有效,有作用,不知道怎么翻译好了。原文:helpful)。
2.每个独立的HashMap子集对应一个锁:N个hash桶(子集)对应N段(Segments)锁。(在图片右边,段(Segments) = 3
3.在设计出将一个高竞争的锁分解成多个不影响数据完整性的锁时,分离锁是非常有用的。
4.更好的并发,在处理"先检查判断状态,再操作"("check-then-act")的竞态条件问题时,concurrentHashMap是一个不需要同步的解决方案。
5.问题:你如何同时保护整个集合(collections)? 获取所有的锁(递归地)?
现在你可能要问了:随着ConcurrentHashMap和java.util.concurrent包的发布,java是一个高性能计算社区(High Performance Computing community)能够在上面创建解决方案来解决他们问题的终极编程平台吗?
不幸的是,很现实的一个回答还是“还没呢”。真的,那么还存在着什么问题呢?
ConcurrentHashMap存在着规模问题和保存中间态对象(medium-lived objects)问题。如果你有一小部分使用concurrentHashMap的关键的集合对象,很可能有些会很大。在某些情况下,在这些集合中存在着大量的中间态对象(medium-lived objects)。这些中间态对象(medium-lived objects)贡献了大部分的GC次数(时间,GC pause times),他们的消耗有可能是短暂对象(short-lived objects)的20倍。长时间存活对象(Long-lived objects)往往停留在终身区(tenured space),短暂对象(short-lived objects)在young区死亡,但是中间态对象(medium-lived objects)会复制到所有的存活空间,并在终身区(trenured space)死亡,中间态对象(medium-lived objects)到处拷贝并在最后被清理产生的消耗十分巨大。最理想的是你能有一个没有GC影响的储存数据的集合。
/******注****/
翻译中的medium-lived objects, short-lived objects,Long-lived objects,tenured space,young space
这类词,对应的是java GC中的词语,对应的中文翻译是啥我记不住了,请编辑或其他朋友修正。
/**********/
ConcurrentHashMap元素在运行时存在Java VM堆里。因为CHM是堆存储,它对于 Stop-the-World (STW) 有着显著的贡献,即使不是最显著的。当STW的GC事件发生,所有应用程序的处理都要忍受着臭名昭著的“紧急暂停”的延时。这种延时,是由CHM(以及它的所有元素)放在堆存储中引起的,是一个惨痛的经历。这是一个经验也是一个高性能计算所不能忍受的问题。
在高性能计算组织完全接受Java之前,必须要有个解决方案来驯服这个堆存储的GC怪兽。
解决方案从精神层面上讲非常简单:就是把CHM放到非堆存储中。
而这个解决方案,OpenJDK的非堆存储JEP当然是支持的。
在我们深入展示非堆存储如何跟HashMap相似之前,我们先完整地了解堆存储的不友好的细节。
堆简史
Java的堆存储是由操作系统分配给JVM的。所有的Java对象都通过堆存储JVM位置/标识来引用。你在堆存储上运行一个对象必定会引用两个堆区域其中之一。这些区域更确切来说为一代。明确来分为:(1)年轻代 (由EDEN和两个SURVIVOR子空间组成) 和 (2) 年老代。 (注:Oracle日前宣布,持久代在JDK7中开始逐步淘汰,而在JDK8将会完全被淘汰)。所有的代都遭受了可怕的“全部停止(Stop-the-World)”完全垃圾回收事件,除非你使用“少量暂停”回收机制例如Azul的Zing.
在垃圾回收的世界里,操作是由“回收机制”执行的,这些回收器的操作对象是堆的“代”(以及子空间)的目标。回收器在堆栈/空间目标中进行操作。完整的垃圾回收是如何工作的内部细节是它自己本身一个(非常大的)主题,有专门的文章会提到。
现在知道一点:如果任何回收器(任何类型的)操作任何一代的堆空间都会造成“停止一切(Stop The World)”的事件——这是一个非常严重的问题。
这是一个问题必须得有个解决方案。
这是一个问题只有非堆存储JEP可以解决。
让我们仔细看看。
Java堆布局: 查看它的历代
垃圾回收使得编程变得更加容易,但是在SLA目标的世界里,无论是书面的还是暗示的(我的Java Applet暂停30秒不是一种选择),停止一切(Stop-The-World)时间暂停对于许多Java开发人员来说是一个很头疼的问题,摆在他们面前的只有性能问题。顺便提一下,还有许多其他性能问题需要处理,只有在STW不再是问题的时候。
使用off-heap存储的好处,就是中等寿命对象的数量可以大幅度下降。它甚至也可以降低短寿命对象的数量。对于高频交易系统,它一天可以创建的垃圾比你的Eden空间大小还要小,这意味着你可以运行一整天而不需要一个简单的回收。一旦你有非常低的内存压力,以及部分对象已经到达年老代(tenured)空间,调整你的GC就会变得很琐碎。通常你甚至不需要设置GC的参数(除非希望增加eden区的大小)。
通过移动对象到非堆存储,Java应用程序往往能够收回监管控制自己的命运,满足SLA性能的期望和义务。
等一下,刚刚最后一句说啥来着?
注意:所有乘客,请收起你的托盘并坐直来。OpenJDK非堆存储JEP的中央租户是一个非常值得重复的事情。
移动回收(如HashMap)到非堆存储,Java应用程序经常能够请求他们的回收(不再依赖于STW的GC机制中的“紧急暂停”事件)去控制他们自身的命运,满足SLA性能的期望和义务。
这是一个很实用的选择,在Java的高频率交易系统中已经在使用。
这个选择也彻底需要Java保持着对高性能计算越来越多的吸引力。
堆存储的优势
-
常见的,写普通的Java代码。所有有经验的Java开发人员都可以做到。
-
访问内存的安全性问题。
-
自动的GC服务——无需自身管理的malloc()/free()操作。
-
完整的 Java Lock API和JMM相结合。
-
添加无序列化/复制数据到一个结构中去。
非堆存储的优势
-
控制"停止一切(Stop the World)"的GC事件到你比较满意的层次。
-
可以超越在规模上的堆存储结构(当使用堆存储的时候会变得很高)
-
可以作为一个本地的IPC传输(无需java.net.Socket的IP回送)
-
分配器的注意事项:
-
NIO DirectByteBuffer到/dev/shm (tmpfs)的map?
-
或者直接sun.misc.Unsafe.malloc()?
-
文档(低地址……执行代码的地方)
-
数据(通过sbrk(2)从低地址升级到高地址来掌管)
-
栈(从高地址到低地址来掌管)
HashMap的介绍 … 有什么新的问题使得这个“老家伙”得以解决呢(通过使用非堆存储)?
介绍 OpenHFT HugeCollections (SHM)
到底哪里才是“非堆存储”?
下图介绍了两个使用ShardHashMap(SHM)作为进程间通信(IPC)的Java VM过程(PID1和PID2)。图表底部的水平轴代表的是完全SHM操作系统的所在域。当OpenHFT对象被操作时,它就会在操作系统中物理内存的用户地址空间或者内核地址空间的某处。往深一层思考,我们知道他们以“关于进程”的局部开始着手。从Linux操作系统来看,JVM是一个a.out (通过调用 gcc呈现的)。当a.out在运行的时候会有一个PID。一个 PID的 a.out (在运行时)包含以下三个方面:
这是PID在操作系统中的表现形式。也就是说,PID是一个执行的JVM,JVM有它自己操作对象潜在的局部性。
从JVM来看,操作对象作为On-PID-on-heap(一般的Java)或者On-PID-off-heap(通过Unsafe或者NIO到Linux mmap(2)的桥梁)。无论在On-PID-on-heap还是在On-PID-off-heap,所有的操作对象仍然存活在用户的地址空间。在C/C++中,API(操作系统调用的)提供了允许C++操作对象有 Off-PID-off-heap的地方,这些操作对象都寄存在内核地址空间内。
(点击图片可以放大)
根据上面这个图,分为以下六个方面:
#1. 为了更好地落实图中的流程,我们可以先遵从JavaBean的规范,把PID 1定义成BondVOInterface。我们想要证明(以下的编号是上图中流程的编号)如何操作 Map<String,BondVOInterface> ,并把非堆存储的优势标注起来。
public interface BondVOInterface { /* add support for entry based locking */ void busyLockEntry() throws InterruptedException; void unlockEntry(); long getIssueDate(); void setIssueDate(long issueDate); /* time in millis */ long getMaturityDate(); void setMaturityDate(long maturityDate); /* time in millis */ double getCoupon(); void setCoupon(double coupon); // OpenHFT Off-Heap array[ ] processing notice ‘At’ suffix void setMarketPxIntraDayHistoryAt(@MaxSize(7) int tradingDayHour, MarketPx mPx); /* 7 Hours in the Trading Day: * index_0 = 9.30am, * index_1 = 10.30am, …, * index_6 = 4.30pm */ MarketPx getMarketPxIntraDayHistoryAt(int tradingDayHour); /* nested interface - empowering an Off-Heap hierarchical “TIER of prices” as array[ ] value */ interface MarketPx { double getCallPx(); void setCallPx(double px); double getParPx(); void setParPx(double px); double getMaturityPx(); void setMaturityPx(double px); double getBidPx(); void setBidPx(double px); double getAskPx(); void setAskPx(double px); String getSymbol(); void setSymbol(String symbol); } }
PID 1(在上图所示的 step#1,使用了Interface)调用OpenHFT的 SharedHashMap工厂类,如下所示:
SharedHashMap<String, BondVOInterface> shm = new SharedHashMapBuilder() .generatedValueType(true) .entrySize(512) .create( new File("/dev/shm/myBondPortfolioSHM"), String.class, BondVOInterface.class ); BondVOInterface bondVO = DataValueClasses.newDirectReference(BondVOInterface.class); shm.acquireUsing("369604103", bondVO); bondVO.setIssueDate(parseYYYYMMDD("20130915")); bondVO.setMaturityDate(parseYYYYMMDD( "20140915")); bondVO.setCoupon(5.0 / 100); // 5.0% BondVOInterface.MarketPx mpx930 = bondVO.getMarketPxIntraDayHistoryAt(0); mpx930.setAskPx(109.2); mpx930.setBidPx(106.9); BondVOInterface.MarketPx mpx1030 = bondVO.getMarketPxIntraDayHistoryAt(1); mpx1030.setAskPx(109.7); mpx1030.setBidPx(107.6);
现在,OpenHFT从堆存储到非堆存储的魔法要开始了!看仔细了……浏览完整篇文章后你就会发现,每次提到的“魔法”意味着这里是最值得“观光”的时刻:
#2. 在每个运行的进程中调用上面所提到的OpenHFT,编译BondVOInterface£native 内部的实现,使得更好地控制字节寻址运算,并完成非堆存储中的abstractAccess()/abstractMutate()操作设置(使用接口getXX()/setXX()并遵循JavaBean的命名规范)。OpenHFT运行时采用你的接口并在一个实现类中编译,这将为你提供了更为明确的非堆存储性能的桥梁。数组被封装成带有索引的getter和setter,数组的接口也以同样的方式生成一个外部接口。数组的setter和getter的命名方式为 setXxxxAt(int index, Type t); and getXxxxAt(int index); (注:数组的gettr/settr的命名均以“At”后缀结尾)。
所有细节已经通过OpenHFT JIT编译器中的一个正在运行的进程来向你描述清楚了。你现在只要做的就是提供接口。是不是很酷啊?
#3. PID 1调用OpenHFT API中的shm.put(K, V); 通过键值 (V = BondVOInterface) 把数据塞进非堆存储的SHM中。这样我们可以跨越在[2]中建立的OpenHFT的桥梁。
我们现在就是非堆存储啦!真是太神奇了,不是吗? :-)
我们接下来看看如何从PID 2到达这一步。
#4. 一旦PID 1已经完成了把它的数据塞进非堆存储SHM中,PID 2才开始调用完全相同的OpenHFT工厂类,如下所示:
SharedHashMap<String, BondVOInterface> shmB = new SharedHashMapBuilder() .generatedValueType(true) .entrySize(512) .create( new File("/dev/shm/myBondPortfolioSHM"), String.class, BondVOInterface.class );
开始它横跨OpenHFT所构建的桥梁的旅程。当然,假设PID 1和PID 2是存在于同一台本地机子的操作系统中,共享文件夹 /dev/shm (同样具有优先访问 /dev/shm/myBondPortfolioSHM 文件夹权限).
#5. PID 2 调用V = shm.get(K); (每次都会创建一个新的非堆存储的引用) 或者 PID 2 调用 V2 = shm.getUsing(K, V); 来根据你的选择重新引用一个非堆存储 (或者当 K 不是一个Entry 的时候返回NULL)。在OpenHFT的API中,还有第三种 获取 签名的方式提供给 PID 2: V2 = acquireUsing(K,V); 其中的差别是,K 必须不能是一个 Entry, 这样你就将不会返回NULL - 但是 - 相应的你 将会返回一个新提供的非NULL V2的占位符引用 。这个引用允许 PID 2 适当地去操作位于SHM的非堆存储 V2 Entry。
注: 每当 PID 2 调用 V = shm.get(K); 它就会返回一个新的非堆存储的引用。这就使得你每次为这些数据创建引用的时候产生了很多垃圾,直到你销毁它。 然而, 当 PID2 调用 V2 = shm.getUsing(K, V); 或者 V2 = shm.acquireUsing(K, V);, 非堆存储的引用就被移到新键值的位置上,而这个操作会减少GC,因为你已经自己在回收每一个东西了。
注:在这个地方是没有拷贝发生的,只有在数据位于非堆存储空间才会被设置或者改变。
BondVOInterface bondVOB = shmB.get("369604103"); assertEquals(5.0 / 100, bondVOB.getCoupon(), 0.0); BondVOInterface.MarketPx mpx930B = bondVOB.getMarketPxIntraDayHistoryAt(0); assertEquals(109.2, mpx930B.getAskPx(), 0.0); assertEquals(106.9, mpx930B.getBidPx(), 0.0); BondVOInterface.MarketPx mpx1030B = bondVOB.getMarketPxIntraDayHistoryAt(1); assertEquals(109.7, mpx1030B.getAskPx(), 0.0); assertEquals(107.6, mpx1030B.getBidPx(), 0.0);
#6. 一个非堆存储的记录是一个为了非堆存储操纵和偏移而包装成字节的引用。通过改变这两个,只要是你选择的接口,内存的任何角落都能被访问。当PID 2操作'shm'的引用,设置了正确的字节和偏移,读取存储在/dev/shm文件视图的哈希映射来进行运算。在getUsing()返回后,偏移量的计算是微乎其微的,也是内联的。即一旦代码经过JITed编译的,get()和set()方法就会变成简单的机器代码指令来访问这些字段,只有在你访问的字段是可读或者可写的,这才是真正意义上的零拷贝!非常棒!
//ZERO-COPY // our reusable, mutable off heap reference, generated from the interface. BondVOInterface bondZC = DataValueClasses.newDirectReference(BondVOInterface.class); // lookup the key and give me my reference to the data if it exists. if (shm.getUsing("369604103", bondZC) != null) { // found a key and bondZC has been set // get directly without touching the rest of the record. long _matDate = bondZC.getMaturityDate(); // write just this field, again we need to assume we are the only writer. bondZC.setMaturityDate(parseYYYYMMDD("20440315")); //demo of how to do OpenHFT off-heap array[ ] processing int tradingHour = 2; //current trading hour intra-day BondVOInterface.MarketPx mktPx = bondZC.getMarketPxIntraDayHistoryAt(tradingHour); if (mktPx.getCallPx() < 103.50) { mktPx.setParPx(100.50); mktPx.setAskPx(102.00); mktPx.setBidPx(99.00); // setMarketPxIntraDayHistoryAt is not needed as we are using zero copy, // the original has been changed. } } // bondZC will be full of default values and zero length string the first time. // from this point, all operations are completely record/entry local, // no other resource is involved. // now perform thread safe operations on my reference bondZC.addAtomicMaturityDate(16 * 24 * 3600 * 1000L); //20440331 bondZC.addAtomicCoupon(-1 * bondZC.getCoupon()); //MT-safe! now a Zero Coupon Bond. // say I need to do something more complicated // set the Threads getId() to match the process id of the thread. AffinitySupport.setThreadId(); bondZC.busyLockEntry(); try { String str = bondZC.getSymbol(); if (str.equals("IBM_HY_2044")) bondZC.setSymbol("OPENHFT_IG_2044"); } finally { bondZC.unlockEntry(); }
意识到全局的OpenHFT的堆存储←→非堆存储的魔法会在以上的图表中发生是非常重要的。
事实上,在第#6步,OpenHFT SHM的实现是在运行时拦截arg-2,重载 V2 = shm.getUsing(K, V);。本质上,SHM实现的是查询:
( ( arg2 instanceof Byteable ) ? ZERO_COPY : COPY )
以及作为零拷贝来执行(通过更新引用),来取代完整拷贝(通过Externalizable)。
如何实现非堆存储引用功能的关键接口是Byteable。这是允许引用(重新)分配的。
public interface Byteable { void bytes(Bytes bytes, long offset); }
如果你实现了你自己的类去支持这个方法,你可以实现或者生成属于你自己的Byteable的类。
直到现在,如之前我们所提到的,你可能已经禁不住觉得“这一切发生得太奇妙了”。这所有的魔法就在应用程序执行的进程中发生了!使用运行编译器作为 BondVOInterface 接口的输入,OpenHFT内部确定了接口的源码并编译该源码(同样的,是在进程中)到OpenHFT的实现类。如果你不想在运行过程中才生成该类,你可以提前生成并在构建的时候编译它。OpenHFT内部会重新加载新的实现类到可运行的上下文中。接下来,运行时再物理执行 BondVOInterface£native 内部类生成的方法去影响映射到非堆存储的 Bytes[]记录上的零拷贝操作器的容量。这个容量是零拷贝的,所以你将在一个线程上执行线程安全操作,这对另一个线程是可见的,尽管另一个线程是在别的进程中。
你可以看到OpenHFT SHM奇迹之处:Java现在拥有真正意义上的零拷贝IPC。
嘛哩嘛哩哄!
性能结果: CHM vs.SHM
On Linux 13.10, i7-3970X CPU @ 3.50GHz, hex core, 32 GB of memory.
SharedHashMap -verbose:gc -Xmx64m
ConcurrentHashMap -verbose:gc -Xmx30g
当然,主要导致CHM低于SHM438%的原因是,CHM要忍受21.8秒的STW GC事件。但是从SLA的角度来看,导致这个的原因(并没有针对此进行补救)是无关紧要的。对于SLA来说,CHM只是慢了438%。而从SLA的角度来看,CHM的性能是难以忍受的慢。
JSR-107的自适性:SHM为(100%可互操作)非堆存储的JCACHE运算对象
Java Community Process将在2014年第二季度宣布JCACHE所发布的JSR-107 EG标准规范——Java缓存标准API/SPI。 JCACHE 将会为Java缓存社区做那些过去JDBC为Java RDBMS社区做的事情。在JCACHE的核心和基础是它原始的缓存操作接口 javax.cache.Cache<K,V>。如果仔细看Cache API就会发现Cache非常接近Map的超集(只有一点点学术上的差别)。JCACHE其中一个主要目的是帮助实现可扩展的(向上和向外扩展)解决 Java数据局部性、延迟和缓存的问题。 所以,如果JCACHE的中央运算对象是一个Map,而JCACHE其中一个主要的任务是解决数据局部性/延迟性的问题,那使用OpenHFT的非堆存储SHM作为实现JCACHE的主要运算对象的接口究竟适不适合呢?对于某些Java高速缓存的使用情况, OpenHFT的非堆存储SHM是非常完美的。
在这里稍等片刻(请留个位)这篇文章将分享如何把OpenHFT SHM完全作为一个JSR-107可互操作的非堆存储JCACHE运算对象。在这之前,我们想陈述这个javax.cache.Cache接口是java.util.Map接口的超集事实。我们需要确切地知道“一个超集有多大?”……因为这会影响到我们究竟需要做多少工作才完全达到100%河100%完全适应SHM作为实现接口。
- 什么是Cache必须提供,而基础的HashMap不提供?
-
Eviction, Expiration
-
WeakRef, StrongRef (顺便提一下,和非堆存储Cache的实现类无关)
-
本地角色 (例如 Hibernate L2)
-
EntryProcessors
-
ACID事务处理
-
事件监听
-
“Read Through” 操作器 (同步/异步)
-
“Write Behind” 操作器 (同步/异步)
-
JPA 参与
- OpenHFT+Infinispan “联姻日” 计划 (JCACHE庆典)
下图描述的是开发工作范围内的一小部分,它将采取社区主导OpenHFT程序员去适应/贡献OpenHFT非堆存储SHM作为完整的JSR-107可互操作JCACHE运算对象(社区主导开源JCACHE 提供方=RedHat Infinispan)。
(点击图片放大)
总结:非堆存储Hashmap...今天,明天,“直到永远”
在这接近“最后一站”,我们希望在离别前给大家讲个寓言故事作参考。
社区主导开源非堆存储HashMap供应者和JCACHE提供的供应商(包括私有和开源)之间的关系可以协同作用的。每一块都为使得最终用户的非堆存储经验更丰富发挥着重要作用。非堆存储Hash-Map提供者可以实现非堆存储HashMap的核心(如JCACHE)运算对象。JCACHE供应商(包括私有和开源)可以使运算对象适应到他们产品中,然后提供核心JCACHE运算符(和基础结构)。
这种关系就像奶牛(奶农,如果你愿意,核心运算对象制造方=牛奶)与乳业公司(牛奶经营者操作符 set={巴氏灭菌,脱脂,1%,2%,一半一半,等等})之间的关系。总之,(奶牛,乳业公司)是共同生产一个产品,使得最终用户能够享受到高于(奶牛,乳业公司)不合作而得到的。最终用户是需要他们两个的。
但是,“买家要担心”这句话要送给最终用户:
有的人应该会遭遇私有厂商传播闭源非堆存储HashMap/Cache的解决方案,声称他们闭源非堆存储的运算对象在某方面“胜于”开源和社区主导的方法。好吧,只要记住这一点:
乳业公司并不生产牛奶。奶牛可以产奶。
奶牛使得牛奶变得公开化,24/7,以及完全不感到厌烦的焦点。乳业公司可以使得牛奶变得更多元化(一半一半,2%,1%,脱脂)……所以他们只是有机会发挥重要作用……但他们并不产奶。眼下开源“奶牛”是制造非堆存储HashMap的“牛奶”。如果私有解决方案提供商可以使得那个牛奶更多元化,那就去做吧,这种努力的尝试也是大家所希望看到的。但是并不鼓励这些提供商试图去宣称他们私有的牛奶是在各方面都更胜一筹的“牛奶”。因为奶牛才是制造最好的牛奶。
最后,Java开始融入高性能计算社区是一件多么值得兴奋的事啊。一切都会有很大的变化,而所有的变化都是好的。
从 并发包, 从越来越多优秀的 现代 GC 解决方案, 从 非阻塞 I/O 性能, 从 套接字直接协议的 native RDMA, JVM 内部方法, …. , 所有的方法都是通向本地 Caching, OpenHFT的SHM作为 原生 IPC 传输, 以及 机器级别HTM-助手功能 都被要求在这个OpenJDK非堆存储JEP,有一点是清楚的:OpenJDK的平台社区确实是把提高性能的优先级放在最高。 have a high-priority to improve performance.
再来看看这个可爱的老家伙HashMap现在能做的!OpenJDK,OpenHFT,Linux和非堆存储HashMap现在在“低处”是朋友了(即本地操作系统)。
保护好现在不受STW GC干扰,HashMap现在作为一个重要的HPC数据结构运算对象而重生。永葆年轻,HashMap...永远年轻!
感谢和我们一起共同履行,我们希望你会欣赏这次的旅程。下次再见。