2025-02-15

第一次討論這個問題是我一邊寫activex控件和java applet程序,老孟提到瞭這個問題,直覺告訴我java的運行速度遠快於c。
如果不信的話,你可裝一下vc6和vj6,寫兩個功能相同的程序,運行一下,你會感覺到從運行速度和編譯速度上,vj不遜於vc,WFC好於MFC。
大傢會說java的IO處理不如c,這也是扯淡。我做過長時間的測試,同樣的Linux 2.6服務器apache和resin的吞吐量基本相同,沒有性能的差異,nginx例外,不是人人都可以寫出nginx。mustang也使用 epoll技術,java唯一沒有作到就是沒有辦法直接從linux內核取得數據.還是需要把數據從內核空間復制到用戶空間。
java的對象池設計是個失誤,我在sohu經常看見類似的代碼,新建一個對象,然後放到memcache中,美其名曰,我用的時候,再從memcache取,殊不知,連接一次memcache,取到對象,再進行反序列化,恐怕上百個cpu指令都不夠,新建一個java對象,不過10個cpu指令,而且這個對象啥時候釋放,也是一個問題,不如新建一個單體,反復使用效率高。
以下是其他網站的引用
現代 JVM 中的分配比執行得最好的 malloc 實現還要快得多。HotSpot 1.4.2 之後虛擬機中的 new Object() 常見代碼路徑最多 10 條機器指令(Sun 提供的數據;請參閱 參考資料),而用 C 語言實現的執行得最好的 malloc 實現,每個調用平均要求的指令在 60 到 100 條之間(Detlefs 等;請參閱 參考資料)。而且分配性能在整體性能中不是一個微不足道的部分,測評顯示:對於許多實際的 C 和 C 程序(例如 Perl 和 Ghostscript),整體執行時間中的 20% 到 30% 都花在 malloc 和 free 上。如果不信,找段c程序看一看,很多語句就是malloc和free。
這條“聽起來有理的意見” (以大批量清理垃圾要比一天到晚一點點兒清理垃圾更容易)得到瞭數據的證實。一項研究(Zorn; 請參閱 參考資料)測量瞭在許多常見 C 應用程序中,用保守的 Boehm-Demers-Weiser(BDW)替換 malloc 的效果,結果是:許多程序在采用垃圾收集而不是傳統的分配器運行時,表現出瞭速度提升。(BDW 是個保守的、不移動的垃圾收集器,嚴重地限制瞭對分配和回收進行優化的能力,也限制瞭改善內存位置的能力;像 JVM 中使用的那些精確的浮動收集器可以做得更好。)

在 JVM 中的分配並不總是這麼快,早期 JVM 的分配和垃圾收集性能實際上很差,這當然就是 JVM 分配慢這一說法的起源。在非常早的時候,我們看到過許多“分配慢”的意見 —— 因為就像早期 JVM 中的一切一樣,它確實慢 —— 而性能顧問提供瞭許多避免分配的技巧,例如對象池。(公共服務聲明:除瞭對最重量的對象之外,對象池現在對於所有對象都是嚴重的性能損失,而且要在不造成並發瓶頸的情況下使用對象池也很需要技巧。)但是,從 JDK 1.0 開始已經發生瞭許多變化;JDK 1.2 中引入的分代收集器(generational collector)支持簡單得多的分配方式,可以極大地提高性能。

分代垃圾收集

分代垃圾收集器把堆分成多代;多數 JVM 使用兩代,“年輕代”和“年老代”。對象在年輕代中分配;如果它們在一定數量的垃圾收集之後仍然存在,就被當作是”長壽的“,並晉升到年老代。

HotSpot 提供瞭使用三個年輕代收集器的選擇(串行拷貝、並行拷貝和並行清理),它們都采用“拷貝”收集器的形式,有幾個重要的公共特征。拷貝收集器把內存空間從中間分成兩半,每次隻使用一半。開始時,使用中的一半構成瞭可用內存的一個大塊;分配器滿足分配請求時,返回它沒有使用的空間的前 N 個字節,並把指針(分隔“使用”部分)從“自由”部分移動過來,如清單 1 的偽代碼所示。當使用的那一半用滿時,垃圾收集器把所有活動對象(不是垃圾的那些對象)拷貝到另一半的底部(把堆壓縮成連續的),然後從另一半開始分配。

清單 1. 在存在拷貝收集器的情況下,分配器的行為

void *malloc(int n) {
if (heapTop – heapStart doGarbageCollection();

void *wasStart = heapStart;
heapStart = n;
return wasStart;
}

從這個偽代碼可以看出為什麼拷貝收集器可以實現這麼快的分配 —— 分配新對象隻是檢查在堆中是否還有足夠的剩餘空間,如果還有,就移動指針。不需要搜索自由列表、最佳匹配、第一匹配、lookaside 列表 ,隻要從堆中取出前 N 個字節,就成功瞭。

如何回收?

但是分配僅僅是內存管理的一半,回收是另一半。對於多數對象來說,直接垃圾收集的成本為零。這是因為,拷貝收集器不需要訪問或拷貝死對象,隻處理活動對象。所以在分配之後很快就變成垃圾的對象,不會造成收集周期的工作量。

在典型的面向對象程序中,絕大多數對象(根據不同的研究,在 92% 到 98% 之間)“死於年輕”,這意味著它們在分配之後,通常在下一次垃圾收集之前,很快就變成垃圾。(這個屬性叫作 分代假設,對於許多面向對象語言已經得到實際測試,證明為真。)所以,不僅分配要快,對於多數對象來說,回收也要自由。

線程本地分配

如果分配器完全像 清單 1 所示的那樣實現,那麼共享的 heapStart 字段會迅速變成顯著的並發瓶頸,因為每個分配都要取得保護這個字段的鎖。為瞭避免這個問題,多數 JVM 采用瞭 線程本地分配塊,這時每個線程都從堆中分配一個更大的內存塊,然後順序地用這個線程本地塊為小的分配請求提供服務。所以,線程花在獲得共享堆鎖的大量時間被大大減少,從而提高瞭並發性。(在傳統的 malloc 實現的情況下要解決這個問題更困難,成本更高;把線程支持和垃圾收集都構建進平臺促進瞭這類協作。)

 
堆棧分配

C 向程序員提供瞭在堆或堆棧中分配對象的選擇。基於堆棧的分配更有效:分配更便宜,回收成本真正為零,而且語言提供瞭隔離對象生命周期的幫助,減少瞭忘記釋放對象的風險。另一方面,在 C 中,在發佈或共享基於堆棧的對象的引用時,必須非常小心,因為在堆棧幀整理時,基於堆棧的對象會被自動釋放,從而造成孤懸的指針。

基於堆棧的分配的另一個優勢是它對高速緩存更加友好。在現代的處理器上,緩存遺漏的成本非常顯著,所以如果語言和運行時能夠幫助程序實現更好的數據位置,就會提高性能。堆棧的頂部通常在高速緩存中是“熱”的,而堆的頂部通常是“冷”的(因為從這部分內存使用之後可能過瞭很長時間)。所以,在堆上分配對象,比起在堆棧上分配對象,會帶來更多緩存遺漏。

更糟的是,在堆上分配對象時,緩存遺漏還有一個特別討厭的內存交互。在從堆中分配內存時,不管上次使用內存之後留下瞭什麼內容,內存中的內容都被當作垃圾。如果在堆的頂部分配的內存塊不在緩存中,執行會在內存內容裝入緩存的過程中出現延遲。然後,還要用 0 或其他初始值覆蓋掉剛剛費時費力裝入緩存的那些值,從而造成大量內存活動的浪費。(有些處理器,例如 Azul 的 Vega,包含加速堆分配的硬件支持。)

escape 分析

Java 語句沒有提供任何明確地在堆棧上分配對象的方式,但是這個事實並不影響 JVM 仍然可以在適當的地方使用堆棧分配。JVM 可以使用叫作 escape 分析 的技術,通過這項技術,JVM 可以發現某些對象在它們的整個生命周期中都限制在單一線程內,還會發現這個生命周期綁定到指定堆棧幀的生命周期上。這樣的對象可以安全地在堆棧上而不是在堆上分配。更好的是,對於小型對象,JVM 可以把分配工作完全優化掉,隻把對象的字段放入寄存器。

清單 2 顯示瞭一個可以用 escape 分析把堆分配優化掉的示例。Component.getLocation() 方法對組件的位置做瞭一個保護性的拷貝,這樣調用者就無法在不經意間改變組件的實際位置。先調用 getDistanceFrom() 得到另一個組件的位置,其中包括對象的分配,然後用 getLocation() 返回的 Point 的 x 和 y 字段計算兩個組件之間的距離。

清單 2. 返回復合值的典型的保護性拷貝方式

public class Point {
private int x, y;
public Point(int x, int y) {
this.x = x; this.y = y;
}
public Point(Point p) { this(p.x, p.y); }
public int getX() { return x; }
public int getY() { return y; }
}

public class Component {
private Point location;
public Point getLocation() { return new Point(location); }

public double getDistanceFrom(Component other) {
Point otherLocation = other.getLocation();
int deltaX = otherLocation.getX() – location.getX();
int deltaY = otherLocation.getY() – location.getY();
return Math.sqrt(deltaX*deltaX deltaY*deltaY);
}
}
getLocation() 方法不知道它的調用者要如何處理它返回的 Point;有可能得到一個指向 Point 的引用,比如把它放在集合中,所以 getLocation() 采用瞭保護性的編碼方式。但是,在這個示例中,getDistanceFrom() 並不會這麼做,它隻會使用 Point 很短的時間,然後釋放它,這看起來像是對完美對象的浪費。

聰明的 JVM 會看出將要進行的工作,並把保護性拷貝的分配優化掉。首先,對 getLocation() 的調用會變成內聯的,對 getX() 和 getY() 的調用也同樣處理,從而導致 getDistanceFrom() 的表現會像清單 3 一樣有效。

清單 3. 偽代碼描述瞭把內聯優化應用到 getDistanceFrom() 的結果

public double getDistanceFrom(Component other) {
Point otherLocation = new Point(other.x, other.y);
int deltaX = otherLocation.x – location.x;
int deltaY = otherLocation.y – location.y;
return Math.sqrt(deltaX*deltaX deltaY*deltaY);
}

在這一點上,escape 分析可以顯示在第一行分配的對象永遠不會脫離它的基本塊,而 getDistanceFrom() 也永遠不會修改 other 組件的狀態。(escape 指的是對象引用沒有保存到堆中,或者傳遞給可能保留一份拷貝的未知代

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *