一文了解sun.misc.Unsafe

Java语言和JVM平台已经度过了20岁的生日。它最初起源于机顶盒、移动设备和Java-Card,同时也应用在了各类服务器系统中,Java已成为物联网(Internet of Things)的通用语言。咱们显然能够看到Java已经无处不在!html

可是不那么为人所知的是,Java也普遍应用于各类低延迟的应用中,如游戏服务器和高频率的交易应用。这只因此可以实现要归功于Java的类和包在可见性规则中有一个恰到好处的漏洞,让咱们可以使用一个很便利的类,这个类就是sun.misc.Unsafe。这个类从过去到如今一直都有着很大的分歧,有些人喜欢它,而有些人则强烈地讨厌它——但关键的一点在于,它帮助JVM和Java生态系统演化成了今天的样子。基本上能够说,Unsafe类为了速度,在Java严格的安全标准方面作了一些妥协。java

若是在Java世界中移除了sun.misc.Unsafe(和一些较小的私有API),而且没有足够的API来替代的话,那Java世界将会发生什么呢,针对这一点引起了热烈的讨论,包括在JCrete上、“sun.misc.Unsafe会发生什么”论文以及在DripStat像这样的博客文章。Oracle的最终提议(JEP260)解决了这个问题,它提供了一个很好的迁移路径。但问题依然存在——在Unsafe真的消失后,Java世界将会是什么样子呢?git

组织

 

乍看上去,sun.misc.Unsafe的特性集合可能会让咱们以为有些混乱,它一站式地提供了各类特性。github

我试图将这些特性进行分类,能够获得以下5种使用场景:数据库

  • 对变量和数组内容的原子访问,自定义内存屏障
  • 对序列化的支持
  • 自定义内存管理/高效的内存布局
  • 与原生代码和其余JVM进行互操做
  • 对高级锁的支持

在咱们试图为这些功能寻找替代实现时,至少在最后一点上能够宣告胜利。Java早就有了强大(坦白说也很漂亮)的官方API,这就是java.util.concurrent.LockSupport。api

原子访问

原子访问是sun.misc.Unsafe被普遍应用的特性之一,特性包括简单的“put”和“get”操做(带有volatile语义或不带有volatile语义)以及比较并交换(compare and swap,CAS)操做。数组

public long update() {
 for(;;) {
   long version = this.version;
   long newVersion = version + 1;
   if (UNSAFE.compareAndSwapLong(this, VERSION_OFFSET, version, newVersion)) {
      return newVersion;
   }
  }
}

可是,请稍等,Java不是已经经过官方API为该功能提供了支持吗?绝对是这样的,借助Atomic类确实可以作到,可是它会像基于sun.misc.Unsafe的API同样丑陋,在某些方面甚至更糟糕,让咱们看一下到底为何。缓存

AtomicX类其实是真正的对象。假设咱们要维护一个存储系统中的某条记录,而且但愿可以跟踪一些特定的统计数据或元数据,好比版本的计数:安全

public class Record {
 private final AtomicLong version = new AtomicLong(0);

 public long update() {
   return version.incrementAndGet();
 }
}

尽管这段代码很是易读,可是它却污染到了咱们的堆,由于每条数据记录都对应两个不一样的对象,而不是一个对象,具体来说,这两个对象也就是Atomic实例以及实际的记录自己。它所致使的问题不只仅是产生无关的垃圾,并且会致使额外的内存占用以及Atomic实例的解引用(dereference)操做。性能优化

可是,咱们能够作的更好一点——还有另一个API,那就是java.util.concurrent.atomic.AtomicXFieldUpdater类。

AtomixXFieldUpdater是正常Atomic类的内存优化版本,它牺牲了API的简洁性来换取内存占用的优化。经过该组件的单个实例就能支持某个类的多个实例,在咱们的Record场景中,能够用它来更新volatile域。

public class Record {
 private static final AtomicLongFieldUpdater<Record> VERSION =
      AtomicLongFieldUpdater.newUpdater(Record.class, "version");

 private volatile long version = 0;

 public long update() {
   return VERSION.incrementAndGet(this);
 }
}

在对象建立方面,这种方式可以生成更为高效的代码。同时,这个updater是一个静态的final域,对于任意数量的record,只须要有一个updater就能够了,而且最重要的是,它如今就是可用的。除此以外,它仍是一个受支持的公开API,它始终应该是优选的策略。不过,另外一方面,咱们看一下updater的建立和使用方式,它依然很是丑陋,不是很是易读,坦白说,凭直觉看不出来它是个计数器。

那么,咱们能更好一点吗?是的,变量句柄(Variable Handles)(或者简洁地称之为“VarHandles”)目前正处于设计阶段,它提供了一种更有吸引力的API。

VarHandles是对数据行为(data-behavior)的一种抽象。它们提供了相似volatile的访问方式,不只可以用在域上,还能用于数组或buffers中的元素上。

乍看上去,下面的样例可能显得有些诡异,因此咱们看一下它是如何实现的。

public class Record {
 private static final VarHandle VERSION;

 static {
   try {
     VERSION = MethodHandles.lookup().findFieldVarHandle
        (Record.class, "version", long.class);
   } catch (Exception e) {
      throw new Error(e);
   }
 }

 private volatile long version = 0;

 public long update() {
   return (long) VERSION.addAndGet(this, 1);
 }
}

VarHandles是经过使用MethodHandles API建立的,它是到JVM内部连接(linkage)行为的直接入口点。咱们使用了MethodHandles-Lookup方法,将包含域的类、域的名称以及域的类型传递进来,或者也能够说咱们对java.lang.reflect.Field进行了“反射的反操做(unreflect)”。

那么,你可能会问它为何会比AtomicXFieldUpdater API更好呢?如前所述,VarHandles是对全部变量类型的通用抽象,包括数组甚至ByteBuffer。也就是说,咱们可以经过它抽象全部不一样的类型。在理论上,这听起来很是棒,可是在当前的原型中依然存在必定的不足。对返回值的显式类型转换是必要的,由于编译器还不能自动将类型判断出来。另外,由于这个实现依然处于早期的原型阶段,因此它还有一些其余的怪异之处。随着有更多的人参与VarHandles,我但愿这些问题未来可以消失掉,在Valhalla项目中所提议的一些相关的语言加强已经逐渐成形了。

序列化

在当前,另一个重要的使用场景就是序列化。无论你是在设计分布式系统,仍是将序列化的元素存储到数据库中,或者实现非堆的功能,Java对象都要以某种方式进行快速序列化和反序列化。这方面的座右铭是“越快越好”。所以,不少的序列化框架都会使用Unsafe::allocateInstance,它在初始化对象的时候,可以避免调用构造器方法,在反序列化的时候,这是颇有用的。这样作会节省不少时间而且可以保证安全性,由于对象的状态是经过反序列化过程重建的。

public String deserializeString() throws Exception {
 char[] chars = readCharsFromStream();
 String allocated = (String) UNSAFE.allocateInstance(String.class);
 UNSAFE.putObjectVolatile(allocated, VALUE_OFFSET, chars);
 return allocated;
}

请注意,即使在Java 9中sun.misc.Unsafe依然可用,上述的代码片断也可能会出现问题,由于有一项工做是优化String的内存占用的。在Java 9中将会移除char[]值,并将其替换为byte[]。请参考提高String内存效率的JEP草案来了解更多细节。

让咱们回到这个话题:尚未Unsafe::allocateInstance的替代提议,可是jdk9-dev邮件列表在讨论解决方案。其中一个想法是将私有类sun.reflect.ReflectionFactory::newConstructorForSerialization转移到一个受支持的地方,它可以阻止核心的类以非安全的方式进行初始化。另一个有趣的提议是冻结数组(frozen array),未来它可能也会对序列化框架提供帮助。

看起来效果可能会以下面的代码片断所示,这彻底是按照个人想法所造成的,由于这方面尚未提议,可是它基于目前可用的sun.reflect.ReflectionFactory API。

public String deserializeString() throws Exception {
 char[] chars = readCharsFromStream().freeze();
 ReflectionFactory reflectionFactory = 
       ReflectionFactory.getReflectionFactory();
 Constructor<String> constructor = reflectionFactory
       .newConstructorForSerialization(String.class, char[].class);
 return constructor.newInstance(chars);
}

这里会调用一个特殊的反序列化构造器,它会接受一个冻结的char[]。String默认的构造器会建立传入char[]的一个副本,从而防止外部变化的影响。而这个特殊的反序列化构造器则不须要复制这个给定的char[],由于它是一个冻结的数组。稍后还会讨论冻结数组。再次提醒,这只是我我的的理解,真正的草案看起来可能会有所差异。

内存管理

sun.misc.Unsafe最重要的用途可能就是读取和写入了,这不只包括第一节所看到的针对堆空间的操做,它还能对Java堆以外的区域进行读取和写入。按照这种说法,就须要原生内存(经过地址/指针来体现)了,而且偏移量须要手动计算。例如:

public long memory() {
 long address = UNSAFE.allocateMemory(8);
 UNSAFE.putLong(address, Long.MAX_VALUE);
 return UNSAFE.getLong(address);
}

有人可能会跳起来讲,一样的事情还能够直接使用ByteBuffers来实现:

public long memory() {
 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);
 byteBuffer.putLong(0, Long.MAX_VALUE);
 return byteBuffer.getLong(0);
}

表面上看,这种方式彷佛更有吸引力:不过遗憾的是,ByteBuffer只能用于大约2GB的数据,由于DirectByteBuffer只能经过一个int(ByteBuffer::allocateDirect(int))来建立。另外,ByteBuffer API的全部索引都是32位的。比尔·盖茨不是还说过“谁须要超过32位的东西呢?”

使用long类型改造这个API会破坏兼容性,因此VarHandles来拯救咱们了。

public long memory() {
 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);
 VarHandle bufferView = 
           MethodHandles.byteBufferViewVarHandle(long[].class, true);
 bufferView.set(byteBuffer, 0, Long.MAX_VALUE);
 return bufferView.get(byteBuffer, 0);
}

在本例中,VarHandle API真得更好吗?此时,咱们受到相同的限制,只能建立大约2GB的ByteBuffer,而且针对ByteBuffer视图所建立的内部VarHandle实现也是基于int的,可是这个问题可能也“能够解决”。因此,就目前来说,这个问题尚未真正的解决方案。不过这里的API是与第一个例子相同的VarHandle API。

有一些其余的可选方案正处于讨论之中。Oracle的工程师Paul Sandoz,他同时仍是JEP 193:Variable Handles项目的负责人,曾经在twitter讨论过内存区域(Memory Region)的概念,尽管这个概念还不清晰,可是这种方式看起来颇有前景。一个清晰的API可能看起来会以下面的程序片断所示。

public long memory() {
 MemoryRegion region = MemoryRegion
      .allocateNative("myname", MemoryRegion.UNALIGNED, Long.MAX_VALUE);

 VarHandle regionView = 
             MethodHandles.memoryRegionViewVarHandle(long[].class, true);
 regionView.set(region, 0, Long.MAX_VALUE);
 return regionView.get(region, 0);
}

这只是一个理念,但愿Panama项目,也就是OpenJDK的原生代码项目,可以为这些抽象提出一项提议,由于这些内存区域也须要用到原生库,在它的调用中会预期传入内存地址(指针)。

互操做性

最后一个话题是互操做性(interoperability)。这并不限于在不一样的JVM间高效地传递数据(可能会经过共享内存,它多是某种类型的内存区域,这样可以避免缓慢的socket通讯),并且还包含与原生代码的通讯和信息交换。

Panama项目致力于取代JNI,提供一种更加相似于Java并高效的方式。关注JRuby的人可能会知道Charles Nutter,这是由于他为JNR所做出的贡献,也就是Java Native Runtime,尤为是JNR-FFI实现。FFI指的是外部函数接口(Foreign Function Interface),对于使用其余语言(如Ruby、Python等等)的人来讲,这是一个典型的术语。

基本上来说,FFI会为调用C(以及依赖于特定实现的C++)构建一个抽象层,这样其余的语言就能够直接进行调用了,而没必要像在Java中那样建立胶水代码。

举例来说,假设咱们但愿经过Java获取一个pid,当前所须要的是以下的C代码:

extern c {
  JNIEXPORT int JNICALL 
       Java_ProcessIdentifier_getProcessId(JNIEnv *, jobject);
}

JNIEXPORT int JNICALL 
       Java_ProcessIdentifier_getProcessId(JNIEnv *env, jobject thisObj) {
 return getpid();
}

public class ProcessIdentifier {
 static {
   System.loadLibrary("processidentifier");
 }

 public native void talk();
}

使用JNR咱们能够将其简化为一个简单的Java接口,它会经过JNR实现绑定的原生调用上。

interface LibC {
  void getpid();
}

public int call() {
 LibC c = LibraryLoader.create(LibC.class).load("c");
 return c.getpid();
}

JNR内部会将绑定代码织入进去并将其注入到JVM中。由于Charles Nutter是JNR的主要开发者之一,而且他还参与Panama项目,因此咱们有理由相信会出现一些很是相似的内容。

经过查看OpenJDK的邮件列表,咱们彷佛很快就会拥有MethodHandle的另一种变种形式,它会绑定原生代码。可能出现的绑定代码以下所示:

public void call() {
 MethodHandle handle = MethodHandles
               .findNative(null, "getpid", MethodType.methodType(int.class));
 return (int) handle.invokeExact();
}

若是你以前没有见过MethodHandles的话,这看起来可能有些怪异,可是它明显要比JNI版本更加简洁和具备表现力。这里最棒的一点在于,与反射获得Method实例相似,MethodHandle能够进行缓存(一般也应该这样作),这样就能够屡次调用了。咱们还能够将原生调用直接内联到JIT后的Java代码中。

不过,我依然更喜欢JNR接口的版本,由于从设计角度来说它更加简洁。另外,我确信将来可以拥有直接的接口绑定,它是MethodHandle API之上很是好的语言抽象——若是规范不提供的话,那么一些热心的开源提交者也会提供。

还有什么呢?

围绕Valhalla和Panama项目还有其余的一些事宜。有些与sun.misc.Unsafe没有直接的关系,可是值得说起一下。

ValueTypes

在这些讨论中,最热门的话题可能就是ValueTypes了。它们是轻量级的包装器(wrapper),其行为相似于Java的原始类型。顾名思义,JVM可以将其视为简单的值,能够对其进行特殊的优化,而这些优化是没法应用到正常的对象上的。咱们能够将其理解为可由用户定义的原始类型。

value class Point {
 final int x;
 final int y;
}

// Create a Point instance
Point point = makeValue(1, 2);

这依然是一个草案API,咱们不必定会拥有新的“value”关键字,由于这有可能破坏已经使用该关键字做为标识符的用户代码。

即使如此,那ValueTypes到底有什么好处呢?如前所述,JVM可以将这些类型视为原始值,那么就能够将它的结构扁平化到一个数组中:

int[] values = new int[2];
int x = values[0];
int y = values[1];

它们还可能被传递到CPU寄存器中,极可能不须要分配在堆上。这实际上可以节省不少的指针解引用,并且会为CPU提供更好的方案来预先获取数据并进行逻辑分支的预判。

目前,相似的技术已经获得了应用,它用于分析大型数组中的数据。Cliff Click的h2o架构彻底就是这么作的,它为统一的原始数据提供了速度极快的map-reduce操做。

另外,ValueTypes还能够具备构造器、方法和泛型。Oracle的Java语言架构师Brian Goetz曾经很是形象的这样描述,咱们能够将其理解为“编码像类同样,可是行为像int同样”。

另一个相关的特性就是咱们所期待的“specialized generics”,或者更加普遍的“类型具体化”。它的理念很是简单:将泛型系统进行扩展,不只要支持对象和ValueTypes,还要支持原始类型。无处不在String类将会按照这种方式,成为使用ValueTypes进行重写的候选者。

Specialized Generics

为了实现这一点(并保持向后兼容),泛型系统须要进行改造,将会引入一些新的特殊的通配符。

class Box<any T> {
  void set(T element) { … };
  T get() { ... };
}

public void generics() {
 Box<int> intBox = new Box<>();
 intBox.set(1);
 int intValue = intBox.get();

 Box<String> stringBox = new Box<>();
 stringBox.set("hello");
 String stringValue = stringBox.get();

 Box<RandomClass> box = new Box<>();
 box.set(new RandomClass());
 RandomClass value = box.get();
}

在本例中,咱们所设计的Box接口使用了新的通配符any,而不是你们所熟知的?通配符。它为JVM内部的类型specializer提供描述信息,代表可以接受任意的类型,无论是对象、包装器、值类型仍是原始类型都可以。

关于类型具体化在今年的JVM语言峰会(JVM Language Summit,JVMLS)上有一个很精彩的讨论,这是由Brian Goetz本人所作的。

Arrays 2.0

Arrays 2.0的提议已经有挺长的时间了,关于这方面能够参考JVMLS 2012上John Rose的演讲。其中最突出的特性将是移除掉当前数组中32位索引的限制。在目前的Java中,数组的大小不能超过Integer.MAX_VALUE。新的数组预期可以接受64位的索引。

另一个很棒的特性就是“冻结(freeze)”数组(就像咱们在上面的序列化样例中所看到的那样),容许咱们建立不可变的数组,这样它就能够处处传递而没有内容发生变化的风险。

并且好事成双,咱们指望Arrays 2.0可以支持specialized generics!

ClassDynamic

另一个相关的更有意思的提议被称之为ClassDynamic。相对于到如今为止咱们所讨论的其余内容,这个提议目前所处的状态多是最初级的,因此目前并无太多可用的信息。不过,咱们能够提早估计一下它是什么样子的。

动态类引入了与specialized generics相同的泛化(generalization)概念,不过它是在一个更普遍的做用域内。它为典型的编码模式提供了模板机制。假设将Collections::synchronizedMap返回的集合视为一种模式,在这里每一个方法调用都是初始调用的同步版本:

R methodName(ARGS) {
  synchronized (this) {
    underlying.methodName(ARGS);
  }
}

借助动态类以及为specializer所提供的模式模板(pattern-template)可以极大地简化循环模式(recurring pattern)的实现。如前所述,当编写本文的时候,尚未更多的信息,我但愿在不久的未来可以看到更多的后续信息,它可能会是Valhalla项目的一部分。

结论

总体而言,对于JVM和Java语言的发展方向以及它的加速研发,我感到很是开心。不少有意思和必要的解决方案正在进行当中,Java变得更加现代化,而JVM也提供了高效的方案和功能加强。

从个人角度来说,毫无疑问,我认为你们值得在JVM这种优秀的技术上进行投资,我指望全部的JVM语言都可以重新添加的集成特性中收益。

我强烈推荐JVMLS 2015上的演讲,以了解上述大多数话题的更多信息,另外,我建议读者阅读一下Brian Goetz针对Valhalla项目的概述。

关于做者

Christoph Engelbert是Hazelcast的技术布道师。他对Java开发充满热情,是开源软件的资深贡献者,主要关注于性能优化以及JVM和垃圾收集的底层原理。经过研究软件的profiler并查找代码中的问题,他很是乐意将软件的能力发挥到极限。

 

查看英文原文:A Post-Apocalyptic sun.misc.Unsafe World

相关文章
相关标签/搜索