1. 程式人生 > >明白生產環境中的jvm參數

明白生產環境中的jvm參數

owin log 事件 one 這也 ron 內存大小 alloc session

明白生產環境中的jvm參數

寫代碼的時候,程序寫完了,發到線上去運行,跑一段時間後,程序變慢了,cpu負載高了……一堆問題出來了,所以了解一下生產環境的機器上的jvm配置是有必要的。比如說:

JDK版本是多少?采用何種垃圾回收器?
程序啟動的時候默認分配堆內存空間是多少?隨著程序的運行,程序最多能使用多大的內存空間?
程序中使用了多少個線程?這些線程又處於何種狀態?
了解了這些,會對程序的運行有一個更好的了解。本文結合生產實踐,記錄一下我常用的一些操作。

註意:如果沒有特殊說明,下面所有的參數討論都是基於JDK8 server class machine 而言的

根據官方調優文檔,server類型的機器滿足以下要求:

A class of machine referred to as a server-class machine has been defined as a machine with the following:

2 or more physical processors
2 or more GB of physical memory
我的理解:就是這臺機器 有兩個以上的物理處理器,並且 具有2G或2G以上的內存,那麽就是 server 類型的機器

可通過這個命令查看機器的物理處理器核數:

cat /proc/cpuinfo | grep "physical id" | sort | uniq | wc -l

可通過這個命令查看機器的總內存大小:

cat /proc/meminfo | grep MemTotal

當寫完一個Spring boot Maven 工程,使用 mvn clean package 打包成可運行的jar文件後,可使用如下命令開始執行:

nohup java -Xloggc:${logging_file_location}gc.log -XX:+PrintGCDetails -jar app.jar --spring.profiles.active=${environment} --logging.file.location=${logging_file_location} --domain=com.xx.xxx.xxxx > /dev/null 2>&1 &

-Xloggc: 指定程序運行過程中產生的 GC 日誌輸出到 gc.log 文件中。
-XX:+PrintGCDetails 指定 輸出詳細的GC日誌。
spring.profiles.active=${environment} 可根據 environment變量來選擇是生產環境還是測試環境。有時生產環境中使用的數據源(比如 Mysql)與測試環境不一樣,這樣就很方便。
--logging.file.location指定程序輸出的日誌
--domain 這個參數主要用來對程序進行標識。比如,使用 ps aux | grep com.xx.xxx.xxxx 就能方便地找到程序的進程號了。
查看GC收集器

JDK版本號一般很容易知道,java --version就行了。那如何知道運行你的程序的JAVA虛擬機采用何種垃圾回收器呢?

其實可以從gc日誌裏面看出 jvm 使用的何種垃圾收集器。我安裝的JDK8,server 類型的機器,新生代默認使用的是:parallel scavenge,而老年代默認使用:ParOldgen 垃圾收集器。而看JVM調優官方文檔:Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide JDK9 默認是G1收集器

一條新生代GC日誌:

0.791: [GC (Allocation Failure) [PSYoungGen: 64000K->3229K(74240K)] 64000K->3237K(243712K), 0.0040270 secs] [Times: user=
0.04 sys=0.00, real=0.00 secs]

一條老年代GC日誌:

152561.075: [Full GC (Ergonomics) [PSYoungGen: 9303K->0K(68096K)] [ParOldGen: 424954K->389313K(439296K)] 434258K->389313K
(507392K), [Metaspace: 42513K->42513K(1087488K)], 0.0598682 secs] [Times: user=0.39 sys=0.01, real=0.06 secs]

查看JVM堆使用

知道了垃圾回收器,再來看看默認情況下,程序運行時初始堆大小,隨著程序的運行,堆內存最終可達到多大?

如果在啟動程序時使用-Xmx 指定了最大堆容量,那堆內存最終可達到的值,就是 Xmx設置的值(當然,Xmx不可能設置得比機器的物理內存還要大,同時也不要設置得和機器內存很接近,畢竟還有留一些內存給機器上的其他程序用)

下面以一臺實際的物理機器,來分析下,程序是如何使用堆內存的。這臺物理機器的內存大小為:16225356KB(約為16GB),物理處理器核數為2。因此符合 sever class machine。對於 server class 機器,默認使用如下參數:

On server-class machines, the following are selected by default:

Throughput garbage collector
Initial heap size of 1/64 of physical memory up to 1 GB
Maximum heap size of 1/4 of physical memory up to 1 GB
Server runtime compiler
使用以吞吐量優先的GC 回收器。

Initial heap size of 1/64 of physical memory up to 1 GB 這句話,解讀很多。我的理解是:JAVA程序啟動時,默認分配的堆大小為:機器物理內存的64分之一,在我的示例中,機器的物理內存是16225356KB,因為初始時分配的堆大小為247MB:

16225356/64/1024
247

而使用java -XX:+PrintCommandLineFlags命令:(單位是B)

-XX:InitialHeapSize=259605696 -XX:MaxHeapSize=4153691136

可看出初始堆大小為259605696B,259605696/1024/1024=247MB。由此可知:JVM啟動時分配的初始堆大小為物理機器內存的64分之一。

然後我再在一臺內存為128GB的機器上:

cat /proc/meminfo | grep MemTotal
MemTotal: 131829708 kB

131829708 / 1024 /1024 =125 (也即:128GB內存)

java -XX:+PrintCommandLineFlags
-XX:InitialHeapSize=2109275328 -XX:MaxHeapSize=32037767584 -XX:+PrintCommandLineFlags -XX:+UseCompressedOops -XX:+UseParallelGC

可以看出:-XX:InitialHeapSize=2109275328,也即:2109275328/1024/1024=2GB,也就是說:在物理內存為128GB的機器上,JAVA堆的初始分配大小為2GB,是超過1GB的。

Maximum heap size of 1/4 of physical memory up to 1 GB,隨著程序的運行,JVM堆內存會越來越大,但是一個JAVA進程最大能使用多大的堆內存空間呢?答案是 四分之一的物理機器內存。

更具體地,對於一臺物理內存為16GB的機器,如果在JAVA程序啟動時 不用 Xms、Xmx 參數指定jvm堆大小,即:這個程序就是使用默認的 java堆大小配置,一開始JAVA堆大小為:16GB/64 ,約為:247MB;然後隨著程序的運行,JAVA堆分配的內存會動態增大,動態增大的上限是:物理機器內存的四分之一,即約為4GB。

比如,我查看 程序啟動後 第一次 GC日誌如下:

0.791: [GC (Allocation Failure) [PSYoungGen: 64000K->3229K(74240K)] 64000K->3237K(243712K), 0.0040270 secs] [Times: user=0.04 sys=0.00, real=0.00 secs]
GC前該內存區域(新生代)大小:62MB,GC後該區域的大小:3MB,該區域的總內存大小:72MB。
而GC前JAVA堆使用量62MB,gc後JAVA堆使用量3.1MB. JAVA堆的總大小:238MB(與247MB很接近)

我來做個猜想:根據第一條gc日誌,JAVA堆總大小是238MB,新生代與老年代的比例是1:2,即:-XX:NewRatio=2,1/3的堆是新生代,2/3的堆是老生代,這樣的話,新生代的堆大小是:238/3=79MB,剛好與發生GC的區域總內存72MB接近。
而新生代再進一步細分:分為 Eden區、兩個Survivor區,其中Eden區占新生代堆大小的8/10,兩個Survivor區占新生代堆大小的2/10。即:Eden區的大小為:790.8=63MB ,與前面提到的 62MB非常接近。也就是說:在Eden區空間不足以容納新創建的對象的時候,發生了一次 PSYoungGen 垃圾回收操作。而Survivor區的大小為790.1=8MB,回收完成後,將剩余的3.1MB對象 存儲 在其中一個Survivor區了。

當隨著程序運行一段時間後:再看一條GC日誌:

95191.984: [GC (Allocation Failure) [PSYoungGen: 48668K->2932K(73216K)] 507352K->461823K(648192K), 0.0032727 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
GC前該內存區域(新生代)大小:48668/1024=47MB,GC後該區域大小約為2MB,該區域的總內存大小:71MB。GC前JAVA堆內存的使用量 507352/1024=495MB,GC後JAVA堆的內存使用量461823/1024=450MB,JAVA堆內存總大小:648192/1024=633MB

可見,運行一段時間後,JAVA堆內存大小從:238MB,動態增加到了:633MB

?

進程的各種狀態

一般我們會用 ps aux | grep java來查看java進程,知道進程ID號(比如13988)後,可通過:

cat /proc/13998/status | grep Threads
Threads: 78

查看一個JAVA進程下一共啟動了多少個線程。

另外,ps aux 中其中有一列是顯示進程狀態的,那進程狀態有哪些呢?

man ps 找到的進程狀態的解釋:

PROCESS STATE CODES
Here are the different values that the s, stat and state output specifiers (header "STAT" or "S") will display to describe the state of a process:
D uninterruptible sleep (usually IO)
R running or runnable (on run queue)
S interruptible sleep (waiting for an event to complete)
T stopped, either by a job control signal or because it is being traced.
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z defunct ("zombie") process, terminated but not reaped by its parent.

For BSD formats and when the stat keyword is used, additional characters may be displayed:
< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)

  • is in the foreground process group.
    D 代表不可中斷的阻塞,比如說I/O操作。不管是顯示IO,還是隱式IO,訪問本地磁盤的IO操作時,一般會處於D狀態。

In practice, processes typically go into D state ("uninterruptible sleep") when they‘re blocked on access to a local disk, whether that‘s explicit I/O (read/write) or implicit (paging).
S代表可中斷的睡眠狀態,比如線程執行下面的代碼:sleep(500),就處於可中斷的睡眠狀態吧。

try {
    Thread.sleep(500);
} catch (InterruptedException e) {
    logger.info("thread interrupted:{}", e.getCause());
}

關於S狀態的解釋:(waiting for an event to complete),比如說,線程A在爭搶鎖時,由於這把鎖已經被線程B拿到了,那麽 線程A 就會進入 S 狀態吧,線程A等待著線程B釋放鎖這一事件。

談到線程的狀態,其實有個參數與線程狀態息息相關,那就是CPU負載。處於哪個狀態的線程,才會計入CPU的負載呢?

There are two contributions to the load factor: number of processes/threads on the ready-to-run queue and the number blocked on I/O. The processes blocked on I/O show up in the "D" state in ps and top and also contribute to this number.

Not all processes blocked on I/O are in D state - for a common example, processes blocked on I/O to a network socket or terminal will simply be in the S state, and not count towards load.
可以這樣理解:準備運行的線程( ready-to-run queue)和阻塞在I/O操作上的線程都是計入負載的。

但是阻塞在I/O操作上的線程有兩種狀態,一種是D狀態,另一種是S狀態。其中S狀態的線程是不計入負載的。

總結

在我的生產環境中,默認安裝JDK8,在未人為指定任何JVM參數的情況下,新生代采用Parallel Scavenge收集器,它是一個以吞吐量為目標的GC,采用多線程基於復制算法對新生代進行垃圾回收。老年代采用Parallel Old垃圾收集器,采用多線程基於 Mark-sweep 算法進行回收。
每一款垃圾收集器都會有一個相應的調優目標,以最短停頓時間為目標、以最大吞吐量為目標、以最小使用堆內存空間為目標。這些目標是有優先級的,優先級最大的目標是停頓時間,其次是吞吐量。對於並行垃圾收集器(Parallel Collectors),默認情況下,並沒有設置“最大停頓時間”這一目標,這也就意味著最短停頓時間目標默認是實現了的。因此,Parallel Collectors 達到設置的吞吐量要求,而這個吞吐量由參數 -XX:GCTimeRatio指定,默認值為99,也即:為了保證吞吐量,GC時間只能占到整個程序運行時間的1% (參考JVM官方調優指南)
最短停頓時間目標是關於GC時間的,而最大吞吐量也是講GC時間,在我看來:最短停頓時間是在一次GC過程中設置了一個“硬指標”,即:每次GC時間不能超過 設置的停頓時間這個值;而最大吞吐量則是講:所有的總的GC時間加起來不要超過某個值 歡迎工作一到五年的Java工程師朋友們加入Java群: 891219277
群內提供免費的Java架構學習資料(裏面有高可用、高並發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
,以此來保證吞吐量。二者關註點是有區別的。
參考資料:

明白生產環境中的jvm參數