使用 Java* 进行持久内存编程

概述

全新英特尔® 傲腾™ 数据中心级持久内存重新定义了传统架构,提供大容量和全新内存层,且价格实惠。Java 是云端和企业工作负载中流行的语言选择。持久内存在需要持久存储数据的 Java 应用程序中提供了设计灵活性和显著的性能机会。英特尔开发了 low-Level Persistence Library (LLPL),用于灵活且高性能地从 Java 访问持久内存。它具有易于使用的惯用 Java API,适合直接使用或作为更高级别的抽象的基础。

您的学习内容

  • 简要概述从当前的 Java 访问持久内存的不同方法。
  • 深入探讨 LLPL
  • Java 代码示例的演练展示了使用 LLPL 和直接内存编程来存储持久数据的强大功能。
  • 要对 PMDK 的持久内存概念和功能拥有基本的了解,请访问英特尔® 开发人员专区中的持久内存

文字稿

欢迎大家。感谢您抽空参加本次主题为“使用持久内存的Java编程”网络讲座。我是Olasoji Denloye。我是英特尔开发者软件工程部的软件工程师,我将担任今天的主持。
持久内存是兼具内存和存储器属性的一种新的内存技术。它像动态随机存取内存一样快速并具有存储器的高容量,可用于使现今的数据中心提速。Java仍然是广泛用于数据中心应用程序的语言例如数据库。今天的网络讲座将介绍通过Java使用持久内存的不同方法,以及深入了解英特尔开发的开源库,它为使用Java提供了对持久内存的高性能存取。
我们今天的嘉宾是我的同事Steve Dohrmann。Steve是英特尔的高级软件工程师。他在过去超过20年的时间曾参与多个不同的Java项目,并且热衷于Java及其特性。他的Java开发工作包括Java媒体框架、密码框架、Java嵌入式引导编程、以及目前的实现Java对持久内存的使用。 
在我们开始之前,我想提醒大家,本次网络讲座将提供点播,在直播结束后,您可以通过同一链接进行访问。同时,如果您在讲座期间有任何问题,您可以使用屏幕上的“提问”(Ask a Question)提交您的问题。我们将在讲座的最后回答您的问题。
现在把时间交给Steve。Steve?
好的,谢谢,Soji。大家好。感谢你们的到来。我将直接进入概要介绍。这是一个清单,使用Java进行持久内存编程的五个可行路径。前三个是英特尔开发的开源库。第一个是底层持续库,LLPL。我们在本次将主要探讨它的情况。第二个是称为pmemkv的键值存储库。它属于持久内存开发包的一部分,该套件中的其中一个库,它提供了对包括Java在内的多种语言的绑定。因此,这是一个易于使用的库。清单上的第三个是我们为开发的另一个库,称为Java持续集合。这是一个高级库,提供持续集合类和其他持续类,以及自动内存管理等。这个库目前处于实验阶段,但它和另两个库一样,在GitHub上开放。然后还有对开放JDK的两项提升,均在目前的JDK14发布中可用。第一个是持续MappedByteBuffer,这只是对MappedByteBuffer API作出的细微更改,使其更适合具有得到持久内存支持的MappedByteBuffer。然后该清单中第二个称为异质内存访问API。这是JDK14提供的新安装包中的一部分,Java.Foreign,它使您可以无需编写任何C代码、无需编写JNI代码,即可进行异质库的调用,因此使一个纯Java异质功能接口,然后其基层就是这一内存访问API,它让您可以编写Java代码进行堆外编程,包括堆外持久内存编程这。
里是这五种路径的高层次比较表。我在这里不会进行深入的说明。我们今天将主要介绍左边的LLPL。我只是强调一下,这五种路径的两个最大不同之处可能是它们要求运行的最低JDK版本。您看到前三个是JDK 8以上,而新的,当然要求JDK14,而第二个主要不同是编程层次。它们提供了高级和底层编程的不同选择。在后面的LLPL和内存访问API,提供的是底层编程,如指针和创建可连接结构的指针的能力,而中间的三种,相对处于高层的位置,这就是五种路径的另一个不同之处。
那么,让我们深入了解一下LLPL库吧。如前面所介绍的,它需要运行在JDK 8或更新的版本,而其目前的版本为1.0,于去年年底发布,我们现在正努力开发版本1.1。它是PMDK的一个组件,事实上,它使用了PMDK套件中的量个库Pmem和pmemobj。您可以使用Maven或Make来编译LLPL,我们目前正着手使LLPL可在Maven Central上使用。当我们尝试编写LLPL的目标时,给出了这三个。首要目标是提供通过Java对持久内存的非常高性能的访问,并且这是底层访问,为您带来了使用上的高度灵活性。特别是您可以将其用作作为一个库的直接编程API,或者您可以把LLPL用作您要创建的较高级抽象的基础,尽管其底层本质,我们的目标是使API实际成为惯用的Java。因此,我们将看看那些代码并希望您会了解到它们的使用相当容易,尽管是属于底层的API。
因此,在LLPL中有三个主要元素:堆,内存块和事务。现在,堆只是一个持久内存池及其分配器。您可以按需要创建几乎任意大小的大量的堆。万亿字节大小的堆是可行的。当然,这些堆是持续的,因此我们必须能够在重启后修改它们而您可以重新开放这些堆。这我们将在代码中看到。有别于Java,对这种持久内存的内存管理是手动的。其中将使用分配和空闲配对方法,堆API本身是线程安全的。因此,您分配的是一块内存,您将得到一个MemoryBlock对象,这是对已分配块的存取器API。
这一MemoryBlock API由底层设值方法和取值方法组成。您将使用从MemoryBlock起始位置开始的基于零的偏移来说明您需要读取或写入多少个字节,同时您可以长期使用这些已分配的块,它们可用于写入其他内存款,因此您可以把块连接起来并创建基于引用的数据结构。这些引用以被称为句柄的Java long值的形式出现,它们在堆的生命周期将保持稳定。MemoryBlock API本身不会进行加锁或者其他线程封闭。开发者有责任和自由创建他们认为安全和永久的并发方案。
第三三个组件是事务,这让您可以应用故障安全权限、创建故障安全权限、以及把这些权限组合成原子聚合,它将一起执行全部权限并表现得像根本没有执行过一样。一个单线程参与到一个事务中,您可以以任何需要的特定方式嵌套它们。但是,它们将作为一个扁平事务运作,其中最外层的事务体将为其创建容器,所有内体将一起提交或中止。您将看到在代码中,实际上得到了非常易于使用的一个交易的实现,我们精心地将这一事务API和Java的异常处理、匿名函数等惯用特性整合了在一起。我们为事务体使用了匿名函数。
这是一个概要方块图,显示了在左边的动态随机存取存储器中的Java堆,以及右手边在持久内存中的LLPL堆,我们使用这一LLPL库所做的工作是创建控制对象,它们是常规的Java对象并位于左边的Java堆中,它们是非常薄的对象。它们基本上都具有一个指向持久内存中一个位置的指针,而Java对象内的该API则控制着它所指向的任何内容。因此,我们在左边有一些堆对象,我们在右边有指向内存块的一些MemoryBlock对象。您只需要拥有这些常规Java对象中的一个,这些现有的控制对象,如果您主动地访问持久内存的特定片段,正如您在这里看到的一样,您可以拥有由相同Java进程访问的多个堆。堆仍然是分离的,特别是,内存块自身属于堆特定,但是如果您需要的话,您可以在LLPL的堆之间复制内存。
当Java进程退出时,当然,Java堆和所有那些控制对象将消失,但所有持久内存状态仍将保留并且您创建的内存块之间的所有关系仍将保留。例如,在右边的LLPL堆1中我们展示了把MemoryBlock A连接至MemoryBlock B的箭头。这基本上是把那些句柄值中的一个写入到MemoryBlock A而它指向着MemoryBlock B,而即使在Java进程退出后所有这些仍将持续。
这里是显示出MemoryBlock API底层本质的一个快照。我们为Java整形数值类型提供了设值方法和取值方法,然后有批量操作可在持久内存块之间或者在持久内存块和Java字节数组之间进行内存拷贝复制。对于MemoryBlock还有一些其他的支持方法,其中一个可以在需要对MemoryBlock解除分配时释放它,还有一个取值方法可以取得句柄,这本质上是对该MemoryBlock的数字名称。我们将很快看到它们在代码中的使用。
那么,因为我们现在正尝试依靠此内存,可能是长期地,使用其持续性,我们希望能够实现数据完整性和适合我们应用程序的一致性方案。这将因不同的应用程序而有所不同,但特别地,作为应用程序开发者,我们希望能够说清楚关于像常规进程退出等预期事件或者向崩溃或电源故障等意外事件发生之后堆的可用性的一些情况。为了支持建立这些类型的策略,我们将介绍并在代码中展示两类型的权限,您可以修改持久内存,持久权限或事务性权限,如果您使用持久权限,策略的基础将是如果那些权限没有被中断,那么当您重新开放您的堆时,一切都将原封不动并可以使用,但有时您需要更强的能力以应对意外事件,例如崩溃,那么您可以使用事务性权限,这将给予您更强的保证。他们说如果您使用事务性权限,那么即使您遇到了崩溃或者电源故障,您所使用的权限的一致性和权限的完整性都将存在,即使在那些事件发生之后,而我们也在代码中说明了这一点,它将使您保持健全和可用的堆,即使面对如崩溃或者电源故障等事件时也一样。LLPL提供了这两种类型的权限和其他一些工具,使得您可以建立这样的数据完整性和一致性策略,无论是简单的还是高度自定义的都可以。
那么,我们刚才讨论的是持久写入、事务性写入。连同作为持久内存编程核心的这两种类型写入的,还有出现在表层的的一些错误,我们希望能够采取步骤减少或排除那些错误。因此,我将在排行榜上加入成为易失性写入的第三种写入,只是为了表明它是另两种类型的一个组件。当我们对动态随机存取存储器进行常规写入时,假设我们正在设置一个Java对象字段,我们写入数据,它将进入CPU缓存中,它将变得对系统上的其他线程可见,我们的逻辑将得到该行为的服务,但是不必一直进入到动态存取存储器的内存模块中。它的基础是,当在缓存中需要空间时,将把它清除至模块本身。持久内存正是以这种方式运作。当我们进行写入时,它将开始一次本质上是易失性的写入而我们写入,数据将进入CPU缓存,但它同样不必清除至持久内存DIMM,因此我们必须这样做。有时,要进行持久写入,我们必须清除它,因此我们知道必须一直去到持续介质。
对于事务性写入,我们将进行那两个步骤,但我们在写入数据之前先进行第一个步骤,那是要告知事务我们即将修改什么数据。特别地,我们将告知其我们将修改的字节范围,而它将会做的就是创建我们即将更改至撤销缓冲区内的数据的一个备份,假如一个事务被中断,它可以使用该备份将数据恢复至其原状态,它的事务前状态,为我们提供了一致性的保证。
使用那两种类型的写入,特别是,加粗显示的那些步骤,会带来两种可能的编程错误。一种是您忘记了清除持久写入,而第二种是您忘记了在实际覆盖数据之前添加范围到事务中。因此,要对您的代码进行彻底测试就非常困难了。它们在这种意义上看有一点像竞争条件。那么,LLPL所做的以帮助改正这些错误的工作是它为您提供了灵活性,您可能想要创建一个任意的一致性方案,因此我们在代码中显示的通用堆将为您提供这样的灵活性,但您要自己进行清除并且您要自己进行“添加至事务范围”,所以您可能会忘记这样做,那么那些程序错误就会出现。如果那时不可接受的,那么您可以为持久写入使用对应MemoryBlock中的PersistentHeap,这种对保证您进行的写入会被清除。因此,如果您的代码可以编译,您会知道它没有忘记进行刷新,因为它已经为您这样做了。对于具有更强一致性保证的事务性写入,您可以使用TransactionalHeap,而它将给予您保证,如果您的代码编译成功,您所有的事务性写入将在数据被覆盖之前适当地添加至事务,所以您不可能会忘记这样做。使用这些PersistentHeap和TransactionalHeap,您会失去一些灵活性,并可能会损失一些性能,但您可以在这三者之间选择还有还有使其匹配您希望实现的数据一致性和完整性策略。
好的,在我们继续浏览一些代码之前,我想要介绍一个激励的例子。这是一次我们和称为Cassandra数据库的开源社区之间的一次协作。我们为Cassandra实现了一个持久内存存储引擎,其设计显示在这张幻灯片上。该设计实际上来自一名Cassandra PMC成员您可以看到一个成为存储引擎的箱子。那是一个接口的实现,一个可插拔的存储引擎接口,它是为Cassandra而开发,而Cassandra的前端显示在上方的两个箱子中。那根本没有更改。这将插入到数据库的后端存储中。这是一个有趣的设计。这在数据库中常用于共享数据以提供高并发访问,而那就是它的情况。您拥有队列,接收任何它们负责的共享数据的工作。这是一个存储引擎中的一体化节点。事实上,这只是数据库中的一个表,然后每一个队列背后的单线程拥有共享的数据且它将会进行对下面持续数据结构的读取和写入,这显示在三角形中,每一个三角形就是一个树型数据结构和自适应基数树的实现。您可以在下方找到论文链接。我们实现了该论文中的自适应基数树。事实上,我们使用LLPL实现了所有持久内存数据结构。
那么,这做得不错,在下一张幻灯片中,我将展示一些结构并简单说明加速来自哪里。在灰色栏上,我们显示了运行着四个非常快速的NVMe SSD基线Cassandra,蓝色的是持久内存存储引擎。我们看到在整个板上读取和写入、以及混合工作负载的非常良好的加速效果,巨大的加速实际上来自两个主要方面。例如,在读取的例子中,当我们从Cassandra的基线中基于块、基于磁盘的存储转变为基于这些内存中的数据结构的持久内存存储引擎时,进行读取操作所需要的指令数量大幅减少。事实上,它减少了三倍,因此我们只需要三分之一的工作来进行一次查询。
得到大幅改善的第二个度量元素是并发性。在使用这一直接内存访问和为数据结构设计专门的并发性方案,即共享方案中,我们能够应用Cassandra的相同实例中的更多线程以执行读取,同时保持在我们的数据库服务等级协议(SLA)中。因此,当使用基于内存的数据结构时,我们实际上实现了2.3倍的更加的CPU利用率,而这两个数的乘积,三乘以2.3,得到7,并可以高达8,其他方面也能带来改进。因此,我们感到非常满意。通过树中节点之间的指针,使用持久内存和基于内存的数据结构,以实现在经高度优化的数据库中的巨大加速,这只是一种蛮力的方法。Cassandra已经出现了有很长的一段时间,它已经得到高度的优化。
好的,我们将浏览大约四段、或者可能是五段这样的代码示例,我要指出在GitHub上的LLPL仓库中,有一个带有更多完整例子的例子目录,包括,例如使用LLPL对自适应基数树的实现。我们目前所谈论的所有内容,以及我们将要介绍的所有代码,在本次演示的最后一张幻灯片中都提供了相关链接。
那么,我现在将开始转移到代码上来,我们应该会看到称为“开始”(Getting Started)的第一个例子。这将在代码中展示我们在前几张幻灯片中讨论的基础内容,事实上,如果我们看到前面的,例如,36行,我们将实际看到,本质上那里的所有组件都属于持久内存编程。好的,让我们一起浏览一下。
在第10行,我们看到创建了一个字符串。这是一个路径。因此,我们将要创建一个LLPL堆,我们通过一个路径来命名堆。该路径的第一个部分,Pmem部分,那是基地址,即持久内存在持久内存的特殊类型文件系统变得可用的地址。它实际上是一个常规的文件系统,但它具有一个成为DAX——直接访问的选项,这得到了近期的内核的支持,它让您可以使用方便的路径指向持久内存池,并通过和这关联的堆使用实用程序,但在您建立它之后,我们将看到在我们进行读取和写入时,实际上根本没有文件系统的参与。当读取和写入发生时,没有文件系统缓存,没有内核的参与。它本质上只是代码中的移动指令。这就是速度的来源。好的,那么让我们浏览一下,其余部分。
嘿Steve。我们看不到您分享的东西。抱歉打断一下。
啊,对不起。我没有切换。谢谢。我要切换……对不起。
好的,这应该可以了。
我们继续。谢谢Soji。是的,对不起,第10行是我这里说到的字符串路径。顺便说一下,我们在第三、四、五行导入了三个类:Heap、MemoryBlock和Transaction,我们曾介绍过的三个类。 
那么我们将要看看如果路径已经存在于这一文件系统中,如果确实这样,那么我们将开放堆,但对于这首次运行,它尚不存在,所以在第12行,我们将调用第二部分的条件,我们见个调用Heap上成为createHeap的静态方法。它要求两个参数,我们介绍的路径还有一个Java long值,这是对的字节数量,且我们尚未初始化,因此在第14行,我们将进入这第一个块,并且我们要做的第一件事是从这个对分配MemoryBlock。我们在第16行中通过调用我们堆上名为allocateMemoryBlock的实例方法并在第一个函数中为其提供256的大小从而实现这一点。它同时表示我们将要在这里进行线程安全的原子分配。我们不需要一个事务。因此,那是一个Boolean,您可以表示是否需要事务性分配。
现在,我们拥有一个内存块和一个访问报告,在我们继续之前我们要做的是我们将进行和持久内存特定相关的事情,我们将要指定一类书签。我们将调用这个块我们启动数据结构,我们的路由数据结构,我们将为其指定一个书签因此当我们重启我们的应用程序时我们可以回到这一路由数据结构,为此,我们取得该块的句柄。我曾谈到Java long,那是一个块的数字名称。我们在第17行中通过block.handle设置函数的调用来实现它,并且我们穿入八字节的Java long来调用heap.setRoot。那么,我们稍后将看看如何使用它来取回MemoryBlock。
好的,我们将继续并进行我们所介绍的持久写入,在第20行,我们将调用block.setLong。这将设置八个字节,那里的第一个参数时块内的偏移我们要对其设置long值-或者设置long值在—因此我们将写入12345在偏移零,现在我们必须清除它使其变为持久。因此我们调用block.Flush,而它也需要两个参数。第一个时块内开始清除的偏移第二个是字节计数,即我们希望清除多少个字节,在本例中为八。然后,我们将在下几行给出事务性写入的一个简单例子。
在第25行中,我们将通过调用Transaction类中的一个静态方法create,它需要两个参数。每一个事务均为堆特定,而第二个是事务体,在本例中是一个Java runnable对象,在事务体重,我们将进行写入,但因为这属于事务性,在我们实际更改任何值之前我们将告知事务有关我们将要更改的字节范围。因此,我们使用block.addToTransaction调用,同样,这是我们将要进行写入的偏移。八,我们选择了它,我们将更改八个字节,因此我们告知它多少字节将添加至事务。在我们进行原始数据的备份之后,现在我们将继续并在第27行进行写入,setLong为偏移八,我们将给出一个不同的值,23456。
因此,我们创建了一个堆,分配了一个块,完成了一次持久写入并完成了一次事务性写入。我们将进行更多操作,我们将要做的是创建另一个块,我们将在两个块之间建立连接。因此,在第32行中,我们将分配,我们将再次调用allocateMemoryBlock,例如,也是256字节,并在第33行对新的块写入一个值,在新块内的偏移零,我们将写入值111,现在我们将在本例中清除四个字节。我们写入了一个Int。一个Java Int为四个字节,我们将清除它,获取范围零和计数至四的字节,然后在第35行,我们将通过把新块的句柄写入原始块来连接两个块。那么,让我们在原始块中偏移16的位置写入该句柄,我们将要写入的是—它是一个Java long值,但它不是一个算术值。它是一个句柄值,我们通过调用另一个block.handle来取得。现在我们把两个块连接了起来,我们必须清除该long的写入,因此我们在第36行进行另一次清除调用从偏移16开始的八个字节。
那么,我们现在有两个块,我们把它们连接了在一起,而这就是这一特别名称将会退出的地方。因此,当我们重启我们的应用程序时,我们将回到第11行,该堆路径将已存在,因为我们进行了Create,我们将在第12行中进入条件的第一个部分并只需把路径参数传递至静态的openHeap方法就可以重新开放堆。然后我们的初始Else将向下移动至这里的第38行并开始执行,我们在这里要做的一切就是读取所有那些值并判断这些值是正确的。
在这里我将滚动一下,在第40行,我们在这里通过引导程序返回第一个MemoryBlock。我们回到了堆,因为它使用路径作为名称,而那在我们的代码中是静态命名的。我们没有能力对Java内建其他东西,而那是堆中的这一根位置提供给您的,是这一引导程序的功能。因此,我们将调用heap.getRoot,它为我们提供了指向我们原始块的句柄,现在我们可以在第41行中通过调用堆上的实例方法memoryBlockFromHandle对指向MemoryBlock对象的句柄解除引用。那是一个句柄,您可以取回内存块。
现在我们回到了原来的位置,我们拥有对第一个块的设值方法,我们将在第42和43行对写入到该块的两个long进行读取,同时我们将取回我们的其他块,我们创建的第二个块,这是对memoryBlockFromHandle的另一次调用,但我们将通过从第一个块读取句柄来取回句柄。如果您记得的话,我们在第一个块内偏移16的位置写入该句柄,因此在第44行中,我们通过读取那里的取得句柄,把它传递给堆,现在我们拥有我们创建的第二个块的MemoryBlock对象。我们将继续并读取我们在偏移零写入的值,同时判断我们写入的所有三个值实际上,均为正确。
如果我们用完了这些块,您可能不会在正常情况下这样做,当您首次读取它们后就尽快对它们解除分配,但只要显示它是如何完成的。在第53行,您可以通过调用三个块对象以及我们在使用Allocate调用时使用的相同的Boolean值false来对一个块解除分配,无论您是否希望这释放为事务性块。我们不会在本例中使用它,因为我们对原始块解除了分配,我们设置了其句柄至根,我们将重设根为零,即一个无效的句柄值。
好的,这就是持久内存编程核心操作中的基础内容。
我希望浏览的第二个例子是我将会快速展示控制堆大小的方法。通常在易失性编程环境中我们拥有一个堆,它为内核或者系统拥有,应用程序将对动态随机存取存储器进行竞争,其中易失性内存作为一种资源,但在持续应用程序中,您希望让应用程序拥有内存,因此它是私有的并且可以被获取。因此,我们有多个堆,我们必须随着它们对同样作为共享资源持久内存的竞争对那些堆的大小进行管理。
好的,那么我们在第14行中所看到的是我们在前面例子中所做的。我们正在创建一个固定大小的堆。您为createHeap调用传入一个路径,在本例中是一个文件名,以及一个long参数,这是您希望为这个堆保留的字节数。因此,您将为该特定堆保留100兆字节,任何其他人都无法获取那些字节。有时您不知道一个堆应该要有多大,您必须要能够使其增长。
因此,在第二种情况中,在第18行,我们将创建另一个路径,但这不是一个文件路径。它是一个目录路径,我们将前进到第19行并如果目录不存在将创建一个,我们把该路径传递给相同的调用,createHeap,但这是一个重载版本,不接受long并且它将把这一路径解释为一个目录同时它将使用该目录作为创建可增长堆的基础。它将从最小的堆大小开始,目前为八兆字节,它将根据需要随着您的分配以128兆字节的数据块增长直至内存不再可用于分配。您随之将收到内存不足的错误。
好的,第三个例子是前两个的结合。当您需要一个可增长堆时,但您不希望它无限制地增长,您想为其设定限制,我们在第26行中创建一个目录路径并将该相同的目录路径传递给createHeap,但现在我们也可以为其指定一个大小。这里的大小可以解释为最大大小。因此,它将增长至一千兆字节,然后,如果您继续分配,它将抛出内存不足的错误。
有两个更先进的方法来管理堆大小。有时您不需要拥有挂载在持久内存设备上的文件系统。您可以直接使用它,LLPL支持这样做,有时您有挂载在持久内存使用上的多于一个的文件系统,您希望把它们连接起来并利用它们建立一个堆,这您也可以在LLPL中实现,如在那些被添加注释的例子所展示的那样。
那么,让我们来看看这里的第三段代码。它展示了如何使用我们谈到的其他堆。到目前为止,我们只是展示了用于一般目的的堆的代码,我将要展示基本上和TransactionalHeap及PersistentHeap相同的“开始”(Getting Started)代码来看看存在的不同。
好的,我们将从第13 行中非常相似的代码开始。我们有一个文件路径,我们将看到如果我们已经初始化了该路径并为其创建了一个堆。如果有的话,我们将开放堆,如果还没有的话,我们将调用createHeap。这是相同的参数,几乎相同的方法签名,只是类名改变了。我们使用TransactionalHeap而不是Heap。如果我们初始化了,我们将继续并进行一个事务并进行一些写入。但是,在本例中,我们将将在第20行开启一个事务,并提供一个作为runnable的事务体,但在第21行,我们将在事务内进行分配。这相当常见,这样做的原因是因为您希望能够在如果事情被中断时能够拥有一致的堆状态,因此我们希望这个整体能一起发生。那么,在第21行,我们分配一个MemoryBlock,然后在第22行,我们在此进行这一setRoot调用。这看上去几乎完全一样,您会注意到,如果我们在第21和22行之间遭到了中断,例如我们在这一时刻遇到了电源故障,我们不会相信它,因为我们还没有把添加书签到在根中的块,但因为这是在事务内,该分配将卷回恢复并且不会出现任何内存泄漏。因此,这是把分配和初始化整合到事务中的一种常见方法。
我们继续来到第 23 行并进行一次设置调用。这是相同的在偏移零进行的long设置,但注意,我们在这里不需要进行“添加至事务”。这将自动完成,因为我们使用了TransactionalMemoryBlock。
然后,如果我们看到读取代码,我们将在此程序的第二次运行中进行,它是完全相同的调用除了我们使用的是TransactionalMemoryBlock之外。相似的是,PersistentHeap的使用和使用一般堆也很相似,但您将看到—我们将一直往下去到第45行,在这里我们第一次运行,我们使用PersistentMemoryBlock和PersistentHeap。我们将在那里分配MemoryBlock和调用setRoot。我们在第47行执行setLong,但我们不必调用清除。这将自动完成,然后第51至56行的读取代码是相同的,除了我们使用的是PersistentMemoryBlock以及之后是关联的PersistentHeap之外。因此,如果您的代码编译成功,您就知道您不会忘记清除该代码,同上,如果您的代码编译成功,您就知道您不会忘记添加至事务,三种堆之间的编程大部分都是相同的。
好的,我将导览更多的几个例子,了解事务的工作方式,那么,如果我们从第13行开始,我们将在这里创建一个TransactionalHeap,然后在第14行,分配一个1k块,我将重复使用这里的程序小片段。
第17行展示的是我们已经看到过的一个简单事务,我们可以把这想成是完成的事务,在事务体退出后,那些写入将出现在持久内存中。有另一种方法创建事务。这显示在第 25 行中。如果您不想在那里传入事务体,但您想要有一个事务对象,您可以在调用中作为参数传递。您可以使用名为Create的不同的静态方法,但它不接受事务体。它只是接受一个堆并为您返回一个事务对象。
然后在第26行,您可以进行运行调用,一个在该事务上的实例方法运行调用并把runnable传递给它,本例是在事务体。.您会注意到在第27行的第一次写入之后,我们将在这里调用我们程序中名为otherWrites的另一个方法,我们将在该调用中传递事务对象和MemoryBlock作为参数。这在上面的第八行中有展示,您可以看到otherWrites接受一个事务对象和MemoryBlock并指示在事务上进行运行调用,用附加的事务体更新该事务。这是一次嵌套事务调用,可以像这样传递参数对于经重构的代码非常便利,特定的实用方法将可以在这里进行header字段的初始化,但您希望重构您的代码,而这不是都在相同的地方。因此,我们在这里有词法嵌套的事务体,但它符合您可能希望编写代码的自然方式。
好的,这些事务的任何一个都不会出错,它们提交所有它们主体对持久内存的更改都将受到影响。如果某些地方出错了,我们希望在这里进行模拟。如果我们运行此程序,我们将实际引发错误。我今天将不会运行这些程序,但它们全部都是可运行的,对所有这些代码的链接包含在我们展示的幻灯片版面的最后一张幻灯片中。
那么,这里在这一事务例子中,我们将看看事务如何可以卷回恢复,在第37行中我们将开始把一个初始值写入MemoryBlock的偏移100,777,现在我在这里有一个try-catch只是要实现这一行为,事务的控制流。通常,我们不会在那里放置这一try-catch,但我们在第39行开始我们的事务我们调用create,我们传递的事务体有两个写入。第一个是setLong,它将覆盖我们在上面偏移100进行的第一个写入为新的888,然后在第41行,我们将对偏移10,000进行另一次写入在我们块的值234。这超出了我们分配的1k的MemoryBlock的边界,因此这将抛出索引出界异常,我们将在这里实现的是,是LLPL中事务的基本行为,如果一个异常从事务体抛出,一个未捕获的异常,它将立即中止事务,然后重新抛出造成中止的异常。因此,在本例中,该索引出界异常将为事务对象所见,那将引起这一未完成交易的中止,然后它将重新抛出索引出界异常,我们将在第43行捕获它。我们在catch中所做的是有事务已卷回恢复的证明。因此,在第44行,我们将读取偏移100的值并判断它实际已卷回恢复至原始值777,那当然在try-catch以外将会是真。
有时,您想要能够从异常中恢复,同时不会使它们造成事务中止,那很容易做到,它展示在第52至62行只需把try-catch放置在事务体内并捕获任何可恢复的错误,同时事务实际看到它之前处理它们。因此我们在第53行执行一个set,然后我们将开始想象我们将解析一个字符串。这一个将失败,因为它是一个基于小数的字符串而我们要求它解析一个整形值。因此,它将跑出一个数字格式异常,但因为我们在事务体内进行捕获,事务看不到它,不会出现中止。我们将在这里以我们想要的方式处理它只需为该值在局部指定一个退化值,然后我们将在第61行作为我们对MemoryBlock的第二次写入来写入它。这我们将在第65和66行看到,事实上,两次写入都顺利进行。一次具有这一个有趣的退化值,因为那是我们决定要写入的,但没有事务中止会出现。
同时这最后一个快速例子正好展示了有时候您希望当事务提交时执行一些代码,而当它中止时执行其他代码,这是在LLPL中写入提交或者中止句柄的常见方法,这里这一次您已经故意地把try-catch放置在事务周围,一次捕获可抛出的类型,您知道如果您在那里看到任何东西,该事务,如果它来自事务体,事务已终止,那么您可以记下它,然后在第77行,执行您希望作为您的中止处理代码的任何代码。然后我们有一个“finally”将提交处理代码,但首先,它将查看标记重置,如果我们遭到中止,我们将不会执行提交处理代码,否则将会执行。
最后在第86行,维持中止事务适当的卷回恢复很重要,transactions that如果多个线程可以在事务操作期间访问内存的相同区域,您可以隔离这一事务代码或者事务期间一次一个线程,在Java和LLPL中实现它的一个简单方法是只需把同步块放在事务周围,在本例中,我们将锁定块对象,因为那是真正共享的东西。
好的,我还有一个例子,我想我们没有时间进行浏览了。您可以在网站上看到这段代码。当您进行基于偏移的编程时,它非常灵活,但也可以变得很糟糕并容易出错,因此有不同的方法可以对这种基于偏移的编程进行抽象以隐藏偏移运算。一个常见方法是使用常规的Java类并创建一些引用块内偏移的静态字段然后设定该Java类的一个实例字段为一个MemoryBlock,一个LLPL MemoryBlock,然后您可以编写使用上述静态偏移的设值方法和取值方法以封装基于偏移的访问和运算。因此,您编写一次后,从外部、设值方法和取值方法,在本例中只是取值方法,就像正常的Java调用,您不用不断验证偏移的正确性。
好的,Soji,这就是我今天要介绍的全部内容。把时间交给您吧。
谢谢,Steve,为我们带来了详细的介绍和代码导读。现在,我将开始问答环节。
提醒一下,这次网络讲座会进行录制并且在直播结束后可以进行点播。同时,请使用“提问”(Ask a Question)选项卡提出您的问题。
现在已经收到了一些问题。第一个问题是关于垃圾回收的。垃圾回收适用于持续堆吗?是否仅适用于某些库?我相信这是指您提到的 LLPL 和 PCJ,或许还有允许在持久内存中启用保持点的 Java 功能。那么,垃圾回收适用于持续堆吗?
好的,在LLPL中,对持续堆的所有内存管理都是手动的。当然,垃圾回收对于控制对象运作良好,但当一个控制对象这些MemoryBlock中的一个对象,在Java堆上得到回收时,在持续端不会有任何事情发生。在PCJ中,我们有这一实验性的库,我们特别地把Java垃圾回收和自动内存管理耦合在一起,持续堆上的一个基于引用计数的方案,将得到和常规Java对象相似的基于可达性的生命期,对持续对象的内存生命期行为。但那里存在着很多的开销,那就是,正如您可以看到的,更复杂且更困难,它仍然处于实验阶段。因此,它是一个很好的目标,作为一个长期的Java程序员感到很高兴,我们真的希望能拥有自动内存管理,但凡事有轻重,我们有LLPL为持久内存提供最高性能的访问,它可以作为一个基础使用来开发其他东西。事实上,我们确实使用和LLPL非常相似的基础开发了PCJ。
对于提到的第三个库,整个堆,我们没有介绍它,但在持久内存块中有一整个堆,那自JDK12就出现在Java中。它严格限于易失性使用,因此在持久内存的使用中没有可用的持续性,但您确实在那里获得了完全的垃圾回收行为。事实上,它只是常规的已经放置在持久内存中的Java堆,或者堆的一部分。那么,它是以实验形式以可持续的方法可用,它是以产品形式以易失性的方法可用,其余的例子都是手动的。谢谢。
下一个问题是有关Java 14中的持续字节缓冲。LLPL有何不同?
好的,嗯,可能最大的方面——有两个主要的不同之处。一个是字节缓冲是普遍存在的。它们一直都存在,每一个人都知道它们,但它们存在两千兆字节的大小限制,因为它们通过Java整形来实现索引的加入和大小的设定。LLPL没有这样的限制。一切都通过long来设定大小因此您可以有一个万亿兆字节大小的堆和非常大的内存块。第二个不同是LLPL为您提供了句柄以及建立基于指针的数据结构的能力,因此您可以把内存块连接起来。您可以分配小的块并把它们连接起来。这是建立基于内存数据结构的关键例如基数树等。这类事情很难通过映射字节缓冲来做到。它们是真正为应用程序的I/O类型而设计的,就像一种缓冲,它们可能在从它建立链数据结构时扩展得没有那么好。那就是两个最大的不同之处。它非常的好我们有映射字节缓冲,但是LLPL,它在大小扩展方面更加好,您通过LLPL建立很多东西,而那些使用映射字节缓冲可能非常困难。
下一个问题是关于内存块的。如果我在分配一个内存块时没有存储句柄或取消分配这个块,会发生什么情况?会有内存泄漏吗?
是的,它是内存泄漏,而事实上,我们在事务例子中看到了,如果我们进行分配而不管什么,一些设置,一些稍后引用至相同引用的方法,我们将不会出现泄漏。因此,分配,我们取回句柄,如果我们不写入到某处,将其放在我们稍后可以取回的内存的某个地方,那么它将会泄漏。实际上有一个方法可以遍历一个堆中的.所有分配,但我们不在LLPL中直接支持这样做,因此您需要自己处理好涉及事务的分配或者只是知道在您分配一个块的时间和写入其值到某处的时间之间不会遭到中断。
下一个问题是关于您提到过的Cassandra存储引擎。那是什么状态?
它不是向上游的。它仍然在进行种并且已经有一定的时间了。我谈到的可插拔存储引擎API还没有完成,但确实需要将其完成,再一次,是和Cassandra开源社区的协作,而最近一次我们和他们谈论到这件事时,他们感到非常兴奋,通过它和持久内存一起向前发展,但它是在Cassandra推出版本4.0之后,而那已经进行了很长的时间。它是Cassandra的一个稳定发布,而该发布之后,对于新的重大特性已经关上了大门。他们运行了一些alpha版本,他们现在正处在beta版本的阶段,因此应该很快就行了,但是在能够让持久内存存储引擎上游至主干之前我们有更多的工作要做。
我们的最后一个问题是关于堆的。如何决定使用哪一个?我相信您在演示中已经涉及了一小部分。
是的,我认为如果您具有任务关键的数据,对的完整性在这里相当关键,这可能是金融的案例或其他一些领域,使用事务性堆将给予您一些编译时间认知,即您所有的写入都将根据任何您的事务体被写入的方式而卷回恢复,这使其更容易实现您的堆的适当的全部的长期一致性。另一方面,有很多的应用程序可能需要混合式的写入。它们将以持久的方式写入它们的主数据,它们将写入一些日志,可能是提交日志,以事务性的方式。因此,您需要这样的混合,那么持续堆或常规堆都是可以使用的。而对于最大的灵活性,一般目的的对让您可以做任何事情,因此这是权衡您可以获得的一些保证,尤其是编译时间保证,和灵活性以及可能的最大速度。它们全都很快,它们确实是,但是您可以选择不同的堆来作出取舍。
好的,那就是最后一个问题了。再次感谢,参加我们这一网络讲座直播的每一个人,感谢你们的问题,谢谢,Steve。大家再见。
再见。

产品和性能信息

1

英特尔的编译器针对非英特尔微处理器的优化程度可能与英特尔微处理器相同(或不同)。这些优化包括 SSE2、SSE3 和 SSSE3 指令集和其他优化。对于在非英特尔制造的微处理器上进行的优化,英特尔不对相应的可用性、功能或有效性提供担保。该产品中依赖于微处理器的优化仅适用于英特尔微处理器。某些非特定于英特尔微架构的优化保留用于英特尔微处理器。关于此通知涵盖的特定指令集的更多信息,请参阅适用产品的用户指南和参考指南。

通知版本 #20110804