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工具打開查看,效果如下:
