在說java的對象分配內存所在位置前,咱們先來看看C++的對象分配是怎樣的。 C++實例化對象的方式有兩種:java
#include <iostream>
using namespace std;
class ClassA {
private:
int arg;
public:
ClassA(int a): arg(a) {
cout << "ClassA(" << arg << ")" << endl;
}
~ClassA(){
cout << "~ClassA(" << arg << ")" << endl;
}
};
int main() {
ClassA ca1(1); //直接定義對象
ClassA* ca2 = new ClassA(2); //使用new關鍵字
return 0;
}
複製代碼
輸出結果:ios
ClassA(1)
ClassA(2)
~ClassA(1)
複製代碼
直接定義對象的方式會將對象內存分配在棧上,所以main函數退出後會執行ClassA的虛構函數,該對象被回收。而使用new實例化的對象內存分配在堆上,對象在main函數退出後不會執行虛構函數。
C++中,內存能夠被分配到棧上或者堆內存中。
那麼java是否也是這樣呢,若是java在必要的時候也是把對象分配到棧上,從而自動銷燬對象,那必然能減小一些垃圾回收的開銷(java的垃圾回收須要進行標記整理等一系列耗時操做),同時也能提升執行效率(棧上存儲的數據有很大的機率會被虛擬機分配至物理機器的高速寄存器中存儲)。雖然,這些細節都是針對JVM而言的,對於開發者而言彷佛不太須要關心。
然而,我仍是很好奇。算法
寫一段不怎麼靠譜的代碼來觀察Java的輸出結果:數組
public class ClassA{
public int arg;
public ClassA(int arg) {
this.arg = arg;
}
@Override
protected void finalize() throws Throwable {
System.out.println("對象即將被銷燬: " + this + "; arg = " + arg);
super.finalize();
}
}
public class TestCase1 {
public static ClassA getClassA(int arg) {
ClassA a = new ClassA(arg);
System.out.println("getA() 方法內:" + a);
return a;
}
public static void foo() {
ClassA a = new ClassA(2);
System.out.println("foo() 方法內:" + a);
}
public static void main(String[] args) {
ClassA classA = getClassA(1);
System.out.println("main() 方法內:" + classA);
foo();
}
}
複製代碼
輸出結果:bash
getA() 方法內:com.rhythm7.A@29453f44
main() 方法內:com.rhythm7.A@29453f44
foo() 方法內:com.rhythm7.A@5cad8086
複製代碼
執行完getA()方法後,getA()方法內實例化的classA對象實例a被返回並賦值給main方法內的classA。 接着執行foo()方法,方法內部實例化一個classA對象,但只是輸出其HashCode,沒有返回其對象。
結果是兩個對象都沒有執行finalize()方法。
若是咱們強制使用System.gc()
來通知系統進行垃圾回收,結果如何?ide
public static void main(String[] args) {
A a = getA(1);
System.out.println("main() 方法內:" + a);
foo();
System.gc();
}
複製代碼
輸出結果函數
getA() 方法內:com.rhythm7.A@29453f44
main() 方法內:com.rhythm7.A@29453f44
foo() 方法內:com.rhythm7.A@5cad8086
對象即將被銷燬: com.rhythm7.A@5cad8086; arg = 2
複製代碼
這說明,須要通知垃圾回收器進行進行垃圾回收才能回收方法foo()內實例化的對象。 因此,能夠確定foo()內實例化的對象不會跟隨foo()方法的出棧而銷燬,也就是foo()方法內實例化的局部對象不會是分配在棧上的。性能
查閱相關資料,發現JVM的確存在一個 「逃逸分析」 的概念。
內容大概以下:
逃逸分析是目前Java虛擬機中比較前沿的優化技術,它並非直接優化代碼的手段,而是爲其餘優化手段提供依據的分析技術。
逃逸分析的主要做用就是分析對象做用域。
當一個對象在方法中被定義後,它可能被外部方法所引用,例如做爲調用參數傳遞到其餘方法中,這種行爲就叫作 方法逃逸。甚至該對象還可能被外部線程訪問到,例如賦值被類變量或能夠在其餘線程中訪問的實例變量,稱爲 線程逃逸。
經過逃逸分析技術能夠判斷一個對象不會逃逸到方法或者線程以外。根據這一特色,就可讓這個對象在棧上分配內存,對象所佔用的內存空間就能夠隨幀棧出棧而銷燬。在通常應用中,不會逃逸的局部對象所佔比例很大,若是能使用棧上分配,那麼大量的對象就會隨着方法的結束而自動銷燬了,垃圾收集系統的壓力就會小不少。
除此以外,逃逸分析的做用還包括 標量替換 和 同步消除 ;
標量替換 指:若一個對象被證實不會被外部訪問,而且這個對象能夠被拆解成若干個基本類型的形式,那麼當程序真正執行的時候能夠不建立這個對象,而是採用直接建立它的若干個被這個方法所使用到的成員變量來代替,將對象拆分後,除了可讓對象的成員變量在棧上分配和讀寫以外,還能夠爲後續進一步的優化手段創造條件。
同步消除 指:若一個變量被證實不會逃逸出線程,那麼這個變量的讀寫就確定不會出現競爭的狀況,那麼對這個變量實施的同步措施也就能夠消除掉。
說了逃逸分析的這些做用,那麼Java虛擬機是否有對對象作逃逸分析呢?優化
答案是否。ui
關於逃逸分析的論文在1999年就已經發表,但直到Sun JDK 1.6才實現了逃逸分析,並且直到如今這項優化還沒有足夠成熟,仍有很大的改進餘地。不成熟的緣由主要是不能保證逃逸分析的性能收益一定高於它的消耗。由於逃逸分析自己就是一個高耗時的過程,假如分析的結果是沒有幾個不逃逸的對象,那麼這個分析所花費時候比優化所減小的時間更長,這是得不償失的。
因此目前虛擬機只能採用不那麼準確,但時間壓力相對較小的算法來完成逃逸分析。還有一點是,基於逃逸分析的一些優化手段,如上面提到的「棧上分配」,因爲HotSpot虛擬機目前的實現方式致使棧上分配實現起來比較複雜,所以在HotSpot中暫時尚未作這項優化。
事實上,在java虛擬機中,有一句話是這麼寫的:
The heap is the runtime data area from which memory for all class instances and arrays is allocated。
堆是全部的對象實例以及數組分配內存的運行時數據區域。
因此,忘掉Java棧上分配對象內存的想法吧,至少在目前的HotSpot中是不存在的。也就是說Java的對象分配只在堆上。
PS: 若是有須要,而且確認對程序運行有益,用戶可使用參數-XX:+DoEscapeAnalysis來手動開啓逃逸分析,開啓以後能夠經過參數-XX:+PrintEscapeAnalysis來查看分析結果。