Java記憶體模型:Java Memory Model,簡稱JMM。整體上,JVM記憶體包含堆記憶體和線程棧記憶體,原始資料類型和對象引用在棧記憶體上,對象(及其成員變量)、靜态變量都在堆記憶體上。堆記憶體上的所有對象,可以被所有線程拿到,屬于共享區域。大部分時候,我們處理的都是堆記憶體上的問題。

JVM的GC(垃圾回收)采用分代機制,即堆記憶體分為年輕代、老年代,年輕代裡面繼續分三塊:新生代(Eden-Space)、兩個存活區(S0、S1)。GC的大緻過程是:建立對象時會在Eden區配置設定記憶體,GC的時Eden區和S區(S0和S1中非空的那個)中存活的對象複制到另外一個空的S區。S0和S1在任何時候都有一個空區域專門存儲被年輕代GC後仍然存活的對象,這批對象在S0和S1中來回複制多次,最終達到一定存活時間的對象,被移到老年代。
在預設情況下,Eden:S0:S1的記憶體配置設定比例是:8:1:1。之是以來回複制,主要是為了解決GC後的記憶體碎片問題。
JVM采用的GC算法是:标記清除算法(Mark and Sweep)。
-
标記:會周遊所有可達對象,這個階段會讓所有應用程式的線程暫停,導緻STW停頓問題(Stop The
World);
- 清除:不可達對象占用的記憶體被回收,以便重用;
JVM調優,很大程度上是GC的調優,下面我們先看看GC相關的知識點。為了友善後面做示範,我們建立一個Java工程(過程略)。
我們可以先來一段耗記憶體的代碼:
List<Order> orderList=new ArrayList<>();
while(true){
Order order=new Order();
order.setId(1);
order.setName("Java In Action");
order.setPrice(20.5);
orderList.add(order);
System.out.println( "Hello World!"+order);
}
工程建議加上以下依賴,示範的時候會達成jar包,并用指令行執行:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.learn.performance.MainApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
打包之後,在Linux上啟動運作,筆者這裡采用CentOS。下面我們看看怎麼檢視GC資訊。
首先使用jps檢視目前程序ID:
然後使用jstat檢視GC情況:
jstat -gcutil 程序ID 間隔時間
解釋:
S0: 新生代中Survivor space 0區已使用空間的百分比
S1: 新生代中Survivor space 1區已使用空間的百分比
E: 新生代已使用空間的百分比
O: 老年代已使用空間的百分比
M: 永久帶已使用空間的百分比
YGC: 從應用程式啟動到目前,發生Yang GC 的次數
YGCT: 從應用程式啟動到目前,Yang GC所用的時間
FGC: 從應用程式啟動到目前,發生Full GC的次數
FGCT: 從應用程式啟動到目前,Full GC所用的時間
GCT: 從應用程式啟動到目前,用于垃圾回收的總時間
當E和O區占比一直偏大,GC頻繁時,就要開始注意系統的記憶體問題了。前面我們也提到過,GC時,會導緻系統卡頓(STW),此時就必須要考慮調優了。
使用jstat檢視GC,很多時候是為了線上快速定位問題。而在實際場景中,我們通常會考慮在整個運作階段都将GC資訊打到日志裡面,等到出問題後,可以讓運維拉日志排查問題。
要實作這個效果,需要新增啟動指令-XX:+PrintGCDetails,它可以幫助列印GC細節,然後通過-Xloggc指定GC日志的輸出檔案。其中%p是程序ID的占位符:
java -XX:+PrintGCDetails -Xloggc:log/gc_%p.log -jar PerformanceDemo-1.0.jar
這樣,最終會在log目錄下生成gc_[程序ID].log檔案。在GC檔案裡面,大家可以看到一些預設的配置,比如初始化堆記憶體、最大堆記憶體、GC算法(UseParallelGC)假如想知道GC發生的時間,還可以加上-XX:+PrintGCDateStamps參數。
在實際場景中,我們會在啟動時指定記憶體參數,比如:
java -Xmn80m -Xms200m -Xmx200m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:log/gc_%p.log -jar PerformanceDemo-1.0.jar
解釋:
- -Xmn 設定年輕代大小
- -Xms 設定初始記憶體,此值可以和-Xmx一樣
- -Xmx 設定JVM最大可用記憶體
啟動後,我們可以使用jmap指令檢視記憶體使用情況:
jmap -heap 程序id
一旦發現堆記憶體占用比過大時,我們就需要搞清楚到底是哪些對象導緻的,此時可以繼續使用jmap指令來檢視存活對象數量和大小:
jmap -histo:live 程序ID > jmapinfo
此時會把存活對象資訊打到jmapinfo這個檔案裡面,我們可以打開檔案看下:
可以很明顯看出,我們自定義的Order對象是最多的(Double作為Order的屬性之一)。這實際上就是調優的依據之一。
當系統真正出現OOM的時候,可能就直接挂了,這個時候我們是沒辦法使用jmap指令的,那怎麼辦呢?在生産環境中,我們通常會做OOM後生産記憶體dump檔案的配置,新增的指令參數如下:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=log/heapdump.hprof
此時一旦OOM,會自動将當時的記憶體資訊打到heapdump.hprof檔案中,我們可以下載下傳到本地,使用JDK自帶的jvisualvm工具打開檢視,效果如下: