Java不像C++那樣顯式的分配和釋放記憶體,對Java程式員是一種解放,很大程度上降低了編程的難度,因為記憶體管理的工作都交由JVM自動進行。但是JVM自動記憶體管理也是一把雙刃劍,會造成寶貴的資源浪費,搞不好還會造成記憶體泄露。
記憶體空間的劃分:
Sun jdk也是遵照jvm規範,將記憶體空間劃分為方法區、堆、本地方法棧、pc寄存器、jvm方法棧。如:
,JVM主要包括兩個子系統和兩個組件。兩個子系統分別是Class loader子系統和Execution engine(執行引擎) 子系統;兩個組件分別是Runtime data area (運行時資料區域)組件和Native interface(本地介面)組件。
Class loader子系統的作用:根據給定的全限定名類名(如 java.lang.Object)來裝載class檔案的內容到 Runtime data area中的method area(方法地區)。Java程式員可以extends java.lang.ClassLoader類來寫自己的Class loader。
Execution engine子系統的作用:執行classes中的指令。任何JVM specification實現(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好壞主要就取決於他們各自實現的Execution engine的好壞。
Native interface組件:與native libraries互動,是其它程式設計語言互動的介面。當調用native方法的時候,就進入了一個全新的並且不再受虛擬機器限制的世界,所以也很容易出現JVM無法控制的native heap OutOfMemory。
Runtime Data Area組件:這就是我們常說的JVM的記憶體了。它主要分為五個部分——
①Method Area(方法區):
方法區存放了要載入的類的資訊、類中的靜態變數、類中被定義為final類型的靜態常量、類中的field資訊、類中的方法資訊。方法區是全域共用的,特定條件下會進行GC,當方法區要是用的記憶體大於運行大小時會跑出OutOfMemory異常。
Sun jdk中這塊記憶體對應Permanent Generation,也叫持久代,預設最小16M,最大64M,通過-XX:PermSize和-XX:MaxPermSize參數指定持久代的最小和最大值。
②堆(heap):
堆用於儲存物件執行個體及數組值,可以認為java中所有通過new操作符建立的對象都放在堆中,堆中對象由GC進行回收。一個Java虛擬執行個體中只存在一個堆空間。
這塊記憶體大小可以通過兩個參數進行指定:-Xms和-Xmx。
-Xms表示JVM啟動時申請的最小heap記憶體,預設為實體記憶體的1/64但小於1G。
-Xmx表示JVM可申請的最大heap記憶體,預設為實體記憶體的1/4但小於1G。
預設空閑堆記憶體小於40%時,JVM會增大heap到-Xmx指定的大小,這個比例可以通過參數-XX:MinHeapFreeRatio=來指定;預設當空閑堆記憶體大於70%時,jvm會減少heap到-Xms指定的大小,這個比例可以通過參數-XX:MaxHeapFreeRatio=來指定。建議將-Xms和-Xmx設定為相同的值,以避免頻繁調整jvm堆大小。
由於不同對象在jvm中存活的時間不同,有的很快就可以回收,有的可能生命週期貫穿整個jvm的生命週期,所以在Sun jdk從1.2開始就對堆記憶體進行分代管理。如:
1. Young(年輕代)
大多數情況下java程式中建立的對象是從新生代分配記憶體,新生代有兩部分組成:Eden Space和兩塊大小相等的Survivor Space(S0和S1)。可以通過參數-Xmn來指定新生代的大小,通過-XX:SurivorRatio來指定Eden Space和Survivor Space的大小。
2. Tenured(年老代)
年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。
3. Perm(持久代)
用於存放靜態檔案,如今Java類、方法等。持久代對記憶體回收沒有顯著影響,但是有些應用可能動態產生或者調用一些class,例如Hibernate等,在這種時候需要設定一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設定。
③Method Area(方法地區):被裝載的class的資訊儲存在Method area的記憶體中。當虛擬機器裝載某個類型時,它使用類裝載器定位相應的class檔案,然後讀入這個class檔案內容並把它傳輸到虛擬機器中。
④Native method stack(本地方法棧):
本地方法棧用於儲存native方法進入地區的地址,支援native方法的執行,Sun jdk中實現是本地方法棧和jvm方法棧是同一個。虛擬機器只會直接對Java stack執行兩種操作:以幀為單位的壓棧或出棧。
⑤pc寄存器和jvm方法棧:
每個線程都有自己的pc寄存器和jvm方法棧,pc寄存器佔用的可能是cpu寄存器或作業系統記憶體,jvm方法棧佔用的為作業系統記憶體,jvm方法棧為線程私人,線程運行完畢時其對應棧所佔用的記憶體全部自動釋放。
PC寄存器的內容總是指向下一條將被執行指令的餓地址,這裡的地址可以是一個本地指標,也可以是在方法區中相對應於該方法起始指令的位移量。
Sun jdk中通過參數-Xss來指定jvm方法棧大小,當jvm方法棧空間不足時拋出StackOverflowError。
記憶體的分配:
Jvm堆是所有線程共用的,因此在堆上分配記憶體需要加鎖,從而導致建立對象開銷較大。當堆空間不足時會觸發GC,如果GC後堆空間仍然不足會拋出OutOfMemory異常。
Sun jdk為提升記憶體配置效率,在新生代的Eden space為每個線程建立一個叫做TLAB(Thread Local Allocation Buffer)的地區,當線上程中建立對象時jvm會盡量在TLAB中分配記憶體,這時就不需要加鎖,節省了建立對象的開銷。
還有一種基於逃逸分析的方法,jvm會在棧上直接分配記憶體,線程結束時自動就釋放掉。
記憶體的回收:
收集器演算法
Jvm通過GC來回收記憶體,GC就是通過剖析器中不再被使用的對象,把這些對象所佔的記憶體收回,GC通常採用收集器方式,主要有引用計數收集器和跟蹤收集器。
引用計數收集器:採用分散的管理方式,通過記錄對象的引用次數進行判斷對象是否可能回收,當對象的引用計數為0時GC就可以回收該對象。但是引用計數器方式有他的缺點:每次對對象的賦值操作都會伴隨有引用計數的增減,帶來一定的額外消耗;對象間出現循環參考時會失效。所以Sun jdk實現中沒有採用引用計數器的方式。
跟蹤收集器:採用集中式的管理方式,全域記錄資料的引用狀態,執行GC時從根集合進行對象掃描,可能會造成應用程式暫停,線程阻塞。主要有複製(copying),標記-清除(mark-sweep),標記-壓縮(mark-compact)三種演算法實現。
Sun jdk中可用的GC演算法
新生代GC(Minor GC):新生代中對象通常存活時間短,對象少,所以選擇copying演算法實現新生代的GC。GC過程中複製對象時需要一塊未使用的記憶體區來存放存活的對象,這也是新生代劃分為Eden,S0,S1的原因。Eden存放剛建立的對象,S0或S1的其中一塊用作Minor GC的複製目標空間,另一塊被清空;下一次Minor GC時S0和S1交換角色。
串列GC:在整個掃描和複製過程採用單線程的方式來進行,使用於單CPU、新生代空間較小及對暫停時間要求不是非常高的應用上,是client層級預設的GC方式,可以通過-XX:+UseSerialGC來強制指定。
並行回收GC:在整個掃描和複製過程中採用多線程的方式來進行,使用於多CPU、對暫停時間要求較短的應用上,是server層級預設採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數。
並行GC與舊生代的並發GC配合使用。
舊生代GC:舊生代與新生代不同,對象存活的時間比較長,比較穩定,因此採用標記(Mark)演算法來進行回收,所謂標記就是掃描出存活的對象,然後再進行回收未被標記的對象,回收後對空出的空間要麼進行合并,要麼標記出來便於下次進行分配,總之就是要減少記憶體片段帶來的效率損耗。在執行機制上JVM提供了串列(GCSerial)、並行(GCParallel)和並發。
最後,建立對象可能會觸發GC,所以需要頻繁建立的對象可以用池來解決。注意對象的範圍,不用的及時顯式設定為null,便於GC早點回收。