一文了解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

相關文章
相關標籤/搜索