Java語言的執行效率一直被C、C++程序員所嘲笑,其實,事實就是這樣,Java在執行效率方面確實很低,一方面,Java語言採用面向對象思想,這也決定了其必然是開發效率高,執行效率低。另外一方面,Java語言對程序員作了一個美好的承諾:程序員無需去管理內存,由於JVM有垃圾回收(GC),會去自動進行垃圾回收。 其實否則: 一、垃圾回收並不會按照程序員的要求,隨時進行GC。 二、垃圾回收並不會及時的清理內存,儘管有時程序須要額外的內存。 三、程序員不能對垃圾回收進行控制。 1、JVM內存的構成 整體分爲下面幾個部分: 程序計數器(Program Counter Register)、JVM虛擬機棧(JVM Stacks)、本地方法棧(Native Method Stacks)、堆(Heap)、方法區(Method Area)
一、程序計數器(Program Counter Register) 這是一塊比較小的內存,不在Ram上,而是直接劃分在CPU上的,程序員沒法直接操做它,它的做用是:JVM在解釋字節碼文件(.class)時,存儲當前線程所執行的字節碼的行號,只是一種概念模型,各類JVM所採用的方式不一樣,字節碼解釋器工做時,就是經過改變程序計數器的值來選取下一條要執行的指令,分支、循環、跳轉、等基礎功能都是依賴此技術區完成的。還有一種狀況,就是咱們常說的Java多線程方面的,多線程就是經過線程輪流切換而達到的,同一時刻,一個內核只能執行一個指令,因此,對於每個程序來講,必須有一個計數器來記錄程序的執行進度,這樣,當線程恢復執行的時候,才能從正確的地方開始,因此,每一個線程都必須有一個獨立的程序計數器,這類計數器爲線程私有的內存。若是一個線程正在執行一個Java方法,則計數器記錄的是字節碼的指令的地址,若是執行的一個Native方法,則計數器的記錄爲空,此內存區是惟一一個在Java規範中沒有任何OutOfMemoryError狀況的區域。 二、JVM虛擬機棧(JVM Stacks) JVM虛擬機棧就是咱們常說的堆棧的棧(咱們經常把內存粗略分爲堆和棧),和程序計數器同樣,也是線程私有的,生命週期和線程同樣,每一個方法被執行的時候會產生一個棧幀,用於存儲局部變量表、動態連接、操做數、方法出口等信息。方法的執行過程就是棧幀在JVM中出棧和入棧的過程。局部變量表中存放的是各類基本數據類型,如boolean、byte、char、等8種,及引用類型(存放的是指向各個對象的內存地址),所以,它有一個特色:內存空間能夠在編譯期間就肯定,運行期再也不改變。這個內存區域會有兩種可能的Java異常:StackOverFlowError和OutOfMemoryError。 三、本地方法棧(Native Method Stacks) 從名字便可看出,本地方法棧就是用來處理Java中的本地方法的,Java類的祖先類Object中有衆多Native方法,如hashCode()、wait()等,他們的執行不少時候是藉助於操做系統,可是JVM須要對他們作一些規範,來處理他們的執行過程。此區域,能夠有不一樣的實現方法,向咱們經常使用的Sun的JVM就是本地方法棧和JVM虛擬機棧是同一個。 四、堆(Heap) 堆內存是內存中最重要的一塊,也是最有必要進行深究的一部分。由於Java性能的優化,主要就是針對這部份內存的。全部的對象實例及數組都是在堆上面分配的(隨着JIT技術的逐漸成熟,這句話視乎有些絕對,不過至少目前還基本是這樣的),可經過-Xmx和-Xms來控制堆的大小。JIT技術的發展產生了新的技術,如棧上分配和標量替換,也許在不久的幾年裏,即時編譯會誕生及成熟,那個時候,「全部的對象實例及數組都是在堆上面分配的」這句話就應該稍微改改了。堆內存是垃圾回收的主要區域。在32位系統上最大爲2G,64位系統上無限制。可經過-Xms和-Xmx控制,-Xms爲JVM啓動時申請的最小Heap內存,-Xmx爲JVM可申請的最大Heap內存。 五、方法區(Method Area) 方法區是全部線程共享的內存區域,用於存儲已經被JVM加載的類信息、常量、靜態變量等數據,通常來講,方法區屬於持久代,也難怪Java規範將方法區描述爲堆的一個邏輯部分,可是它不是堆。方法區的垃圾回收比較棘手,就算是Sun的HotSpot VM在這方面也沒有作得多麼完美。此處引入方法區中一個重要的概念:運行時常量池。主要用於存放在編譯過程當中產生的字面量(字面量簡單理解就是常量)和引用。通常狀況,常量的內存分配在編譯期間就能肯定,但不必定全是,有一些可能就是運行時也可將常量放入常量池中,如String類中有個Native方法intern() 此處補充一個在JVM內存管理以外的一個內存區:直接內存。在JDK1.4中新加入類NIO類,引入了一種基於通道與緩衝區的I/O方式,它可使用Native函數庫直接分配堆外內存,即咱們所說的直接內存,這樣在某些場景中會提升程序的性能。 2、垃圾回收 做爲Java程序員咱們很難去控制JVM的內存回收,只能根據它的原理去適應,儘可能提升程序的性能。下面開始講解Java垃圾回收,即Garbage Collection,GC。從如下四個方面進行: 一、爲何要進行垃圾回收? 隨着程序的運行,內存中存在的實例對象、變量等信息佔據的內存愈來愈多,若是不及時進行垃圾回收,必然會帶來程序性能的降低,甚至會由於可用內存不足形成一些沒必要要的系統異常。 二、哪些「垃圾」須要回收? 在咱們上面介紹的五大區中,有三個是不須要進行垃圾回收的:程序計數器、JVM棧、本地方法棧。由於它們的生命週期是和線程同步的,隨着線程的銷燬,它們佔用的內存會自動釋放,因此只有方法區和堆須要進行GC。具體到哪些對象的話,簡單概況一句話:若是某個對象已經不存在任何引用,那麼它能夠被回收。 三、何時進行垃圾回收? 基本思想就是:從一個叫GC Roots的對象開始,向下搜索,若是一個對象不能到達GC Roots對象的時候,說明它已經再也不被引用,便可被進行垃圾回收(此處 暫且這樣理解,其實事實還有一些不一樣,當一個對象再也不被引用時,並無徹底「死亡」,若是類重寫了finalize()方法,且沒有被系統調用過,那麼系統會調用一次finalize()方法,以完成最後的工做,在這期間,若是能夠將對象從新與任何一個和GC Roots有引用的對象相關聯,則該對象能夠「重生」,若是不能夠,那麼就說明完全能夠被回收了),如上圖中的Object五、Object六、Object7,雖然它們3個依然可能相互引用,可是整體來講,它們已經沒有做用了,這樣就解決了引用計數算法沒法解決的問題。 補充引用的概念:JDK 1.2以後,對引用進行了擴充,引入了強、軟、若、虛四種引用,被標記爲這四種引用的對象,在GC時分別有不一樣的意義: a> 強引用(Strong Reference).就是爲剛被new出來的對象所加的引用,它的特色就是,永遠不會被回收。 b> 軟引用(Soft Reference).聲明爲軟引用的類,是可被回收的對象,若是JVM內存並不緊張,這類對象能夠不被回收,若是內存緊張,則會被回收。此處有一個問題,既然被引用爲軟引用的對象能夠回收,爲何不去回收呢?其實咱們知道,Java中是存在緩存機制的,就拿字面量緩存來講,有些時候,緩存的對象就是當前無關緊要的,只是留在內存中若是還有須要,則不須要從新分配內存便可使用,所以,這些對象便可被引用爲軟引用,方便使用,提升程序性能。 c> 弱引用(Weak Reference).弱引用的對象就是必定須要進行垃圾回收的,無論內存是否緊張,當進行GC時,標記爲弱引用的對象必定會被清理回收。 d> 虛引用(Phantom Reference).虛引用弱的能夠忽略不計,JVM徹底不會在意虛引用,其惟一做用就是作一些跟蹤記錄,輔助finalize函數的使用。 最後總結,什麼樣的類須要回收呢?無用的類,何爲無用的類?需知足以下要求: 1> 該類的全部實例對象都已經被回收。 2> 加載該類的ClassLoader已經被回收。 3> 該類對應的反射類java.lang.Class對象沒有被任何地方引用。 四、如何進行垃圾回收? 本塊內容以介紹垃圾回收算法爲主,由於咱們前面有介紹,內存主要被分爲三塊,新生代、舊生代、持久代。三代的特色不一樣,造就了他們所用的GC算法不一樣,新生代適合那些生命週期較短,頻繁建立及銷燬的對象,舊生代適合生命週期相對較長的對象,持久代在Sun HotSpot中就是指方法區(有些JVM中根本就沒有持久代這中說法)。首先介紹下新生代、舊生代、持久代的概念及特色: 新生代:New Generation或者Young Generation。上面大體分爲Eden區和Survivor區,Survivor區又分爲大小相同的兩部分:FromSpace 和ToSpace。新建的對象都是用新生代分配內存,Eden空間不足的時候,會把存活的對象轉移到Survivor中,新生代的大小能夠由-Xmn來控制,也能夠用-XX:SurvivorRatio來控制Eden和Survivor的比例. 舊生代:Old Generation。用於存放新生代中通過屢次垃圾回收仍然存活的對象,例如緩存對象。舊生代佔用大小爲-Xmx值減去-Xmn對應的值。 持久代:Permanent Generation。在Sun的JVM中就是方法區的意思,儘管有些JVM大多沒有這一代。主要存放常量及類的一些信息默認最小值爲16MB,最大值爲64MB,可經過-XX:PermSize及-XX:MaxPermSize來設置最小值和最大值。 常見的GC算法: 標記-清除算法(Mark-Sweep) 最基礎的GC算法,將須要進行回收的對象作標記,以後掃描,有標記的進行回收,這樣就產生兩個步驟:標記和清除。這個算法效率不高,並且在清理完成後會產生內存碎片,這樣,若是有大對象須要連續的內存空間時,還須要進行碎片整理,因此,此算法須要改進。 複製算法(Copying) 前面咱們談過,新生代內存分爲了三份,Eden區和2塊Survivor區,通常Sun的JVM會將Eden區和Survivor區的比例調爲8:1,保證有一塊Survivor區是空閒的,這樣,在垃圾回收的時候,將不須要進行回收的對象放在空閒的Survivor區,而後將Eden區和第一塊Survivor區進行徹底清理,這樣有一個問題,就是若是第二塊Survivor區的空間不夠大怎麼辦?這個時候,就須要當Survivor區不夠用的時候,暫時借持久代的內存用一下。此算法適用於新生代。 標記-整理(或叫壓縮)算法(Mark-Compact) 和標記-清楚算法前半段同樣,只是在標記了不須要進行回收的對象後,將標記過的對象移動到一塊兒,使得內存連續,這樣,只要將標記邊界之外的內存清理就好了。此算法適用於持久代。 常見的垃圾收集器: 根據上面說的諸多算法,天天JVM都有不一樣的實現,咱們先來看看常見的一些垃圾收集器: 首先介紹三種實際的垃圾回收器:串行GC(SerialGC)、並行回收GC(Parallel Scavenge)和並行GC(ParNew)。 一、Serial GC。是最基本、最古老的收集器,可是如今依然被普遍使用,是一種單線程垃圾回收機制,並且不只如此,它最大的特色就是在進行垃圾回收的時候,須要將全部正在執行的線程暫停(Stop The World),對於有些應用這是難以接受的,可是咱們能夠這樣想,只要咱們可以作到將它所停頓的時間控制在N個毫秒範圍內,大多數應用咱們仍是能夠接受的,並且事實是它並無讓咱們失望,幾十毫米的停頓咱們做爲客戶機(Client)是徹底能夠接受的,該收集器適用於單CPU、新生代空間較小及對暫停時間要求不是很是高的應用上,是client級別默認的GC方式,能夠經過-XX:+UseSerialGC來強制指定。 二、ParNew GC。基本和Serial GC同樣,但本質區別是加入了多線程機制,提升了效率,這樣它就能夠被用在服務器端(Server)上,同時它能夠與CMS GC配合,因此,更加有理由將它置於Server端。 三、Parallel Scavenge GC。在整個掃描和複製過程採用多線程的方式來進行,適用於多CPU、對暫停時間要求較短的應用上,是server級別默認採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數。如下給出幾組使用組合: 四、CMS (Concurrent Mark Sweep)收集器。該收集器目標就是解決Serial GC 的停頓問題,以達到最短回收時間。常見的B/S架構的應用就適合用這種收集器,由於其高併發、高響應的特色。CMS收集器是基於「標記-清除」算法實現的,整個收集過程大體分爲4個步驟: 初始標記(CMS initial mark)、併發標記(CMS concurrenr mark)、從新標記(CMS remark)、併發清除(CMS concurrent sweep)。 其中初始標記、從新標記這兩個步驟任然須要停頓其餘用戶線程。初始標記僅僅只是標記出GC ROOTS能直接關聯到的對象,速度很快,併發標記階段是進行GC ROOTS 根搜索算法階段,會斷定對象是否存活。而從新標記階段則是爲了修正併發標記期間,因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間會被初始標記階段稍長,但比並發標記階段要短。因爲整個過程當中耗時最長的併發標記和併發清除過程當中,收集器線程均可以與用戶線程一塊兒工做,因此總體來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。 CMS收集器的優勢:併發收集、低停頓,可是CMS還遠遠達不到完美。 CMS收集器主要有三個顯著缺點: a>.CMS收集器對CPU資源很是敏感。在併發階段,雖然不會致使用戶線程停頓,可是會佔用CPU資源而致使引用程序變慢,總吞吐量降低。CMS默認啓動的回收線程數是:(CPU數量+3) / 4。 b>.CMS收集器沒法處理浮動垃圾,可能出現「Concurrent Mode Failure「,失敗後而致使另外一次Full GC的產生。因爲CMS併發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱爲「浮動垃圾」。也是因爲在垃圾收集階段用戶線程還須要運行,即須要預留足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部份內存空間提供併發收集時的程序運做使用。在默認設置下,CMS收集器在老年代使用了68%的空間時就會被激活,也能夠經過參數-XX:CMSInitiatingOccupancyFraction的值來提供觸發百分比,以下降內存回收次數提升性能。要是CMS運行期間預留的內存沒法知足程序其餘線程須要,就會出現「Concurrent Mode Failure」失敗,這時候虛擬機將啓動後備預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。因此說參數-XX:CMSInitiatingOccupancyFraction設置的太高將會很容易致使「Concurrent Mode Failure」失敗,性能反而下降。 c>.最後一個缺點,CMS是基於「標記-清除」算法實現的收集器,使用「標記-清除」算法收集後,會產生大量碎片。空間碎片太多時,將會給對象分配帶來不少麻煩,好比說大對象,內存空間找不到連續的空間來分配不得不提早觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用於在Full GC以後增長一個碎片整理過程,還可經過-XX:CMSFullGCBeforeCompaction參數設置執行多少次不壓縮的Full GC以後,跟着來一次碎片整理過程。 五、G1收集器。相比CMS收集器有很多改進,首先基於標記-整理算法,不會產生內存碎片問題,其次,能夠比較精確的控制停頓,此處再也不詳細介紹。 六、Serial Old。Serial Old是Serial收集器的老年代版本,它一樣使用一個單線程執行收集,使用「標記-整理」算法。主要使用在Client模式下的虛擬機。 七、Parallel Old。Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。 八、RTSJ垃圾收集器,用於Java實時編程,後續會補充介紹。 3、Java程序性能優化 gc()的調用 調用gc 方法暗示着Java 虛擬機作了一些努力來回收未用對象,以便可以快速地重用這些對象當前佔用的內存。當控制權從方法調用中返回時,虛擬機已經盡最大努力從全部丟棄的對象中回收了空間,調用System.gc() 等效於調用Runtime.getRuntime().gc()。 finalize()的調用及重寫 gc 只能清除在堆上分配的內存(純java語言的全部對象都在堆上使用new分配內存),而不能清除棧上分配的內存(當使用JNI技術時,可能會在棧上分配內存,例如java調用c程序,而該c程序使用malloc分配內存時)。所以,若是某些對象被分配了棧上的內存區域,那gc就管不着了,對棧上的對象進行內存回收就要靠finalize()。舉個例子來講,當java 調用非java方法時(這種方法多是c或是c++的),在非java代碼內部也許調用了c的malloc()函數來分配內存,並且除非調用那個了 free() 不然不會釋放內存(由於free()是c的函數),這個時候要進行釋放內存的工做,gc是不起做用的,於是須要在finalize()內部的一個固有方法調用free()。 優秀的編程習慣 (1)避免在循環體中建立對象,即便該對象佔用內存空間不大。 (2)儘可能及時使對象符合垃圾回收標準。 (3)不要採用過深的繼承層次。 (4)訪問本地變量優於訪問類中的變量。 本版塊會不斷更新! 4、常見問題 一、內存溢出 就是你要求分配的java虛擬機內存超出了系統能給你的,系統不能知足需求,因而產生溢出。 二、內存泄漏 是指你向系統申請分配內存進行使用(new),但是使用完了之後卻不歸還(delete),結果你申請到的那塊內存你本身也不能再訪問,該塊已分配出來的內存也沒法再使用,隨着服務器內存的不斷消耗,而沒法使用的內存愈來愈多,系統也不能再次將它分配給須要的程序,產生泄露。一直下去,程序也逐漸無內存使用,就會溢出。