雖然Java的垃圾回收和當前高配置的服務器可以讓程序員大部分時間忘掉OutOfMemoryError的存在,但是訪問量增大後頻繁的GC會額外消耗CPU (使用top查看結果為us值高),系統響應速度下降,積壓的請求又會占用更多內存從而惡性循環,嚴重時可能導致系統不斷Full GC造成應用停頓。
優化內存的使用可從以下幾方面著手:
一、節流
1 使用單例模式
單例模式是開發者最早接觸並使用的設計模式之一,盡管寫代碼的時候可能還不知道用了設計模式。簡單來說就是構造函數private化,通過靜態方法獲得唯一實例。因為其特性,對於某些場景例如每次請求都要使用無狀態工具類的檢驗方法,使用單例模式可以大量節省創建新對象的開銷。
public class Singleton {
private Singleton() {}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
public void doSomething() { }
}
2 緩存常用對象
簡單來說就是按一定特征創建"對象緩存池",使用集合類保存已創建的對象,當有相同特征的對象申請時,使用緩存池中現有的對象代替通過 new關鍵字重新創建。
public class BigObjectPoolTest {
public static void main(String[] args) {
long start = System.nanoTime();
for(int i = 0; i < 10000; i++) {
BigObjectPool.getBigObject("xxx", true);
}
System.out.println("使用緩存池耗時" + TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS) + "毫秒");
start = System.nanoTime();
for(int i = 0; i < 10000; i++) {
BigObjectPool.getBigObject("xxx", false);
}
System.out.println("不使用緩存池耗時" + TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS) + "毫秒");
}
}
class BigObjectPool {
private static Map<String, BigObject> map = new HashMap<String, BigObject>();
static {
map.put("xxx", new BigObject("xxx"));
map.put("yyy", new BigObject("yyy"));
}
public static BigObject getBigObject(String key, boolean usePool) {
if(usePool) {
BigObject bo = map.get(key);
if(bo == null) {
bo = new BigObject(key);
}
return bo;
} else {
return new BigObject(key);
}
}
}
class BigObject{
private String name;
private byte[] data = new byte[1024 * 1024];
public BigObject(String name) { this.name = name; }
}
以-Xms32m -Xmx32m -Xloggc:d:/gc.log 參數運行
使用緩存池耗時3毫秒
不使用緩存池耗時998毫秒
(查看gc.log,可以觀察到不使用緩存池觸發Minor GC 1000次以上)
實際業務中通常使用EhCache等框架代替自己實現緩存池。
與這種實現原理相似的也有一個設計模式:享元模式,區別是享元模式更關注類設計結構上的優化,對上下文環境的設計也做了明確定義。
3 避免設計過大的對象
如果業務模型中要求的類的屬性和方法都非常多,可以嘗試將其拆分成多個小類,再通過合成/聚合模式組裝成一個大類,這也符合設計模式的優化思想。甚至可以結合上面的對象緩存池的方式將其中一部分內容緩存化。
class Composition {
private BigObject bigObject = null;
private int id;
public void setBigObject(BigObject bigObject) {
this.bigObject = bigObject;
}
public Composition(int id) {
this.id = id;
}
}
Composition c = new Composition(1);
c.setBigObject(BigObjectPool.getBigObject("xxx", true));
4 一些小技巧
使用StringBuilder代替用+號連接字符串
盡量使用int, long等基本類型代替Integer, Long包裝對象
合理利用SoftReference和WeakReference
二、開源 - 調整虛擬機參數
一般設置 java -server -Xms2048m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=256m
-Xms和-Xmx決定java堆區可使用的內存最小值和最大值,通常設為相同的值,避免運行期間反復的重新申請內存。如果出現OutOfMemoryError: Java heap space,則在硬件允許的情況下臨時調大-Xmx,為排查問題和優化代碼爭取時間。
-XX:PermSize和-XX:MaxPermSize決定永久代可用空間大小,存放class和meta信息,通常設置為相同的值。如果出現OutOfMemoryError: PermGen space,說明加載的類和jar文件過多,可以調大這兩個參數值。
如果web容器下有多個應用引用了相同的第三方jar文件,可以轉移到容器的共享目錄。
另一個重要參數是-Xmn,決定堆區新生代的大小,通常占-Xmx的比值設置為1/4到1/3。如果業務中有大量體積大且生命周期很短的對象創建需求,可適當調大新生代空間以利於失效對象在新生代中被回收。
此外,可通過參數設置回收算法:
–XX:+UseSerialGC
–XX:+UseParallelGC
–XX:+UseParallelOldGC
–XX:+UseConcMarkSweepGC
回收算法的選擇和對比需要較大的篇幅介紹,這裡不做詳細的解釋。通常來說,對於響應時間優先的web應用,ConcMarkSweepGC(CMS)是個不錯的選擇。
需要注意的是,經過幾代發展後,JVM對內存管理已經做的非常好。如果不是有明確的證據證明JVM的默認選擇不合理,就沒必要做過多細節的調整設置。調整後,可通過-XX:+PrintGCDetails -XX:+PrintGCTimeStamps等參數輸出GC信息進行比對,優化的首要目標是減少Full GC次數和時間。
更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2017-01/139410p2.htm