zoukankan      html  css  js  c++  java
  • 一文了解sun.misc.Unsafe

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

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

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

    组织

     

    乍看上去,sun.misc.Unsafe的特性集合可能会让我们觉得有些混乱,它一站式地提供了各种特性。

    我试图将这些特性进行分类,可以得到如下5种使用场景:

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

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

    原子访问

    原子访问是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

  • 相关阅读:
    [saiku] 系统登录成功后查询Cubes
    216. Combination Sum III
    215. Kth Largest Element in an Array
    214. Shortest Palindrome
    213. House Robber II
    212. Word Search II
    211. Add and Search Word
    210. Course Schedule II
    分硬币问题
    开始学习Python
  • 原文地址:https://www.cnblogs.com/Zyf2016/p/8495883.html
Copyright © 2011-2022 走看看