java让对象分配在栈上_java – Hotspot何时可以在堆栈上分配对象?
我做了一些實驗,以便了解Hotspot何時可以進行堆棧分配.事實證明,它的堆棧分配比基于available documentation的預期要有限得多.Choi“Escape Analysis for Java”引用的文章表明,只分配給局部變量的對象總是可以堆棧分配.但事實并非如此.
所有這些都是當前Hotspot實現的實現細節,因此它們可能會在將來的版本中進行更改.這是指我的OpenJDK安裝,它是X86-64的版本1.8.0_121.
基于相當多的實驗,簡短的總結似乎是:
如果熱點可以堆棧分配對象實例
>所有用途都是內聯的
>永遠不會將其分配給任何靜態或對象字段,僅分配給局部變量
>在程序的每個點,哪些局部變量包含對象的引用必須是JIT時間可確定的,并且不依賴于任何不可預測的條件控制流.
>如果對象是數組,則其大小必須在JIT時知道,并且索引必須使用JIT時間常量.
要知道這些條件何時適用,您需要了解Hotspot的工作原理.由于涉及許多非本地因素,依賴于Hotspot在某種情況下確定堆棧分配可能是有風險的.特別是知道是否所有內容都很難預測.
實際上,如果你只是使用它們進行迭代,那么簡單的迭代器通常可以是棧可分配的.對于復合對象,只能對外層對象進行堆棧分配,因此列表和其他集合總是會導致堆分配.
如果你有一個HashMap< Integer,Something>并且你在myHashMap.get(42)中使用它,42可以在測試程序中堆棧分配,但它不會在完整的應用程序中,因為你可以確定在HashMaps中將有兩種以上的密鑰對象整個程序,因此鍵上的hashCode和equals方法不會內聯.
除此之外,我沒有看到任何普遍適用的規則,它將取決于代碼的細節.
熱點內部
第一個重要的事情是在內聯后執行轉義分析.這意味著Hotspot的轉義分析在這方面比Choi論文中的描述更強大,因為從方法返回但在調用方法本地的對象仍然可以進行堆棧分配.因此,如果您執行此操作,則迭代器幾乎總是可以進行堆棧分配. for(Foo item:myList){…}(myList.iterator()的實現很簡單,它們通常都是.)
Hotspot只有在確定方法“熱”時才編譯優化版本的方法,因此很多次運行的代碼根本沒有得到優化,在這種情況下,沒有堆棧分配或內聯.但對于那些你通常不在乎的方法.
內聯
內聯決策基于Hotspot首先收集的分析數據.聲明的類型并不重要,即使方法是虛擬的,Hotspot也可以根據它在分析期間看到的對象的類型來內聯它.類似的東西適用于分支(即if語句和其他控制流構造):如果在分析期間Hotspot從未看到某個分支被采用,它將基于從不采用分支的假設來編譯和優化代碼.在這兩種情況下,如果Hotspot無法證明其假設始終為真,則會在已編譯的代碼中插入檢查,稱為“不常見的陷阱”,如果遇到此類陷阱,Hotspot將進行去優化并可能重新優化新信息考慮在內.
Hotspot將分析哪些對象類型作為呼叫站點的接收者.如果Hotspot只看到一個類型或在調用站點只發現兩種不同的類型,則它能夠內聯調用的方法.如果只有一個或兩個非常常見的類型,而其他類型的出現頻率低得多,Hotspot還應該能夠內聯常見類型的方法,包括檢查它需要采取哪些代碼. (我不完全確定最后一種情況,有一兩種常見類型和更多不常見的類型).如果有兩種以上的常見類型,Hotspot根本不會內聯調用,而是生成間接調用的機器代碼.
這里的“類型”是指對象的確切類型.不考慮已實現的接口或共享超類.即使在調用站點出現不同的接收器類型,但它們都繼承了方法的相同實現(例如,所有從Object繼承hashCode的多個類),Hotspot仍將生成間接調用而不是內聯調用. (所以i.m.o.在這種情況下,熱點是非常愚蠢的.我希望未來版本能改進這一點.)
Hotspot也只會內聯不太大的方法. “不太大”由-XX確定:MaxInlineSize = n和-XX:FreqInlineSize = n選項. JVM字節碼大小低于MaxInlineSize的Inlinable方法總是內聯的,如果調用是“熱”,則內聯JVM字節碼大小低于FreqInlineSize的方法.更大的方法永遠不會內聯.默認情況下,MaxInlineSize是35并且FreqInlineSize是平臺相關的,但對我來說它是325.所以如果你想讓它們內聯,請確保你的方法不是太大.它有時可以幫助從大方法中分離出公共路徑,以便可以將其內聯到其調用者中.
剖析
關于性能分析的一個重要事項是,性能分析站點基于JVM字節碼,它本身不以任何方式內聯.所以如果你有例如靜態方法
static List map(List list, Function func) {
List result = new ArrayList();
for(T item : list) { result.add(func.call(item)); }
return result;
}
映射可以在列表上調用的SAM函數并返回轉換后的列表,Hotspot會將對func.call的調用視為單個程序范圍的調用站點.您可以在程序中的多個位置調用此地圖功能,在每個呼叫站點傳遞不同的功能(但對于一個呼叫站點則相同).在這種情況下,您可能希望Hotspot能夠內聯映射,然后調用func.call,因為在每次使用map時,只有一個func類型.如果是這樣的話,Hotspot將能夠非常緊密地優化循環.不幸的是,Hotspot對此并不夠聰明.它只為func.call調用站點保留一個配置文件,將所有傳遞給它的func類型集中在一起.您可能會使用兩個以上不同的func實現,因此Hotspot將無法內聯對func.call的調用. Link有更多細節,而archived link原來似乎已經不見了.
(另外,在Kotlin中,等效循環可以完全內聯,因為Kotlin編譯器可以在字節碼級別進行內聯調用.因此,對于某些用途,它可能比Java快得多.)
標量替換
另一個重要的事情是Hotspot實際上并沒有實現對象的堆棧分配.相反,它實現了標量替換,這意味著對象被解構為其組成字段,并且這些字段是像普通局部變量一樣分配的.這意味著根本沒有任何物體.標量替換僅在從不需要創建指向堆棧分配對象的指針時才有效.某些形式的堆棧分配在例如C或Go可以在堆棧上分配完整的對象,然后將引用或指針傳遞給它們到被調用的函數,但在Hotspot中這不起作用.因此,如果需要將對象引用傳遞給非內聯方法,即使引用不會轉義被調用的方法,Hotspot也將始終堆分配這樣的對象.
原則上,Hotspot可能更聰明,但現在卻不是.
測試程序
我使用以下程序和變體來查看Hotspot何時進行標量替換.
// Minimal example for which the JVM does not scalarize the allocation. If field is final, or the second allocation is unconditional, it will.
class Scalarization {
int field = 0xbd;
long foo(long i) { return i * field; }
public static void main(String[] args) {
long result = 0;
for(long i=0; i<100; i++) {
result += test();
}
System.out.println("Result: "+result);
}
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 0) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
}
如果您使用javac Scalarization.java編譯并運行此程序; java -verbose:gc Scalarization你可以看到標量替換是否由垃圾收集的數量起作用.如果標量替換工作,我的系統上沒有發生垃圾收集,如果標量替換不起作用,我會看到一些垃圾收集.
Hotspot能夠進行scalarize的變種運行速度明顯快于不能運行的變種.我驗證了生成的機器代碼(instructions),以確保Hotspot沒有進行任何意外的優化.如果熱點能夠標量替換分配,那么它還可以在循環上進行一些額外的優化,展開幾次迭代,然后將這些迭代組合在一起.因此,在scalarized版本中,每個迭代器執行多個源代碼級迭代的工作時,有效循環計數較低.因此速度差異不僅僅是由于分配和垃圾收集開銷.
意見
我嘗試了上述程序的一些變體.標量替換的一個條件是對象絕不能分配給對象(或靜態)字段,并且可能也不會分配給數組.所以在代碼中
Foo f = new Foo();
bar.field = f;
Foo對象不能被標量替換.即使條本身被標量替換,并且如果你再也不使用bar.field,這就成立了.因此,只能將對象分配給局部變量.
僅憑這一點還不夠,Hotspot還必須能夠在JIT時間靜態地確定哪個對象實例將成為呼叫的目標.例如,使用以下foo實現以及test和remove字段會導致堆分配:
long foo(long i) { return i * 0xbb; }
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 50) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
如果然后刪除第二個賦值的條件,則不再發生堆分配:
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
在這種情況下,Hotspot可以靜態地確定哪個實例是每次調用s.foo的目標.
另一方面,即使s的第二個賦值是Scalarization的子類,具有完全不同的實現,只要賦值是無條件的,Hotspot仍然會對分配進行scalarize.
Hotspot似乎無法將對象移動到之前被標量替換的堆中(至少在沒有去優化的情況下).標量替換是一種全有或全無的事情.因此在原始測試方法中,Scalarization的兩個分配總是發生在堆上.
條件語句
一個重要的細節是Hotspot將根據其分析數據預測條件.如果從未執行條件賦值,Hotspot將根據該假設編譯代碼,然后可能能夠進行標量替換.如果在稍后的時間點確實采取了條件,Hotspot將需要使用這個新假設重新編譯代碼.新代碼不會進行標量替換,因為Hotspot無法再靜態地確定以下調用的接收器實例.
例如,在這個測試變體中:
static long limit = 0;
static long test() {
long ctr = 0x5;
long i = limit;
limit += 0x10000;
for(; i
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 0xf9a0) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
條件賦權僅在程序的生命周期內執行一次.如果此分配發生得足夠早,在Hotspot開始對測試方法進行完整分析之前,Hotspot從不會注意到所采用的條件并編譯執行標量替換的代碼.如果在采取條件時已經開始進行分析,則Hotspot將不會進行標量替換.使用0xf9a0的測試值,標量替換是否發生在我的計算機上是不確定的,因為完全在分析開始時可能會有所不同(例如,因為分析和優化的代碼是在后臺線程上編譯的).因此,如果我運行上述變體,它有時會執行一些垃圾收集,有時則不會.
Hotspot的靜態代碼分析比C/C++和其他靜態編譯器可以做的更加有限,因此Hotspot在通過幾個條件和其他控制結構來跟蹤方法中的控制流以確定變量引用的實例時并不聰明即使它對程序員或更智能的編譯器是靜態可確定的.在許多情況下,分析信息將彌補這一點,但需要注意的是.
數組
如果在JIT時間知道它們的大小,則可以分配堆棧.但是,除非Hotspot還能在JIT時間靜態地確定索引值,否則不支持索引到數組中.所以堆棧分配的數組是沒用的.由于大多數程序不直接使用數組而是使用標準集合,因此這不是非常相關,因為嵌入對象(例如包含ArrayList中的數據的數組)由于其嵌入式而需要進行堆分配.我認為這種限制的原因是對局部變量不存在索引操作,因此這需要額外的代碼生成功能來處理非常罕見的用例.
總結
以上是生活随笔為你收集整理的java让对象分配在栈上_java – Hotspot何时可以在堆栈上分配对象?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天山乌梅的功效与作用、禁忌和食用方法
- 下一篇: 鹅肉汤的功效与作用、禁忌和食用方法