我的上一篇文章是在几周前写的,在收到一些有效的反馈后,我想澄清几点,作为本文的序言。
“ 使用零垃圾创建数百万个对象 ”的主要收获应该是,使用Chronicle,在编写Java程序时,您不会“局限于”使用jvm分配的堆内存。 也许这篇文章更恰当地标题为“使用零堆创建数百万个对象”。 我想指出的另一点是,当您没有堆内存时,不会引起GC活动。
我使用术语“垃圾”来描述分配在堆上的对象的事实使人感到困惑。 尽管分配的对象引起GC活动,但实际上它们不是垃圾。
我设计了一个示例来说明一个,一个是ChronicleMap不使用堆内存,而ConcurrentHashMap则使用第二个;当您使用堆内存时,您不能忽略GC。 至少您需要仔细调整系统,以确保您不会因为长时间的GC暂停而遭受痛苦。 这并不意味着从堆外分配没有任何问题(请参阅本文的结尾),也并不意味着您无法通过堆上解决方案进行优化以消除GC。 摆脱困境绝不是解决所有Java性能问题的灵丹妙药,但是对于非常具体的解决方案,它可以提供有趣的机会,我将在本文中讨论其中的一些机会。
有时可能需要在JVM之间共享数据。
现在让我们简化一下,假设您有两个JVM在同一台计算机上运行,其中一个或两个都希望看到彼此的更新。 每个Java程序都有一个ConcurrentHashMap
,它将对其进行更新,这些更新将被存储并在以后可供使用。 但是,程序如何将另一个Java程序应用的更新获取到其映射?
从根本上讲,不能在JVM之间直接共享HashMap
和ConcurrentHashMap
类的JDK堆集合。 这是因为堆内存由分配它的JVM包含。 因此,当JVM退出时,内存被释放并且数据不再可用,因此没有在JVM的生命周期之外持久保留内存的隐式方法。 因此,您需要找到其他机制来在JVM之间共享数据。 通常,您可以将数据库用作外部共享存储和消息传递服务,以将数据更新发送给其他进程,以通知他们某些数据已被更新。
这导致以下体系结构:
这种体系结构的问题是,使用会丢失HashMap的内存速度,特别是如果写入数据库的速度不是那么快,并且您希望在通过消息传递服务发送消息之前坚持写操作。 同样,许多解决方案都将涉及TCP调用,这可能再次成为延迟的源头。
当然,与使用诸如将日志记录到磁盘的机制(例如,使用ChronicleQueue之类的产品)写入成熟的数据库相比,存在持久化数据的方法要快得多。 但是,如果您确实使用日志,则仍然必须建立所有逻辑以在重新启动时重新创建Map
数据结构,更不用说必须在另一个JVM上保持Map类型结构的最新状态。
(您可能根本想保留数据的原因是,这样您应该能够在重新启动时进行恢复,而不必从源中重播所有数据)。 除了此体系结构引入的延迟之外,还必须处理数据库和消息传递服务的额外代码和配置。
即使接受这种功能都可以封装在框架中,如果您的内存Map
实际上在JVM外部可见,那不是很好。 Map
应该能够隐式持久化数据,以便其数据独立于JVM的生存时间可用。 它应该允许使用与使用堆上映射相同的“内存”速度进行访问。
这就是ChronicleMap
用处ChronicleMap
是java.util.ConcurrentMap
的实现,但重要的是,它使用了堆外内存 ,该内存在JVM外部对于计算机上运行的任何其他进程都是可见的。 (有关堆上内存与堆外内存的讨论,请参见此处 )。
每个JVM将创建一个指向相同的内存映射文件的ChronicleMap
。 当一个进程写入其ChronicleMap
,另一个进程可以立即(约40纳秒)在其ChronicleMap
查看更新。 由于数据存储在JVM外部的内存中,因此JVM退出不会导致任何数据丢失。 数据将保存在内存中(假设不需要分页),并且在JVM重新启动时,它可以非常快速地将其重新映射。 丢失数据的唯一方法是,如果操作系统崩溃而脏页尚未保存到磁盘上,则该操作系统崩溃。 解决方案是Chronicle 支持的使用复制,但超出了本文的范围。
其架构就是这样的:
有关ChronicleMap入门的代码示例,请参阅我的上一篇文章或此处的官方ChronicleMap教程。
在进入ChronicleMap之前,需要考虑很多注意事项和权衡取舍。
- ChronicleMap条目必须可序列化。 对于对性能非常敏感的系统,您需要实现Chronicle提供的称为BytesMarshallable的自定义序列化。 尽管这很容易实现,但堆映射并不是必需的。 (话虽如此,将数据存储到数据库中当然也需要某种序列化方法。)
- 即使使用BytesMarshallable序列化,任何序列化的开销对于某些系统而言也可能很重要。 在这种情况下,可以采用Chronicle支持的零拷贝技术(有关更多详细信息,请参阅我的上一篇博客文章 ),以最大程度地降低序列化的成本。 但是,与使用“普通” Java相比,实现起来有些棘手。 另一方面,在对延迟敏感的程序中,它将具有不创建任何对象的巨大好处,这些对象随后可能需要由GC清理。
- ChronicleMap不会调整大小,因此必须预先调整大小。 如果您不知道预期有多少个项目,可能会出现问题。 但是,应该指出的是,至少在Linux上,过度设置不是一个大问题,因为Linux被动分配内存。
- 编年史依靠操作系统异步刷新到磁盘。 如果要绝对确定数据已实际写入磁盘(而不是仅保存在内存中),则需要复制到另一台计算机。 实际上,任何关键任务系统都应复制到另一台机器上,因此在采用《纪事报》时这可能不是大问题。
- ChronicleMap将受到操作系统内存分页问题的影响。 如果内存已调出并且必须换回,则延迟会引入系统。 因此,即使您能够创建大小远远超过主内存的ChronicleMaps,您也必须意识到根据数据访问模式的不同,可能会发生分页。
翻译自: https://www.javacodegeeks.com/2015/04/chroniclemap-java-architecture-with-off-heap-memory.html