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種使用場景:數據庫
在咱們試圖爲這些功能尋找替代實現時,至少在最後一點上能夠宣告勝利。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了。它們是輕量級的包裝器(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進行重寫的候選者。
爲了實現這一點(並保持向後兼容),泛型系統須要進行改造,將會引入一些新的特殊的通配符。
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的提議已經有挺長的時間了,關於這方面能夠參考JVMLS 2012上John Rose的演講。其中最突出的特性將是移除掉當前數組中32位索引的限制。在目前的Java中,數組的大小不能超過Integer.MAX_VALUE。新的數組預期可以接受64位的索引。
另一個很棒的特性就是「凍結(freeze)」數組(就像咱們在上面的序列化樣例中所看到的那樣),容許咱們建立不可變的數組,這樣它就能夠處處傳遞而沒有內容發生變化的風險。
並且好事成雙,咱們指望Arrays 2.0可以支持specialized generics!
另一個相關的更有意思的提議被稱之爲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並查找代碼中的問題,他很是樂意將軟件的能力發揮到極限。