Java的"偽泛型"變"真泛型"后對性能的影響
泛型存在于Java源代碼中,在編譯為字節(jié)碼文件之前都會進行泛型擦除(type erasure),因此,Java的泛型完全由Javac等編譯器在編譯期提供支持,可以理解為Java的一顆語法糖,這種方式實現(xiàn)的泛型有時也稱為“偽泛型”。
泛型擦除本質(zhì)上就是擦除與泛型相關(guān)的一切信息,例如參數(shù)化類型、類型變量等,Javac還將在需要時進行類型檢查及強制類型轉(zhuǎn)換,甚至在必要時會合成橋方法。
1、真假泛型
如果你是Java業(yè)務開發(fā)者,其實這種所謂的偽泛型已經(jīng)達到了方便使用的目的,例如在容器的使用過程中,能夠記住其類型,這樣就不用在獲取時專門做強制類型轉(zhuǎn)換了,如下:
package cn.hotspotvm; class Wrapper<T> { public T data; public void setData(T data) { this.data = data; } public T getData(){ return data; } } public class TestGeneric { public static void main(String[] args) { Wrapper<Integer> p = new Wrapper<>(); p.setData(new Integer(2)); // 在獲取的時候不用進行強制類型轉(zhuǎn)換,直接用 // Integer接收即可 Integer data = p.getData(); System.out.println(data); } }
泛型尤其在我們使用容器類,如ArrayList、 HashMap等時能提供便利。
假設Java不支持泛型,那么我們針對Integer類型進行封裝的Wrapper代碼應該是如下的樣子:
class Wrapper{ public int data; public void setData(int data) { this.data = data; } public Object getData(){ return data; } } public class TestGeneric { public static void main(String[] args) { Wrapper p = new Wrapper(); p.setData(2); int data = p.getData(); System.out.println(data); } }
這個Wrapper明顯是只能對int類型進行封裝,不過其實現(xiàn)和之前比起來要簡潔,不但實例字段內(nèi)存占用減少,還沒有了裝箱和拆箱操作,也省去了強制類型轉(zhuǎn)換。這也是我們在使用Java泛型時希望看到的版本。
以目前Java的實現(xiàn)來看,是對泛型進行擦除處理的,對Wrapper類型進行擦除后的代碼如下所示。
class Wrapper{ public Object data; public void setData(Object data) { this.data = data; } public Object getData(){ return data; } } public class TestGeneric { public static void main(String[] args) { Wrapper p = new Wrapper(); p.setData(2); // 必須進行強制類型轉(zhuǎn)換為Integer Integer data = (Integer)p.getData(); System.out.println(data); } }
由于在整個應用中,無論是Wrapper還是Wrapper這些類型來說,其Wrapper在虛擬機中只有一個版本,因為需要對任何的Java對象進行封裝,所以聲明為Object,當然如果你知道只會放某個更具體的類或這個類的子類時,也可以將Wrapper類型聲明的更精確一些,如Wrapper。
假設像C++的模板類一樣,Java的“真泛型”也為每一個具體的泛型生成一個獨有的類(類型膨脹式泛型),那么就如下圖所示。
可以看到,真泛型會針對具體的類型生成獨有的類型。針對Wrapper就應該有是這樣:
class Wrapper{ public Integer data; public void setData(Integer data) { this.data = data; } public Integer getData(){ return data; } } public class TestGeneric { public static void main(String[] args) { Wrapper p = new Wrapper(); p.setData(2); Integer data = p.getData(); System.out.println(data); } }
這次類型非常精確,也沒有了強制類型轉(zhuǎn)換。不過與我們理想中的還有差距,主要是沒有將Wrapper中的類型聲明為基本類型int,這會導致裝箱和拆箱操作。在Java中,裝箱和拆箱操作很頻繁,為此JDK開發(fā)人員也在努力優(yōu)化,如延遲裝箱等,現(xiàn)在,Project Valhalla要讓泛型能支持原始類型。好在除非是專門做優(yōu)化的人,否則一般開發(fā)者也不會注意到這種裝箱和拆箱的開銷。
這里還要說的是,由于對象和基本類型找不到一個共同的父類,所以在泛型擦除時只能是對象類型,也就是說,我們不能在Java中寫一個類似Wrapper這樣的聲明。這篇文章我們暫時不討論這個問題,我們討論另外一個重要的問題,也就是為任何一個泛型生成一個真正的類與這種偽泛型的擦除之間會造成哪些性能影響?
2、性能影響
我們舉幾個小例子來看一下:
實例1
class SpecWrapper extends Wrapper<Integer> { public void setData(Integer data) { } }
我們自定義了一個對Integer類封裝的SpecWrapper類,這個類在泛型擦除后如下所示。
class SpecWrapper extends Wrapper { public void setData(Integer data) { } /*synthetic*/ public void setData(Object x0) { // 合成的橋方法 this.setData((Integer)x0); } }
在泛型擦除后,Wrapper類的setData()方法的類型變量T被替換為Object類型,這樣SpecWrapper類中的setData(Integer data)并沒有覆寫這個方法,所以為了覆寫特性,向SpecWrapper類中添加一個合成的橋方法setData(Objext x0)。
這會讓我們在實際調(diào)用setData()方法時調(diào)用的是setData(Object x0)方法,這個方法又調(diào)用了setData(Integer data)方法,而在“真泛型”中,我們直接調(diào)用setData(Integer data)方法即可,雖然JIT會大概率對這種簡單的方法進行內(nèi)聯(lián),但是性能影響肯定是有的。
實例2
class Parent { public static int a = 0; public void invoke() { a = 1; } } final class Sub1 extends Parent { public void invoke() { a = 2; } } public class TestGeneric<T extends Parent> { T t; public TestGeneric(T t1) { this.t = t1; } public static void main(String[] args) throws InterruptedException { TestGeneric<Sub1> s = new TestGeneric<>(new Sub1()); // 調(diào)用test()方法超過一定次數(shù)會分別觸發(fā)C1和C2編譯 for (int i = 0; i < 100000; i++) { s.test(); } // 等待異常的編譯線程編譯完并打印結(jié)果 Thread.sleep(10000); } public void test() { t.invoke(); } }
我們關(guān)注一下test()方法中的t.invoke()動態(tài)分派,通過泛型擦除之后,TestGeneric類中的T是Parent類型,那么test()方法中t聲明的類型就是Parent,如下:
public class TestGeneric{ Parent t; public TestGeneric(Parent t1) { this.t = t1; } public void test() { t.invoke(); } }
配置如下命令查看C2編譯的結(jié)果:
XX:CompileCommand=compileonly,cn/hotspotvm/TestGeneric::test -XX:CompileCommand=print,cn/hotspotvm/TestGeneric::test
C2編譯的版本出來如下:
0x000070e6e00aa915: cmp $0xf800c184,%r8d ; {metadata('cn/hotspotvm/Sub1')} 0x000070e6e00aa91c: jne 0x000070e6e00aa934 0x000070e6e00aa91e: lea (%r12,%r10,8),%rsi 0x000070e6e00aa922: nop 0x000070e6e00aa923: callq 0x000070e6dfe45e60 ; OopMap{off=72} ;*invokevirtual invoke ; - cn.hotspotvm.TestGeneric::test@4 (line 39) ; {optimized virtual_call} 0x000070e6e00aa928: add $0x10,%rsp 0x000070e6e00aa92c: pop %rbp 0x000070e6e00aa92d: test %eax,0x165cb6cd(%rip) # 0x000070e6f6676000 ; {poll_return} 0x000070e6e00aa933: retq 0x000070e6e00aa934: mov $0xffffffde,%esi 0x000070e6e00aa939: mov %r10d,%ebp 0x000070e6e00aa93c: data32 xchg %ax,%ax 0x000070e6e00aa93f: callq 0x000070e6dfe45460 ; OopMap{rbp=NarrowOop off=100} ;*invokevirtual invoke ; - cn.hotspotvm.TestGeneric::test@4 (line 39) ; {runtime_call}
也就等同于如下:
if(t是Sub1類型){ 直接調(diào)用Sub1的invoke()方法,也就是optimized virtual_call的意思 }else{ 動態(tài)分派,通過查找方法表來實現(xiàn)調(diào)用,也就是invokevirtual invoke的意思 }
C2實際是通過運行時采樣,發(fā)現(xiàn)t的類型只有Sub1,所以做了這樣的優(yōu)化,將動態(tài)分派優(yōu)化為了一次比較+一次直接調(diào)用的開銷?! ?/p>
假設是“真泛型”上場,那TestGeneric應該是如下的樣子:
public class TestGeneric{ Sub1 t; public TestGeneric(Sub1 t1) { this.t = t1; } public void test() { t.invoke(); } }
此時C2編譯出來的版本如下:
0x000074bd840b7b5b: callq 0x000074bd83e45e60 ; OopMap{off=64} ;*invokevirtual invoke ; - cn.hotspotvm.TestGeneric::test@4 (line 39) ; {optimized virtual_call}
直接就是直調(diào),比之前省了一次判斷,代碼很簡潔。因為C2得到了t的更精確類型Sub1,并且這個Sub1還是final修飾的類,不會有子類,所以直接調(diào)用即可。
雖然少量調(diào)用可能并不能體現(xiàn)出來差異,更何況現(xiàn)在的C2優(yōu)化實在強大,使得它們的性能差異可能只在極少數(shù)的情況下才能體現(xiàn)出來。
C2編譯器非常喜歡精確類型,這樣在類型傳播的過程中能觸發(fā)許多優(yōu)化,編譯出性能更好的版本,后續(xù)我們在介紹C2時,看一看其類型傳播,就能深刻體會到它的強大?!?/p>
實例3
假設有這么一個方法,實現(xiàn)如下:
class Parent{ // ... } find class Sub1 extends Parent{ // ... } class Sub2 extends Parent{ // ... } public class Wrapper<T extends Parent>{ public void test(T t){ if(t instanceof Sub1){ // 執(zhí)行Sub1的邏輯 }else if( t instanceof Sub2){ // 執(zhí)行Sub2的邏輯 }else{ // 執(zhí)行其它類型的邏輯 } } }
對于偽泛型來說,擦除后,T是Parent類型。方法test(Parent t)無法做任何優(yōu)化。
對于真泛型來說,至少Wrapper和Wrapper版本中的test()是可以優(yōu)化的。
public void test(Sub1 t)版本中只需要直接執(zhí)行Sub1的邏輯就可以,并不需要判斷,因為Sub1是final類,所以t只能是Sub1類型,如下:
public void test(Sub1 t){ 執(zhí)行Sub1的邏輯 }
public void test(Sub2 t)版本中只需要直接執(zhí)行Sub2的邏輯就可以,并不需要判斷,雖然Sub2是非final類型,但是經(jīng)過類層次分析后發(fā)現(xiàn),Sub2沒有具體的實現(xiàn)子類,那這時候也能認為這個版本只有Sub2類型。
public void test(Sub2 t){ 執(zhí)行Sub2的邏輯 }
到此這篇關(guān)于Java的"偽泛型"變"真泛型"后,會對性能有幫助嗎?的文章就介紹到這了,更多相關(guān)Java的"偽泛型"變"真泛型"后,會對性能有幫助嗎?內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java求素數(shù)和最大公約數(shù)的簡單代碼示例
這篇文章主要介紹了Java求素數(shù)和最大公約數(shù)的簡單代碼示例,其中作者創(chuàng)建的Fraction類可以用來進行各種分數(shù)運算,需要的朋友可以參考下2015-09-09Java8 將一個List<T>轉(zhuǎn)為Map<String,T>的操作
這篇文章主要介紹了Java8 將一個List<T>轉(zhuǎn)為Map<String, T>的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02