java不需要開發人員來顯示配置設定和回收記憶體,而是由JVM來自動管理内在的配置設定及回收,這對人員來說确實大大降低了編寫程式的難度,但副作用可能是在不知不覺中浪費了很多記憶體,導緻JVM花費很多時間進行記憶體的回收。另來還可能帶來的副作用是由于不清楚 JVM記憶體的配置設定和回收機制,造成記憶體洩露,最終導緻JVM記憶體不夠用。是以對于java開發人員而言,不能因為JVM自動記憶體管理就不掌握記憶體配置設定和回收的知識了。
除了記憶體的配置設定及回收外,還須掌握跟蹤分析JVM記憶體的使用情況,以便更加準确地判斷程式的運作善及進行性能的調優。
記憶體空間
JVM的記憶體空間劃分為方法區、堆、本地方法棧、PC寄存器及JVM方法棧。
1、方法區
方法區存放了要加載的類的資訊、類中的靜态變量、類中定義為final類型的常量、類中的Field資訊、類中的方法資訊,當開發人員在程式中通過class對象的getName、isInterface等方法來擷取資訊時,這些資料都來源于方法區域。方法區域也是全局共享的,在一定條件下它也會被GC,當方法區域要使用記憶體超過其允許的大小時,會抛出OutOfMemory的錯誤資訊。
在Sun JDK中這塊區域對應Permanet Generation,又稱為持久代,預設最小值為16MB,最大值為64MB,可通過-XX:PermSize及-XX:MaxPermSize來指定最小值和最大值。
2、堆
堆用于存儲對象執行個體及數組值,可以認為java中所有通過new建立的對象的記憶體在此配置設定,Heap中對象所占用的記憶體由GC進行回收,在32位作業系統上最大為2GB,在64位作業系統上同沒有限制,其大小可通過-Xms和-Xmx來控制,-Xms為JVM啟動時申請的最小Heap記憶體,預設為實體記憶體的1/64但小于1GB;-Xmx為JVM可申請的最大Heap記憶體,預設為實體記憶體的1/4但小于1GB,預設當空餘堆記憶體小于40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRatio=來指定這個比例;當空餘堆記憶體大于70%,JVM會減小Heap的大小到-Xms指定的大小,可通過-XX:MaxHeapFreeRatio=來指定這個比例,對于運作系統而言,為避免在運作時頻繁調整Heap的大小,通常将-Xms和-Xmx的值設成一樣。
為了讓記憶體回收更加高效,SunJDK從1.2開始對堆采用了分代管理的方式,分為新生代和舊生代,新生代又分為Eden區、S0和S1區。
新生代(new generation)
大多數情況下java程式中建立的對象都從新生代配置設定記憶體,新生代由Eden space和兩塊相同大小的survivor space(通常雙稱S0和S1或From和To)構成,可通過-Xmn參數來指定新生代的大小,也可通過-XX:SurvivorRatio來調整Eden Space及Survivor Space的大小。不同的GC方式會以不同的方式按此值來劃分Eden space和survivor space,有此GC方式還會根據運作善來動态調整Eden、S0、S1的大小。
舊生代(old generation或tenuring generation)
用于存放新生代中經過多次垃圾回收仍然存活的對象,例如緩存對象,建立的對象也有可能在舊生代上直接配置設定記憶體。主要有兩種情況:一種為大對象,能通過在啟動參數上設定-XX:PretenureSizeThreshold=1024來代表當對象超過多大時就不在新生代配置設定,而是直接在舊生代配置設定,此參數在新生代采用Parallel Scavenge GC時無效,Parallel Scavenge GC會根據運作狀況決定什麼對象直接在舊生代上配置設定記憶體;另一種為大的數組對象,且數組中無引用外部對象。
3、本地方法棧
本地方法棧用于支援native方法的執行,存儲了每個native方法調用的狀态,在Sun JDK的實作中本地方法棧和JVM方法棧是同一個。
4、PC寄存器和JVM方法棧
每個線程均會建立PC寄存器和JVM方法棧,PC寄存器戰勝的可能為CPU寄存器或作業系統記憶體,JVM方法棧占用的為作業系統記憶體,JVM方法棧為線程私有,其在記憶體配置設定上非常高效。當方法運作完畢時,其對應的棧幀所占用的記憶體也會自動釋放。
當JVM方法棧空間不足時,會抛出StackOverflowError的錯誤,在SunJDK中可以通過-Xss來指定其大小。
記憶體配置設定
java對象所占用的記憶體主要從堆上進行配置設定,堆是所有線程共享的,是以在堆上配置設定記憶體時需要進行加鎖,這導緻了建立對象開銷比較大。當堆上空間不足時,會觸發GC,如果GC後空間仍然不足,則抛出OutOfMemory錯誤資訊。
Sun JDK為了提升記憶體配置設定的效率,會為每個新建立的線程在新生代的Eden Space上配置設定一塊獨立的空間,這塊空間稱為TLAB,其大小由JVM根據運作情況計算而得,可通過-XX:TLABWasteTargetPercent來設定TLAB可占用的Eden Space的百分比,預設值為1%。JVM将根據這個比率、線程數量及線程是否頻繁配置設定對象來給每個線程配置設定合适大小的TLAB空間。在TLAB上配置設定内在時不需要加鎖,是以JVM在給線程中的對象配置設定記憶體時會盡量在TLAB上配置設定,如果對象過大或TLAB空間已用完,則仍然在堆上進行配置設定。是以在編寫java程式時,通常多個小的對象比大的對象配置設定起來更加高效,可通過在啟動參數上增加-XX:+PrintTLAB來檢視TLAB空間的使用情況。
記憶體回收
收集器
JVM通過GC來回收堆和方法區上的記憶體,GC的基本原理首先會找到程式中不同地被使用的對象,然後回收這些對象所占用的記憶體,通常采用收集器的方式實作GC,主要的收集器有引用計數收集器和跟蹤收集器。
1.引用計數收集器
引用計數收集器采用的為分散式的管理方式,通過計數器記錄對象的是否被引用。當計數器為0時,說明此對象已經不再被使用,于是可進行回收。注意:引用計數器對于循環引用的場景沒有辦法實作回收。
2.跟蹤收集器
跟蹤收集器采用的為集中式的管理方式,全局記錄資料的引用狀态。基于一定的條件的觸發(定時、空間不足時),執行時需要從根集合來掃描對象的引用關系,這可能會造成應用程式暫停,主要有複制、标記-清除和标記-壓縮三種實作算法。
SunJDK中可用的GC
以上三種跟蹤收集器各有優缺點,SunJDK根據運作的java程式進行分析,認為程式中大部分對象的存活時間都是較短的,少部分對象是長期存活的。基于這個分析,SunJDK将JVM堆劃分為了新生代和舊生代,并基于新生代和舊生代中對象存活時間的特征提供了不同的GC實作。
新生代可用GC
SunJDK認為新生代中的對象通常存活時間較短,是以選擇了基于Copying算法實作對新生代對象的回收,根據以上copying算法的介紹,在執行複制時,需要一塊未使用的空間來存放存活的對象,這是新生代又被劃分為Eden、S0和S1三塊空間的原因。Eden Space存放新建立的對象,S0或S1的其中一塊用于在Minor GC觸發時作為複制的目标空間,當其中一塊為複制的目标空間時,另一塊中的内容則會被清空。是以通常又将S0、S1稱為From Space和 To Space,Sun JDK提供了串行GC、并行回收GC和并行GC三種方式來回收新生代對象所占用的記憶體,對新生代對象所占用的記憶體進行的GC又通常稱為Minor GC。
在對象引用關系上,除了預設的強引用外,SunJDK還提供了軟引用(SoftReference)、弱引用(WeakPeference)和虛引用(PhantomReference)三種引用。
強引用
A a= new A(); 就是一個強引用,強引用的對象隻有在主動釋放了引用後才會被GC。
軟引用
軟引用采用SoftReference來實作,采用軟引用來建立引用的對象,當JVM記憶體不足時會被回收,是以SoftReference很适合用于實作緩存。另外,當GC認為掃描到的SoftReference不經常使用時,也會進行回收,存活時間可通過-XX:SoftRefLRUPolicyMSPerMB來進行控制,其含義為每兆堆空閑空間中SoftReference的存活時間,預設為1秒。
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<Object>(obj);
obj = null;
當需要擷取時,可通過softRef.get來擷取,值得注意的是softRef.get有可能會傳回nul.
弱引用
弱引用采用WeakReference來實作,采用弱引用建立引用的對象沒有強引用後,GC時即會被自動釋放。
WeakReference<Object> weakRef = new WeakReference<Object>(obj);
當需要擷取時,可通過weakRef.get來擷取,值得注意的是weakRef.get有可能會傳回null。
可傳入一個ReferenceQueue對象到WeakReference的構造器中,當object對象被辨別為可回收時,執行weakRef.isEnqueued會傳回true。
虛引用
虛引用采用PhantomReference來實作,采用虛引用可跟蹤到對象 是否已從記憶體中被删除。
ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
PhantomReference<Object> ref = new PhantomReference<Object>(obj,refQueue);
值得注意的是ref.get永遠傳回null,當object從記憶體中删除時,調用ref.isEnqueued()會傳回true。
當掃描引用關系時,GC會對這三種類型的引用進行不同的處理,簡單來說,GC首先會判斷所掃描到的引用是否為Reference類型。如果為Reference類型,且其所引用的對象無強引用,則認為該對象為相應的Reference類型,之後GC在進行回收時這些對象則根據Reference類型的不同進行相應的處理。
當掃描存活的對象時,MinorGC所做動作為将存活的對象複制到目前作為To Space的S0或S1中;當再次進行MinorGC時,之前作為To Space的S0或S1則轉換為From Space,通常存活的對象在MinorGC後并不是直接進入舊生代,隻有經曆過幾次MinorGC仍然存活的對象,才放入舊生代中,這個在MinorGC中存活的次數在串行和ParNew方式時可通過-XX:MaxTenuringThreshold來設定,在Parallel Scavenge時則由Hostspot根據運作狀況來決定。當To Space空間滿剩下的存活對象則直接轉入舊生代中。
三種回收方式:
串行GC(-XX:+UseSerialGC)、并發GC(-XX:+UseParallelGC)、并行GC(-XX:+UseParNewGC)
舊生代和持久代可用的GC
JDK提供了串行、并行及并發三種GC來對舊生代及持久代對象所占用的記憶體進行回收。
為了避免開發人員選擇哪種GC而頭痛,SunJDK還提供了兩種簡單的方式來幫助選擇GC。
1.對呑吐量優先
呑吐量是指GC所耗費的時間占應用運作總時間的百分比,例如應用總共運作了100分鐘,其中GC執行占用了1分鐘,那麼呑吐量就是99%,JVM預設的名額是99%。
呑吐量優先的政策即為以呑吐量為名額,由JVM自行選擇本應的GC政策及控制New Genneration、Old Generation記憶體的大小,可通過在JVM參數中指定-XX:GCTimeRatio=n來使用此政策。
2.暫停時間優先
暫停時間是指每次GC造成的應用的停頓時間,預設不啟用這個政策。