1. 程式人生 > >一文了解sun.misc.Unsafe

一文了解sun.misc.Unsafe

類型轉換 鏈接 str DC arch lang 生態 rip export

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

一文了解sun.misc.Unsafe