前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 17 更新(9):Unsafe 不 safe,我们来一套 safe 的 API 访问堆外内存

Java 17 更新(9):Unsafe 不 safe,我们来一套 safe 的 API 访问堆外内存

作者头像
bennyhuo
发布2021-10-19 14:25:09
2.5K0
发布2021-10-19 14:25:09
举报
文章被收录于专栏:Bennyhuo

关键词:Java Java17

使用 Unsafe 直接访问堆外内存存在各种安全性问题,对于使用者的要求也比较高,不太适合在业务当中广泛使用。于是,Java 在新孵化的 API 当中提供了更安全的方案。

JEP 412: Foreign Function & Memory API (Incubator)

接下来,我们来聊聊访问外部资源的新 API,这些内容来自于 JEP 412: Foreign Function & Memory API (Incubator)。这个提案主要应对的场景就是调用 Java VM 以外的函数,即 Native 函数;访问 Java VM 以外的内存,即堆外内存(off-heap memory)。

这不就是要抢 JNI 的饭碗吗?

对,这个提案里面提到的堆外内存和代码访问都可以用 JNI 来做到,不过 JNI 不够好用,还够不安全。

Java 程序员不仅需要编写大量单调乏味的胶水代码(JNI 接口),还要去编写和调试自己本不熟悉(多数 Java 程序员甚至根本不会)的 C、C++ 代码,更要命的是调试工具也没有那么好用。当然,这些都可以克服,只是 Java 和 C、C++ 的类型系统却有着本质的区别而无法直接互通,我们总是需要把传到 C、C++ 层的 Java 对象的数据用类似于反射的 API 取出来,构造新的 C、C++ 对象来使用,非常的麻烦。

说到这个问题,我甚至在公司内见过有人用 C++ 基于 JNI 把 Java 层的常用类型都封装了一遍,你能想象在 C++ 代码当中使用 ArrayList 的情形吗?我当时一度觉得自己精神有些恍惚。

这些年来 Java 官方在这方面也没有什么实质性的进展。JNI 难用就难用吧,总算还有得用,一些开源的框架例如 JNA、JNR、JavaCPP 都是基于 JNI 做了一些简化的工作,让 Java 与 Native 语言的调用没那么令人难受。

你可能以为这个提案的目的也是搞一个类似的框架,其实不然。Java 官方嘛,不搞就不搞,要搞就搞一套全新的方案,让开发者用着方便,程序性能更好(至少不比 JNI 更差),普适性更强,也更安全 —— 至少,他们是这么想的。

稍微提一下,堆外内存访问的 API 从 Java 14 就开始孵化,到 Java 17 连续肝了四个版本了已经,仍然还是 incubator;访问外部函数的 API 则从 Java 16 开始孵化,到现在算是第二轮孵化了吧。如果大家要想在自己的程序里面体验这个能力,需要给编译器和虚拟机加参数:

代码语言:javascript
复制
--add-modules jdk.incubator.foreign --enable-native-access ALL-UNNAME

由于内容较多,本篇我们只介绍堆外内存的访问。外部函数访问的内容我们放到下一篇介绍。

访问堆外内存

基于现在的方案,我们有三种方式能访问到堆外内存,分别是

ByteBuffer(就是 allocateDirect),这个方式用起来相对安全,使用体验也与访问虚拟机堆内存一致,但执行效率相对一般:

代码语言:javascript
复制
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

使用 Unsafe 的相关方法,这个方式在 JIT 优化之下效率较高,但非常不安全,因为它实际上可以访问到任意位置的内存,例如:

代码语言:javascript
复制
Unsafe unsafe = ...;
var handle = unsafe.allocateMemory(8); // 申请 8 字节内存

unsafe.putDouble(handle, 1024); // 往该内存当中写入 1024 这个 double
System.out.println(unsafe.getDouble(handle)); // 从该内存当中读取一个 double 出来

unsafe.freeMemory(handle); // 释放这块内存

使用 JNI,通过 C/C++ 直接操作堆外内存。

对于 Java 程序员来讲,效率较高的后两种方式都不是特别友好。

接下来我们看一下新的内存访问方案,它主要解决了分配、访问和作用域等几个问题。

堆外内存分配

我们可以通过 MemorySegment 来做到这一点:

代码语言:javascript
复制
MemorySegment segment = MemorySegment.allocateNative(100, ResourceScope.newImplicitScope());

尽管看上去跟前面的 Unsafe 类似,但这里面有很多细节上的差异,因为它对于堆外内存的访问是受限制的,就像访问数组一样更加安全。另外请注意 ResourceScope 这个参数,它会控制分配的堆外内存的作用范围,这个我们会在后面介绍。

堆外内存访问

在堆外内存开辟以后,我们通常需要按照某种变量的方式去访问它,例如想要以 int 的方式读写,那么就创建一个 VarHandle 即可:

代码语言:javascript
复制
VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

这里支持的类型就是基本类型,包括 byte、short、char、int、float、long、double。

代码语言:javascript
复制
for (int i = 0; i < 25; i++) {
    intHandle.set(segment, /* offset */ i * 4, /* value to write */ i);
}

我们知道 Java 的 int 占 4 个字节,因此直接对前面开辟的内存 segment 进行读写操作即可。那如果我读写的范围越界会发生什么呢?

代码语言:javascript
复制
intHandle.set(segment, 100 /* out of bounds!! */, 1000);

运行程序结果发现抛了个异常,这个异常就是 MemorySegment 抛出来的:

代码语言:javascript
复制
Exception in thread "main" java.lang.IndexOutOfBoundsException: Out of bound access on segment MemorySegment{ id=0x17366e0a limit: 100 }; new offset = 100; new length = 4

这样相比使用 Unsafe 访问内存的好处就在于受控制。

使用 Unsafe 访问堆外内存就好像直接使用 C 指针操作内存一样。C 语言主张相信程序员,所以对于 C 程序员使用指针访问内存不加任何限制。可是在内存管理这个问题上,Java 程序员并不一定像 C 程序员那么可靠。

img

我们不妨再给大家看看 Unsafe 的例子,看看是不是如同操作 C 指针一样:

代码语言:javascript
复制
var handle = unsafe.allocateMemory(16);

// 操作分配的内存之后的部分,实际上这部分内存完全不可预见
unsafe.putInt(handle + 16, 1000); 
// 读取非法内存
System.out.println(unsafe.getInt(handle + 16)); 

unsafe.freeMemory(handle);
// 内存已经回收了,仍然可以读
System.out.println(unsafe.getInt(handle)); 

这样我们就知道 Unsafe 是真的不 safe 啊。不仅如此,一旦忘了释放内存,就会造成内存泄漏。我们甚至无法通过 handle 来判断内存是否有效,对于已经回收的内存,handle 对象不就是野指针了嘛。

除了提升安全性以外,新 API 还提供了一套内存布局相关的 API:

这套 API 可以降低堆外内存访问的代码复杂度,例如:

代码语言:javascript
复制
SequenceLayout intArrayLayout = MemoryLayout.sequenceLayout(25, MemoryLayout.valueLayout(32, ByteOrder.nativeOrder()));
MemorySegment segment = MemorySegment.allocateNative(intArrayLayout, newImplicitScope());
VarHandle indexedElementHandle = intArrayLayout.varHandle(int.class, PathElement.sequenceElement());
for (int i = 0; i < intArrayLayout.elementCount().getAsLong(); i++) {
    indexedElementHandle.set(segment, (long) i, i);
}

这样我们在开辟内存空间的时候只需要通过 SequenceLayout 描述清楚我们需要什么样的内存(32bit,Native 字节序),多少个(25 个),然后用它去开辟空间,并完成读写。

  • PaddingLayout 会在我们需要的数据后添加额外的内存空间,主要用于内存对齐。
  • ValueLayout 用来映射基本的数值类型,例如 int、float 等等。
  • GroupLayout 可以用来组合其他的 MemoryLayout。它有两种类型,分别是 STRUCT 和 UNION。熟悉 C 语言的小伙伴们应该立刻就能明白,它在调用 C 函数的时非常有用,可以用来映射 C 的结构体和联合体。

简单来说,在调用 C 函数时,我们可以很方便地使用这些 MemoryLayout 映射到 C 类型。

img

堆外内存的作用域

作用域这个东西实在是关键。

Java 的一大优点就是内存垃圾回收机制。内存都被虚拟机接管了,我们只需要考虑如何使用内存即可,虚拟机就像个大管家一样默默的为我们付出。这极大的降低了程序员管理内存的成本,也极大的降低了程序员在内存操作上犯错误的可能,对比我之前写 C++ 的时候经常因为某个内存错误查到半夜找不到头绪的情况,用 Java 写程序时开发效率的提升真不是一点儿半点儿。

要想让 Java 程序员用得舒服,那必须把堆外内存的管理也尽可能做到简单易用。为此,JDK 引入了资源作用域的概念,对应的类型就是 ResourceScope。这是一个密封接口,它有且仅有一个非密封的实现类 ResourceScopeImpl,JDK 还为这个实现类提供了三种具体的实现:

  • GLOBAL:这实际上是一个匿名内部类对象,它是全局作用域,使用它开辟的堆外内存不会自动释放。
  • ImplicitScopeImpl:我们在前面演示新 API 的使用时已经提到过,调用 ResourceScope.newImplicitScope() 返回的正是 ImplicitScopeImpl。这种类型的 Scope 不能被主动关闭,不过使用它开辟的内存会在持有内存的 MemorySegment 对象不再被持有时释放。这个逻辑在 CleanerImpl 当中通过 ReferenceQueue 配合 PhantomReference 来实现。
  • SharedScope:最主要的能力就是提供了多线程共享访问的支持;是 ImplicitScopeImpl 的父类,二者的差别在于 SharedScope 可以被主动关闭,不过必须确保只能被关闭一次。
  • ConfinedScope:单线程作用域,只能在所属的线程内访问,比较适合局部环境下的内存管理。

我们再来看一个例子:

代码语言:javascript
复制
try(var scope = ResourceScope.newConfinedScope()) {
    MemorySegment memorySegment = MemorySegment.allocateNative(100, scope);
    ...
}

这个例子当中我们使用 ConfinedScope 来开辟内存,由于这个 scope 在 try-resource 语句结束之后就会被关闭,因此其中开辟的内存也会在语句结束的时候理解回收。

小结

Java 17 为访问堆外内存提供了一套较为完成的 API,试图简化 Java 代码操作堆外内存的难度。从实际的使用体验来看,安全性确实可以得到一定程度上的保障,不过易用性嘛,倒是保持了 Java 的传统,这个我们在下一篇文章当中还会提及。


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kotlin 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JEP 412: Foreign Function & Memory API (Incubator)
  • 访问堆外内存
    • 堆外内存分配
      • 堆外内存访问
        • 堆外内存的作用域
        • 小结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档