速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

OpenJDK 与 HashMap……放心地教这个老家伙一些新(非堆!)技巧

  • 2015-05-25
  • 本文字数:14744 字

    阅读完需:约 48 分钟

OpenJDK 的非堆 JDK 增强提议(JDK Enhancement-Proposal,JEP)试图标准化一项基础设施,它从 Java6 开始,只能在 HotSpot 和 OpenJDK 内部使用。这种设施能够像管理堆内存那样管理非堆内存,同时避免了使用堆内存所带来的一些限制。对于上百万短期存在的对象 / 值来说,堆内存工作起来是很好的,但是如果你想要增加一些其他的需求,如几十亿的对象 / 值的话,假若你想避免持续增加的 GC 暂停,那么你需要做一些更加有创造性的工作。在有些场景下,你还需要完全避免暂停。非堆提供了构建“arenas”内存存储的功能,它遵循自己的规则,并不会影响到 GC 的暂停时间。两个很容易使用 arenas 的集合是 Queue 和 HashMap,因为它们具有很简单的对象生命周期,所以编写自己的垃圾收集并不太繁琐。这种集合所带来的好处就是它的大小能够比传统的堆集合大得多,甚至超过主存储器(main memory)的规模,而对暂停时间的影响却微乎其微。相比之下,如果你的堆大小超过了主存储器,那么你的机器就会变得不可用,可能会需要关电源重启。

本文将会调查这个 JEP 的影响,它会让大家熟悉的 Java HashMap 具备新的非堆功能。简而言之,这个 JEP 所具有的魔法能够“教会”HashMap(这是一个可爱的 _ 老 _ 家伙 old dog)一些 _ 新的 _ 技巧。这个 JEP 会要求将来的 OpenJDK 发布版本与传统 Java 平台的优先级产生很大的差异:

  1. 将 sun.misc.Unsafe 中有用的部分重构为一个新的 API 包
  2. 提倡使用新的 API 包在非堆的原生内存操作对象上直接进行高性能的原生内存操作。
  3. (通过新的 API)提供外部功能接口(Foreign Function Interface,FFI)来桥接 Java 与操作系统资源(Operating System resource)和系统调用(system call)。
  4. 允许 Java 运行时借助硬件事务内存(Hardware Transactional Memory)提供者的foci,将低并发的字节码重写为高并发性的 speculatively branched 机器码。
  5. 移除 FUD(坦率的说,这是一种技术上的偏执),它与使用非堆编程策略来实现 Java 性能的提升有关。最终,基本明确的是这个 JEP 要求 OpenJDK 平台要开放性地将其纳为主流,它曾经被视为黑暗的工艺、非堆参与者的秘密组织。

本文力图(以一种通俗和温和的方式)让所有感兴趣的 Java 开发人员都能有所收获。作者希望即使是新手也能完整地享受本文所带来的这段旅程,尽管在路途上可能会有一些不熟悉的“坑坑洼洼”,但是不要气馁——希望您在位置上安坐直到文章结束。本文会提供一个有关历史问题的上下文,这样你会对下面的问题具备足够的背景知识:

  • 堆 HashMap 的问题是怎么产生的?
  • 为了应对这些问题,历史上所给出方案的成功 / 失败之处是什么?
  • 在堆 HashMap 的使用场景中,依然存在的未解决问题是什么?
  • 新 JEP 所提供的功能能够带来什么助益(也就是将 HashMap 变为非堆的)?
  • 对于非堆 JEP 所没有解决的问题,将来的 JEP 能够给我们什么期待呢?

那么,让我们开始这段旅程吧。需要记住的一点是在 Java 之前,哈希表(hash table)是在原生内存堆中实现的,比如说在 C 和 C++ 中。在一定程度上可以说,重新介绍非堆存储是“老调重弹”,这是大多数当前的开发人员所不知道的。在许多方面可以说,这是一趟“回到未来”的旅行,因此享受这个过程吧!

OpenJDK 非堆 JEP

针对非堆 JEP,已经有了几个提议(submission)。下面的样例展现了支持非堆内存的最小需求。其他的提议尝试提供 sun.misc.Unsafe 的替代品,这个类是目前的非堆功能所需要的。它们还包含了很多其他有用和有趣的功能。

_JEP 概述:_ 创建 sun.misc.Unsafe 部分功能的替代品,这样就没有必要再去直接使用这个库了。

_ 目标:_ 移除对内部类的访问。

非目标: 不支持废弃(deprecated)的方法,也不支持 Unsafe 尚未实现的方法。

_ 成功指标:_ 实现与 Unsafe 和 FileDispatcherImpl 相同的核心功能,并且性能方面要与之保持一致。

驱动力: 目前来讲,Unsafe 是构建大规模、线程安全的非堆数据结构的唯一方法。在如下的领域,这种方式会很有用,如最小化 GC 的影响、跨进程共享内存以及在不使用 C 和 JNI 的情况下实现嵌入式数据库,因为使用 C 和 JNI 的话,可能会更慢并且更加困难。FileDispatcherImpl 目前需要将内存映射为任意的大小。(标准 API 限制为小于 2GB。)

描述: 为非堆内存提供一个包装类(类似于 ByteBuffer ),但是具有如下的功能增强。

  • 64 位的大小和偏移。
  • 线程安全结构,如 volatile 和顺序访问、比较和交换(compare and swap,CAS)操作。
  • JVM 优化的边界检查,或开发人员控制边界检查。(提供的安全设置允许这样做)
  • 在一个缓冲区中,能够为不同的记录重用部分缓冲区。
  • 能够将非堆的数据结构映射到这样一个缓冲区之中,在这个过程中,边界检查已经被优化掉了。

要保留的核心功能:

  • 支持内存映射文件
  • 支持 NIO
  • 支持将写操作提交到磁盘上。

_ 替代方案:_ 直接使用 sun.misc.Unsafe。

_ 测试:_ 测试需求应该与目前的 sun.misc.Unsafe 和内存映射文件相同。还需要额外的测试来证明它与 AtomicXxxx 类一致的线程安全操作。AtomicXxxx 类可以使用这个公开 API 进行重写。

_ 风险:_ 有很多的开发人员在使用 Unsafe,他们可能并不认同合适的替代方案是什么。这意味着这个 JEP 的范围可能会扩大,或者会创建新的 JEP 来涵盖 Unsafe 中的其他功能。

其他 JDK: NIO

_ 兼容性:_ 需要保持向后兼容的库。这可以针对 Java 7 实现,如果有足够兴趣的话,也可以支持 Java 6。(当撰写本文的时候,当前的版本是 Java 7)

安全性:理想情况下,安全性的风险不应该超过当前的 ByteBuffer。

性能和可扩展性:优化边界检查会比较困难。可能需要为这个新的缓冲区添加更多的功能,通过通用的操作来减少损耗,如 writeUTF、readUTF。

HashMap 简史

“哈希码(Hash Code)”这个术语最早于 1953 年 1 月出现在 Computing 文献之中, H. P. Luhn (1896-1964)在编写 IBM 内部备忘录时,使用到了这个术语。Luhn 试图解决的问题是“给定一个文本格式的单词流,要实现 100% 完整的(单词、页集)索引,最优的算法和数据结构是什么样的?”

H.P. Luhn (1896-1964)

Luhn 写到“hashcode”是基本的运算符(operator)。

Luhn 写到“关联数组(Associative Array)”是基本的运算对象(operand)。

术语“HashMap”(亦称为 HashTable)逐渐形成了。

注意:HashMap 这个词源自出生于 1896 年的计算机科学家。HashMap 真的是个老家伙了!

让我们将 HashMap 的故事从它的起始阶段转移到早期的实际使用阶段,也就是从 1950 年代中期跳到 1970 年代中期。

在其 1976 年写成的经典著作《算法+ 数据结构= 程序》之中, Niklaus Wirth 讨论了“算法”,将其视为基本的“运算符”,并将“数据结构”视为基本的 “运算对象”,对于所有的计算机程序来讲这都是适用的。

从那时开始,数据结构领域(HashMap、堆等)的进步是很缓慢的。在 1987 年,我们确实也看到了 Tarjan _ 非常 _ 重要的 F-Heap 突破,但是除此之外,在运算对象方面确实乏善可陈。当然需要记住的是,HashMap 最早出现于 1953 年,已经有超过六十年的历史了!

然而,在算法社区( Karmakar 1984, NegaMax 1989, AKS Primality 2002, Map-Reduce 2006, Grover Quantum 搜索 - 2011)却是发展迅速,为计算机基础领域提供了新鲜和强大的运算符。

但是在 2014 年,数据结构领域可能再次会有一些重大的进展。在 OpenJDK 平台方面,非堆 HashMap 是一个正在不断发展的数据结构。

关于HashMap 的历史,我们已经介绍了很多的内容。现在,我们开始探索一下如今的HashMap,尤其是看一下在Java 中,HashMap 当前的三个变种。

N. Wirth 1934-

java.util.HashMap(非线程安全)

在真正的多线程(Multi-Threaded,MT)并发用户场景下,它会快速失败,并且每次都是如此。所有地方的代码必须使用 Java 内存模型(Java Memory Model,JMM)的内存屏障策略(如 synchronized 或 volatile)以保证执行的顺序。

会发生失败的简单假设场景:

- 同步写入

- 非同步读取

- 真正并发(2 x CPU/L1)

让我们看一下为什么会发生失败……

假设 Thread 1 往 HashMap 中进行写入,而写入的效果 _ 只 _ 存储在 CPU 1 的一级缓存之中。然后,Thread 2 几秒后得以在 CPU 2 上继续执行,它会读取来自于 CPU 2 一级缓存中的 HashMap——这并不会看到 Thread 1 的写入,这是因为写入和读取线程中的写读操作之间 **没有内存屏障操作,而这是共享状态的 Java 内存模型所需要的。即便 Thread 1 同步写操作,写操作的效果刷新到了主内存中,Thread 2 依然看不到变化的效果,因为读取操作来自于 CPU 2 的一级缓存。所以,在写入操作上的同步只能避免 _ 写入 _ 操作的冲突。要满足所有线程的内存屏障操作,你必须还要 ** 同步读取。

thrSafeHM = Collections.synchronizedMap(hm) ;(粗粒度的锁)

要使用“synchronized”达到高性能的话,竞争出现的机率要比较低。**这种场景是非常常见的,因此在很多场景中,这并不会像听上去那么糟糕。** 但是,如果你要引入竞争的话(多个线程同时尝试操作同一个集合),就会影响到性能了。在最坏的场景下,如果有高频率的竞争,最终的结果可能是多个线程的性能甚至比不上单个线程的性能(没有任何锁定和竞争的操作)。

这是通过在所有的 key 上粗粒度地阻塞所有 mutate() 和 access() 操作实现的,实际上就是在所有的线程操作符上阻塞整个 Map 操作对象,只有一个线程可以对其进行访问。这导致的了零多线程并发(Zero MT-concurrency),也就是同时只有一个线程在进行访问。这种粗粒度锁的另外一个结果是我们非常不喜欢的一个场景,被称之为高度的锁竞争(High Lock Contention)(参见左图,N 个线程在竞争一个锁,但是必须要阻塞等待,因为这个锁被正在运行的一个线程所持有)。

对于这种完全同步、非并发、isolation=SERIALIZABLE(并且总体上来说令人失望)的 HashMap,幸好在我们即将到来的 OpenJDK 非堆 JEP 中有了推荐的补救措施:硬件事务性内存(Hardware Transactional Memory,HTM)。借助HTM,在Java 中编写粗粒度同步阻塞将会再次变得很酷。HTM 会帮助将零并发的代码在硬件层面转换为真正并发且100% 线程安全的。这会再次变得很酷,对吧?

java.util.concurrent.ConcurrentHashMap(线程安全、更巧妙的锁,但是依然不“完美”)

在 JDK 1.5 发布的时候,Java 程序员发现在核心 API 中包含了期待已久的 java.util.concurrent.ConcurrentHashMap。尽管 CHM 并不能成为 HashMap 统一的替代方案(CHM 使用更多的资源,在低竞争的场景下可能并不合适),但是它确实解决了其他 HashMap 所不能解决的问题:实现真正的多线程安全和真正的多线程并发。让我们画图来展现一下 CHM 能够带来什么好处 _。_

  1. 锁分片
  2. 对于 java.util.HashMap 中独立的子集有一个锁的集合:N 个 hash 桶 /N 个分段(Segment)锁。(右侧的图中,Segments=3)
  3. 如果在设计时,想要将高度竞争的锁重构为多个锁,而又不损害数据完整性时,锁分段是非常有用的。
  4. 对于“检查并执行(check-then-act)”的竞态条件问题,它能够提供并发性更好且非同步的解决方案。
  5. 问题:该如何同时保护整个集合?(递归)获取所有的锁?

那么,现在你可能会问:有了 ConcurrentHashMap 和 java.uti.concurrent 包,高性能计算社区(High Performance Computing community)是否可以将Java 作为编程平台来构建方案以解决他们的问题呢?

非常遗憾的是,最为现实的答案依然是“时机尚未成熟”。那么,还存在的问题到底是什么?

CHM 有一个问题是有关扩展性和持有中等生命周期(medium-lived)对象的。如果有少量的重要集合使用 CHM 的话,那么其中有一些可能会非常大。在有些场景下,你会有大量中等存活时间的对象保存在这样的集合中。中等生命周期对象的问题在于它们占用了大部分的 GC 暂停时间,比起短期存活(short-lived)的对象,它们的成本可能会高上 20 倍。长期存活的对象会位于老年代,而短期存活的对象在新生代就会死亡,但是中等生命周期的对象会经历所有的 survivor 空间复制,然后在老年代死亡,这使得它们的复制和最终清理成本很高。理想情况下,你所需要的存储数据的集合对 GC 的影响是零。

ConcurrentHashMap 中的元素在运行时位于 Java VM 的堆中。CHM 位于堆上,因此它是造成 Stop-the-World (STW)暂停的重要因素,我们不将其称之为 _ 最重要的因素 _ 其实也差不多。当 STW GC 事件发生时,所有的应用程序线程都会经历“难堪的暂停”延迟。这种延迟,是由位于堆上的 CHM(及其所有的元素)造成的,这是一种痛苦的体验。这种体验和问题是高性能计算社区所无法忍受的。

在高性能计算社区 _ 完全 _ 拥抱 Java 之前,_ 必须 _ 要有一种方案驯服堆GC 这个怪兽。

这个方案在理论上非常简单:将CHM 放在堆外。

当然,该方案_ 也正是_ 这个OpenJDK 非堆JEP 所要设计支持的。

在深入介绍HashMap 非堆生命周期之前,让我们看一下有关堆的细节,这些细节描述了它的不便之处。

Heap 的简史

Java 堆内存是由操作系统分配给 JVM 的。所有的 Java 对象都是通过其堆上的 JVM 地址 / 标识来进行引用的。堆上的运行时对象引用肯定会位于两个不同的堆区域中的某一个上。这些区域更为正式的叫法是 _ 代(generation)。具体来讲:(1)新生(Young)代(包括 EDEN 区和两个 SURVIVOR 子空间)以及(2)老年(Tenured)代。(注意:Oracle 宣布 **永久代** 将会从 JDK 7 开始逐渐淘汰,并会在 JDK 8 中完全消除掉)。所有的分代 _ 都会导致恐怖的“Stop-the-World”完整垃圾回收事件,除非你使用“无暂停(pause less)”的收集器,如 Azul 的 Zing

在垃圾收集的领域,_ 操作 _ 是由“收集器”执行的,这些收集器的 _ 操作对象 _ 就是堆中的目标分代(及其子空间)。收集器会操作在堆的目标分代 / 空间上。垃圾收集的完整内部细节是另外一个(很大的)主题,在一篇专门的文章中进行了阐述。

就现在来说,记住这一点就够了:如果(任意类型的)某个收集器在任何分代的堆空间上导致“Stop the World”事件,那么这就是一个严重的问题。

这是一个必须要有解决方案的问题。

这是非堆JEP 能够解决的一个问题。

让我们近距离地看一下。

Java 堆的布局:_ 按照分代 _ 的视角

垃圾收集使得编写程序容易了许多,但是当面临 SLA 目标时,不管是写在书面上的还是隐含的(比如 Java Applet 停止 30 秒是 _ 不能 _ 允许的),Stop-The-World 暂停时间都是一个很令人头疼的问题。这个问题非常严重,以至于对于很多 Java 开发人员来说,这是他们所面对的唯一的问题。值得一提的是,当 STW 不再是问题的时候,还有很多其他要解决的性能问题。

使用非堆存储的收益在于中等生命周期对象的数量会急剧下降。它甚至还能降低短期存活对象的数量。对于高频率的交易系统,一天之内所创建的对象可能会比 Eden 区还小,这意味着一天之内甚至不会触发一次 minor 收集。一旦内存方面的压力降低了,并且有很少的对象能够到达老年代,那么优化 GC 将会变得非常容易。通常你甚至不需要设置任何的 GC 参数(除了可能会增加 eden 的大小)。

借助转移到非堆上,Java 应用通常可以宣告完全主宰自己的命运,也就是能够满足性能的 SLA 期待和条款。

稍等。刚才最后一句话是什么意思?

注意:所有的乘客,请收起您的折叠板并将座椅调至直立状态。这是很值得重复的一句话,也是这个 OpenJDK 非堆 JEP 所解决的核心问题所在。

通过将集合(如 HashMap)实现非堆,Java 应用通常可以宣告完全主宰自己的命运(不再受 STW GC“难堪的暂停”事件的摆布),也就是能够满足性能的 SLA 期待和条款。

这是一个具备实用性的可选方案,在基于 Java 的高频率交易系统上已经得到了应用。

对于 Java 来说,如果想对高性能计算社区保持持续的吸引力,这也是一个完全必要的方案。

堆的优势

  1. 以熟悉的方式,很自然地编写 Java 代码。所有有经验的 Java 开发人员都能编写这样的代码。
  2. 安全,不必担心内存访问问题。
  3. 自动化的 GC 服务——没有必要自己去管理 malloc()/free() 操作。
  4. 对 Java 锁 API 和 JMM 的集成都完全不必再担心。
  5. 没有序列化 / 复制的数据要添加到结构体之中。

非堆的优势

  1. 能够将“Stop The World” GC 事件控制到你认为合适的级别。
  2. 在扩展性方面(当使用堆所造成的影响足够高的时候)要强于堆上的结构。
  3. 可以用做原生的 IPC 传输手段(不会有 java.net.Socket 的 IP 回路)。
  4. 在分配方法上的考虑因素:
  • 使用 NIO DirectByteBuffer,实现到 /dev/shm (tmpfs) 的映射?
  • 或者直接使用 sun.misc.Unsafe.malloc()?

HashMap 的现状……(通过使用非堆)这个“老家伙”能够解决什么新问题?

OpenHFT HugeCollections (SHM) 简介

“非堆”到底是什么?

在下面的图中,阐述了两个 JavaVM 进程(PID1 和 PID2),它们试图使用 SharedHashMap(SHM)作为进程间通信(inter-process communication,IPC)的设施。图中底部的水平轴展现了完整的 SHM OS 位置分布域。当进行操作的时候,OpenHFT 对象 _ 必须 _ 要位于 OS 物理内存的用户地址空间或者内核地址空间。继续深入研究一下,我们知道开始的时候,它们必须是“On-Process”的位置。按照 Linux OS 的视角来看,JVM 是一个 a.out (通过调用 gcc 来生成)。当这个 a.out 运行时,从 Linux 进程内部来看,这个运行的 a.out 有一个 PID。 PID 的 a.out(在运行时)有一个大家所熟知的内部构造, 包含了三个段(segment):

  1. 文本段(Text,低地址……代码执行的地方)
  2. 数据(Data,通过 sbrk(2) 实现从低地址到高地址的增长)
  3. 栈(从高地址向低地址增长)

这是在 OS 的角度来看 PID。PID 是一个正在执行的 JVM,这个 JVM 对其操作对象的可能位置分布有一个自己的视角。

按照 JVM 的视图,操作对象可能位于 On-PID-on-heap(正常的 Java)或者 On-PID-off-heap(通过 Unsafe 或 NIO 的 bridge 桥接到 Linux mmap(2) )之中。不管是 On-PID-on-heap 还是 On-PID-off-heap,所有的操作对象依然都还是在用户地址空间中执行。在 C/C++ 中,有 API(OS 系统调用)能够允许 C++ 操作对象位于 Off-PID-off-heap 上。这些操作对象存在于核心地址空间上。

下面 6 个编号的段落对上图进行了描述。

#1. 为了更好地阐述上图中的流程,假设 PID 1 定义了一个BondVOInterface,它是符合JavaBean 约定的。我们想要阐述(按照上图中的数字顺序)如何操作 Map<String,BondVOInterface>,这种方式会着重强调非堆的优势。

来自于 GitHub:

复制代码
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(在上图的步骤 1 中,使用接口)调用了一个 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);</string>,>

现在,会发生一些堆 →非堆的魔法。请仔细观察……在本文所带给您的整个旅程中,将要分享给您的“魔法”是旅程中“最美的风景”:

#2.在运行时,每个进程调用上面的 OpenHFT 工厂方法时,会生成并编译一个BondVOInterface£native 内部实现,它会完全负责必要的字节位置算法(byte addressing arithmetic),从而实现充分完整的非堆 abstractAccess() / abstractMutate() 操作符集合(通过该接口的 getXX()/setXX() 方法,这些方法符合 Java Bean 的方法签名约定)。它们所造成的效果就是 OpenHFT 在运行时会使用你的接口并将其编译为实现类,这个实现类会作为具体非堆功能的桥梁。数组(array)也是类似的,会使用基于索引的 getter 和 setter。数组的接口也会像外层接口一样。数组的 setter 和 getter 方法签名格式为setXxxxAt(int index, Type t);getXxxxAt(int index); (注意,‘At’后缀同时适用于数组的 getter/setter 签名)。

这是都是在运行时为你生成的,借助于进程中的 OpenHFT JIT 编译器。你所要做的就是提供接口。非常酷,对吧?

#3. PID 1 然后调用 OpenHFT 的 API shm.put(K, V);,从而按照 Key (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
);</string>,>

以这样的方式,跨越了 OpenHFT 构造的连接桥,获得了 _ 完全相同的 _ 非堆 OpenHFT SHM 引用。当然,这假设 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 使用的get 方法签名:V2 = acquireUsing(K,V);,它的区别在于,如果K 不是一个Entry的话,你所得到的并不是 NULL,而是会 _ 返回一个引用 _,这个引用指向了一个新创建的 _ 非 NULL_ 的V2占位符。这个引用能够让 PID 2 在 _ 合适的时候 _ 操作 SHM 的非堆V2 Entry

注意:当 PID 2 调用V = shm.get(K); 时,它会返回一个新的非堆引用。这会产生一些垃圾,但是在丢弃它之前,你能够一直持有对这个数据的引用。然而,当 PID 2 调用V2 = shm.getUsing(K, V); 或者 **V2 = shm.acquireUsing(K, V);**的时候, 非堆引用转移到了新 key 的位置上,这个操作跟 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. 非堆记录是一个引用,它包装了 Bytes 以用来进行非堆的操作,同时还包装了一个偏移量(offset)。通过对这两者进行变更,内存中的任何区域都能够访问到,就如同它是你所选择的接口那样。当 PID 2 操作‘shm’引用时,它要设置正确的 Bytes 和偏移量,这会通过读取存储在 /dev/shm 文件中的 hash map 来进行计算。在 getUsing() 返回后,对于偏移量的计算就会非常简单并且是内联执行的,也就是说,一旦代码被 JIT 之后,get() 和 set() 方法就会变为简单的机器码指令,以实现对这些域的访问。只有你所访问的域会被读取或写入,真正的零复制(ZERO-COPY)!太漂亮了!

复制代码
//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 堆 ←→ 非堆转换是如何实现的。

事实上,OpenHFT SHM 实现在步骤#6 中,在运行时会拦截 **V2 = shm.getUsing(K, V);** 调用的第二个参数的内容。实质上,SHM 实现是这样查询的

复制代码
(
( arg2 instanceof Byteable ) ?
ZERO_COPY<b> :</b>
COPY
)

并且它会以零复制的方式执行(通过引用更新),而不是完全复制(COPY)的方式来执行(通过 Externalizable)。

非堆引用功能的核心接口就是 Byteable,它使得引用能够被(重新)设置。

复制代码
public interface Byteable {
void bytes(Bytes bytes, long offset);
}

如果你要实现自己的支持这个方法的类,那么你尽可以实现或生成自己的 Byteable 类。

现在,就像我们所提到的那样,你可能依然会想“所有的这一切发生地太 _ 神奇 _ 了”。这里其实会发生很多的事情以实现这个神奇的功能,并且所有事情的发生都是与外部无关的,也就是 _ 发生在正在执行的应用进程之内!_ 如果使用运行时编译器(Run-Time-Compiler)的话,它会将我们的BondVOInterface作为输入,OpenHFT 内部会确定接口的源代码并对源码进行编译(同样是在进程内),将其编译为 OpenHFT 所能理解的实现类。如果你不想让这个类在运行时生成的话,那么可以预先生成这个类,并且在构建阶段进行编译。OpenHFT 内部会将这个新生成的实现类加载到运行上下文之中。此时,运行时会物理执行所生成的BondVOInterface£native内部类的方法,这些方法也是生成的,以实现零复制操作符的功能,转换为非堆Bytes[]的记录。这项功能是零复制的,只要你在一个线程内执行了线程安全的操作,它就会对另外的线程可见,即便这个另外的线程可能位于其他的进程之中。.

现在,你已经了解了 OpenHFT SHM 魔法的本质:Java 如今有了 _ 真正 _ 零复制的 IPC。

嘛哩嘛哩哄!

性能结果:CHM 与 SHM

Linux 13.10,i7-3970X CPU @ 3.50GHz,hex core, 32 GB 内存。

SharedHashMap -verbose:gc -Xmx****64m

And there you have the essence of the OpenHFT SHM magic: Java now has true ZERO-COPY IPC.

Abra Cadabra!

PERFORMANCE RESULTS: CHM vs.SHM

On Linux 13.10, i7-3970X CPU @ 3.50GHz, hex core, 32 GB of memory.

SharedHashMap -verbose:gc -Xmx****64m

ConcurrentHashMap -verbose:gc -Xmx****30g

当然,CHM 比 SHM 慢 438% 的主要原因在于 CHM 会经历长达 21.8 秒的 STW GC。但是从 SLA 角度来看,问题产生的原因(对于这个诱因没有补救措施)并不重要。从 SLA 角度来看,事实上 CHM 就是要慢 438%。从 SLA 的角度来看,在这个测试中,CHM 的性能慢得 _ 让人无法接受 _。

适配 JSR-107:将 SHM 作为(100% 协作的)非堆的 JCACHE 操作对象

在 2014 年的第二季度,Java Community Process 发布了 JSR-107 EG 的 Release 版本 JCACHE——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% 完整彻底地采用 SHM 作为实现。

-Cache 必须要提供而基本 HashMap 所没有提供的都有什么呢?

  • 清除(Eviction)、过期(Expiration)
  • 弱引用(WeakRef)、强引用(StrongRef)(其实这与非堆 Cache 实现无关)
  • 本地化角色(Locality Role)(如 Hibernate L2)
  • EntryProcessors
  • ACID 事务
  • 事件监听(Event Listener)
  • “Read Through”操作(同步 / 异步)
  • “Write Behind”操作(同步 / 异步)
  • JGRID 相关的功能( JSR-347
  • JPA 相关的功能

- OpenHFT+Infinispan 的“婚礼日” 计划 (JCACHE 的庆典)

下图展现了社区驱动的OpenHFT 编程人员在采用/ 贡献OpenHFT 非堆SHM 作为完整的JSR-107 协作JCACHE 操作对象时所需要的很少范围的开发工作(社区驱动的开源JCACHE 提供商= RedHat Infinispan )。

(点击图片放大)

结论:非堆HashMap 的现在和未来……“直到奶牛不干了,回家的那一天”

在这个旅程接近“最后一站”的时候,我们用一个类比的故事来向你告别,并解答你所关心的问题。

社区驱动的开源非堆HashMap 提供商以及JCACHE 提供厂商(包括商业的和开源的)之间的业务关系可以是和谐且互相协作的。在为终端用户提供更为愉悦的非堆体验方面,它们中的每一个都扮演着重要的角色。非堆HashMap 提供者可以交付核心的非堆HashMap(作为JCACHE 的)操作对象。JCACHE 厂商(包括商业的和开源的)可以采纳这个操作对象到他们的产品之中,然后提供核心的JCACHE(和基础设施)。

这种关系就类似于奶牛(也可以说是乳业农场主,核心_ 操作对象_ 即牛奶的生产者)与奶制品公司(牛奶_ 操作_ 的生产者,操作集合={巴氏杀菌、脱脂、1%、2%、各占一半等等})之间的关系。这两个组合(奶牛和乳业公司)结合起来能够生产出终端用户更为喜欢的产品,这要优于两者(奶牛和乳业公司)不进行合作的场景。终端用户对这两者都需要。

但是要给终端用户一个“购买者注意!”的提示:

如果有人遇到商业厂商有志于交付闭源的HashMap/Cache 解决方案,并且宣称他们闭源的非堆操作对象要“优于”开源社区驱动的方式,那么,只需要记住这一点:

_ 乳业公司并不制造牛奶。_只有奶牛才会制造牛奶。

奶牛会一直生产牛奶,24/7,并且完全没有其他的干扰。乳业公司能够让牛奶更加美味(各占一半、2%、1%、脱脂)……所以,他们 _ 确实 _ 有机会扮演重要的角色……但是他们并不生产牛奶。现在,开源的“奶牛”正在生产非堆 HashMap 这种“牛奶”。如果商业解决方案厂商认为他们制造的 _ 那种 _ 牛奶更加美味,那么尽可以去做,这样的努力是所有人都欢迎的。但是,这些供应商不应该宣称他们自己的牛奶是“更好”的牛奶。只有奶牛才会生产最好的牛奶。

总之,考虑到 Java 为高性能计算社区所带来的改变是很令人兴奋的。事情确实有了很多的变化,而且所有的变化都是往更好的方向发展。

并发包之中,从不断改善的现代GC 方案之中,从非阻塞I/O 功能之中,从 Sockets Direct Protocol 的原生 RDMA, JVM intrinsics 之中,……,再到原生的 Caching 、OpenHFT 的 SHM 作为原生的IPC 通信方式以及该OpenJDK 非堆JEP 所呼吁的机器级别HTM 辅助功能(machine level HTM-assist feature),有一件事是确定的:OpenJDK 平台社区在提升性能方面_ 确实_ 有着很高的优先级。

来看一下HashMap 这个可爱的_ 老_ 家伙_ 现在_ 能够做些什么吧!借助于OpenJDK、OpenHFT 和Linux,非堆HashMap 在“较低的位置”(也就是原生OS)有了新朋友。

现在不会受到STW GC 的任何干扰了,HashMap 作为重要的HPC 数据结构操作对象,获得了重生。HashMap,保持永远_ 年青_ 吧!

感谢你们陪伴我们的旅程,希望你喜欢这个经历。下次再见。

关于作者

Peter K. Lawrey是 Higher Frequency Trading Ltd. 的首席咨询顾问,以及 OpenHFT 项目的领导者。他是 Java Community Process 的成员,目前在活跃的 JCP 专家组参与定义分布式数据网格(Distributed Data Grids,JSR-347)的 Java 标准 API。他是具有 500 个成员的 Performance Java Users’ Group 的创建者(目前该 Google 群组已经有 1500 多位成员——译者注),以及技术博客“Vanilla Java”(230 篇文章,3 百万的站点点击)的作者。Peter 毕业于 Melbourne University,拿到了两个学位,分别是计算机科学和电气工程。Peter 在 StackOverflow 的问答响应排名中位居前三。最近的五年来,他致力于开发、支持以及提供咨询,并在欧洲和美国东部为高频率的交易系统提供培训。

Ben D. Cotton III 是 J.P.Morgan Chase & Co. 的 IT 咨询顾问,目前在 UHPC Linux 超级计算机上使用 Java 数据网格技术,以判断和计算实时的流动资产风险。Ben 毕业于 Rutgers University,获得了计算机科学的学位。在职业生涯的最初 11 年中,他在 AT&T 贝尔实验室工作,编写 ellMac32-ASM/C/C++ 代码来支持无数的专用通信、网络分析以及提供协议,最近的 14 年中他在编写 Java 代码,实现低延迟、高吞吐、事务性、固定收益、金融衍生物、电子交易、清算、定价以及风险控制的系统。和 Peter 一样,Ben 也是 JSR-347 EG 的成员。

查看英文原文: OpenJDK and HashMap …. Safely Teaching an Old Dog New (Off-Heap!) Tricks

2015-05-25 23:474974

评论

发布
暂无评论
发现更多内容

高可用的系统架构

莫莫大人

极客大学架构师训练营

Week11作业1

熊威

架构师训练营 - 第 11 周作业

Jam

Flink算子状态-9

小知识点

scala 大数据 flink

Newbe.Claptrap 框架入门,第四步 —— 利用 Minion,商品下单

newbe36524

云计算 微服务 dock .net core ASP.NET Core

极客大学架构师训练营 0 期 week 11 学习笔记

chun1123

安全 高可用系统的架构

极客大学架构师训练营 0 期 week 11 作业

chun1123

高可用 密码校验

java安全编码指南之:拒绝Denial of Service

程序那些事

Java 安全编码指南 java安全编码 DOS攻击 zip炸弹

etcd的高可用

李小匪

系统不可用的原因和解决方案

极客李

用户密码验证函数

任小龙

【华为云数据库技术大公开】机房失火后,还能拯救你的数据吗?

华为云开发者联盟

数据库 机房 华为云 数据存储 云数据库

架构师训练营 第十一周 总结

CR

架构师训练营第 0 期第 11 周作业

无名氏

系统稳定高可用的方案以及用户密码验证函数

Acker飏

极客时间架构师训练营 - week11 - 作业 2

jjn0703

极客大学架构师训练营

Week 11命题作业

Jeremy

架构师训练营第十一周作业

子豪sirius

架构师训练营 Week 11 作业

Wancho

【高并发】高并发秒杀系统架构解密,不是所有的秒杀都是秒杀!

冰河

高并发 分布式限流 秒杀系统 异步削峰 签约计划第二季

第十一周命题作业

菲尼克斯

漫画解读:唐僧师徒如何帮助大唐官网打造CDN+OSS完美架构?

阿里云Edge Plus

Week11总结

熊威

架构师训练营 Week 11 总结

Wancho

架构师训练营 第十一周 作业

CR

第十一周学习总结

菲尼克斯

Apache Pulsar 社区周报:08-15 ~ 08-21

Apache Pulsar

云原生 Apache Pulsar 消息系统 消息中间件

安全架构和高可用系统的架构

周冬辉

高可用系统的架构

云上度假村木莲庄酒店助你远离城市的喧嚣

InfoQ_967a83c6d0d7

Week 11 作业

鱼_XueTr

用户密码验证函数

周冬辉

加密

OpenJDK与HashMap……放心地教这个老家伙一些新(非堆!)技巧_Java_Ben Cotton_InfoQ精选文章