博客 / 詳情

返回

【RocketMq】NameServ啓動腳本分析(Ver4.9.4)

NameServ啓動腳本分析

mqnamesrv 啓動命令

這裏直接摘錄了官方文檔:

Start NameServer

### Start Name Server first
$ nohup sh mqnamesrv &
### Then verify that the Name Server starts successfully
$ tail -f ~/logs/rocketmqlogs/namesrv.log
The Name Server boot success...

mqnamesrv 腳本

#!/bin/sh
# 從環境變量當中獲取RocketMq環境變量地址
if [ -z "$ROCKETMQ_HOME" ] ; then
  ## resolve links - $0 may be a link to maven's home
  ## 解決鏈接問題 - $0 可能是maven的主頁鏈接
  # PS:$0 是腳本的命令本身
  PRG="$0"

  # need this for relative symlinks
  # 需要相關鏈接
  while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
      PRG="$link"
    else
      PRG="`dirname "$PRG"`/$link"
    fi
  done
  # 暫存當前的執行路徑
  saveddir=`pwd`

  ROCKETMQ_HOME=`dirname "$PRG"`/..

  # make it fully qualified
  # 拼接獲取RocketMQ絕對路徑
  ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd`
  # 跳轉到當前暫存的命令執行路徑
  cd "$saveddir"
fi

export ROCKETMQ_HOME

# 關鍵: 執行runserver.sh腳本,攜帶logback的日誌xml配置,以及傳遞JVM的啓動main方法的入口類絕對路徑
sh ${ROCKETMQ_HOME}/bin/runserver.sh 
-Drmq.logback.configurationFile=$ROCKETMQ_HOME/conf/rmq.namesrv.logback.xml 
org.apache.rocketmq.namesrv.NamesrvStartup $@

最開始的mqnamesrv.sh 腳本獲取環境變量的部分看不懂其實沒啥影響,大略有個印象即可,當然可以截取部分的命令到Linux運行測試一下就明白了,比如準備環境變量等等,最後一句話比較關鍵。

注意最後的兩個字符$@,這兩個字符的作用如下:

$@ :表示所有腳本參數的內容。

$# :表示返回所有腳本參數的個數。

再次強調前面的一大坨獲取環境變量看不懂沒關係,看懂核心的執行腳本即可。

runserver.sh 腳本

runserver.sh 的腳本內容如下:

#!/bin/sh

#===========================================================================================
# Java Environment Setting
#===========================================================================================
error_exit ()
{
    echo "ERROR: $1 !!"
    exit 1
}

find_java_home()
{
    # uname 是獲取Linux內核參數的指令,不帶任何參數獲取當前操作系統的類型,比如Linux就是“Linux”的文本
    case "`uname`" in
        Darwin)
            JAVA_HOME=$(/usr/libexec/java_home)
        ;;
        *)
        # 可以簡單認為獲取到javac命令的絕對路徑,然後執行兩次cd..操作,以此作為JDK的路徑
        # 比如 /opt/jdk/bin/javac dirname 兩次之後就是 /opt/jdk
            JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac))))
        ;;
    esac
}

# 調用函數
find_java_home

# 讀取JAVA命令的執行地址
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"

# export 導出的臨時環境變量,只適用當前SHELL連接
# JAVA 命令的執行地址,設置為環境變量
export JAVA_HOME
export JAVA="$JAVA_HOME/bin/java"
# $0 代表當前的請求傳遞的第一個參數,根據上一個腳本可以知道是:${ROCKETMQ_HOME}/bin/runserver.sh
export BASE_DIR=$(dirname $0)/..
# 因為需要啓動JVM進程,需要從ROCKETMQ_HOME的conf和lib路徑告訴JDK找依賴包以及相關的配置文件
export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH}

#===========================================================================================
# JVM Configuration
#===========================================================================================
# The RAMDisk initializing size in MB on Darwin OS for gc-log
# 在 Darwin OS 上為 gc-log 初始化 RAMDisk 的大小(以 MB 為單位)
DIR_SIZE_IN_MB=600

choose_gc_log_directory()
{
    # Darwin 操作系統需要特殊處理,忽略
    case "`uname`" in
        Darwin)
            if [ ! -d "/Volumes/RAMDisk" ]; then
                # create ram disk on Darwin systems as gc-log directory
                DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null
                diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null
                echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS."
            fi
            GC_LOG_DIR="/Volumes/RAMDisk"
        ;;
        *)
         # 
            # check if /dev/shm exists on other systems
            # 檢查 /dev/shm 是否存在於其他系統上
            # What Is /dev/shm And Its Practical Usage
            # https://www.cyberciti.biz/tips/what-is-devshm-and-its-practical-usage.html
            if [ -d "/dev/shm" ]; then
                GC_LOG_DIR="/dev/shm"
            else
                GC_LOG_DIR=${BASE_DIR}
            fi
        ;;
    esac
}

choose_gc_options()
{
    # 根據JDK的版本選擇合適的GC參數,RocketMq最低需要JDK8,所以如果是1開頭就是10以及之後的JDK版本
    # Example of JAVA_MAJOR_VERSION value: '1', '9', '10', '11', ...
    # '1' means releases befor Java 9
    JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | sed -r -n 's/.* version "([0-9]*).*$/\1/p')
    if [ -z "$JAVA_MAJOR_VERSION" ] || [ "$JAVA_MAJOR_VERSION" -lt "9" ] ; then
      # 小於JDK 9 版本的參數
      # 堆內存(初始堆內存)為 4 g,新生代 2g,其他空間為 2g。元空間初始化128m,最大的擴容元空間為320mb
      JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
      # g1收集器在jdk11得到並行Full GC能力,而zgc在jdk11版本處於實驗狀態,這裏選擇了比較穩妥的 CMS 老年代垃圾回收器
      # UseCMSCompactAtFullCollection:CMS垃圾在進行了Full GC時,對老年代進行壓縮整理,處理掉內存碎片
      # CMSParallelRemarkEnabled 使用CMS老年代收集器
      JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"
      JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
      JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
    else
      # JDK8 之後的參數
      JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
      JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"
    fi
}

choose_gc_log_directory
choose_gc_options
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages"
#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"

$JAVA ${JAVA_OPT} $@

runserver.sh 腳本的內容,內容比較多,這裏拆分講解。

前置準備工作

首先是有關環境變量的配置獲取以及查找JAVA_HOME

#!/bin/sh

#===========================================================================================
# Java Environment Setting
#===========================================================================================
error_exit ()
{
    echo "ERROR: $1 !!"
    exit 1
}

find_java_home()
{
    # uname 是獲取Linux內核參數的指令,不帶任何參數獲取當前操作系統的類型,比如Linux就是“Linux”的文本
    case "`uname`" in
        Darwin)
            JAVA_HOME=$(/usr/libexec/java_home)
        ;;
        *)
        # 可以簡單認為獲取到javac命令的絕對路徑,然後執行兩次cd..操作,以此作為JDK的路徑
        # 比如 /opt/jdk/bin/javac dirname 兩次之後就是 /opt/jdk
            JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac))))
        ;;
    esac
}

# 調用函數
find_java_home

# 讀取JAVA命令的執行地址
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"

# export 導出的臨時環境變量,只適用當前SHELL連接
# JAVA 命令的執行地址,設置為環境變量
export JAVA_HOME
export JAVA="$JAVA_HOME/bin/java"
# $0 代表當前的請求傳遞的第一個參數,根據上一個腳本可以知道是:${ROCKETMQ_HOME}/bin/runserver.sh
export BASE_DIR=$(dirname $0)/..
# 因為需要啓動JVM進程,需要從ROCKETMQ_HOME的conf和lib路徑告訴JDK找依賴包以及相關的配置文件
export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH}

錯誤處理

首先是開頭部分,如果出現異常就打印錯誤參數。在SH腳本文件中,$1代表了跟在腳本後面的第一個參數,比如./script.sh filename1 dir1,則$1 = filename1

error_exit ()
{
    // $1 通常是調用函數傳入的第一個參數,比如下文的:Please set the JAVA_HOME variable in your environment, We need java(x64)!
    echo "ERROR: $1 !!"
    exit 1
}

查找JDK Home

接着是查找 JAVA_HOME 的位置:

find_java_home()
{
    # uname 是獲取Linux內核參數的指令,不帶任何參數獲取當前操作系統的類型,比如Linux就是“Linux”的文本
    case "`uname`" in
        Darwin)
            JAVA_HOME=$(/usr/libexec/java_home)
        ;;
        *)
        # 可以簡單認為獲取到javac命令的絕對路徑,然後執行兩次 cd.. 操作,以此作為JDK的路徑
        # 比如 /opt/jdk/bin/javac dirname 兩次之後就是 /opt/jdk
            JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac))))
        ;;
    esac
}

uname 是獲取Linux內核參數的指令,不帶任何參數獲取當前操作系統的類型,比如Linux就是“Linux”的文本:

xander@xander:~$ uname 
Linux

這裏通過uname 查找內核,如果是 darwin 的操作系統獲取路徑要特殊一些。而其他的方式則是$(dirname $(dirname $(readlink -f $(which javac))))層層查找:

[zxd@localhost ~]$ echo $(dirname $(dirname $(readlink -f $(which javac))));
/opt/jdk8

這些可以簡單認為獲取到javac命令的絕對路徑,然後執行兩次cd..操作,以此作為JDK的路徑。最簡單的驗證方法是放到Linux上執行一下:

[zxd@localhost ~]$ echo $(readlink -f $(which javac))
/opt/jdk8/bin/javac
[zxd@localhost ~]$ echo $(dirname $(dirname $(readlink -f $(which javac))));
/opt/jdk8

取JAVA命令的執行地址

這裏比較簡單,最後一句調用了error_exit函數,對應了第一個參數就是要打印的值。

# 讀取JAVA命令的執行地址
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"
# export 導出的臨時環境變量,只適用當前SHELL連接
# 取JAVA 命令的執行地址,設置為環境變量
export JAVA_HOME
export JAVA="$JAVA_HOME/bin/java"
# $0 通常代表腳本名稱本身,這裏獲取出來的結果是:${ROCKETMQ_HOME}
export BASE_DIR=$(dirname $0)/..
# 因為需要啓動JVM進程,需要把ROCKETMQ_HOME的conf和lib路徑告訴JDK找依賴包以及相關的配置文件
export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH}

上面需要注意的點:

  1. export 導出的臨時環境變量,只適用當前SHELL連接。
  2. $0 代表當前j腳本名稱本身,../dirname 結合類似../../效果,$(dirname $0)/..為Rocketmq的安裝目錄地址。
  3. 需要把ROCKETMQ_HOMEconflib路徑告訴JDK找依賴包以及相關的配置文件。
JAVA="$JAVA_HOME/bin/java"
BASE_DIR=${ROCKETMQ_HOME}/bin
CLASSPATH=.:${ROCKETMQ_HOME}/bin/conf:${BASE_DIR}/lib/*:${CLASSPATH}

JVM 配置部分

之後往下的部分是JVM的配置部分,這部分我們拆分成兩個部分來講,最關鍵的是JVM參數配置部分,最開始是獲取JVM 的GC_LOG地址,這裏用uname識別操作系統,

# 在 Darwin OS 上為 gc-log 初始化 RAMDisk 的大小(以 MB 為單位)
DIR_SIZE_IN_MB=600

choose_gc_log_directory()
{
    # Darwin 操作系統需要特殊處理,忽略
    case "`uname`" in
        Darwin)
            if [ ! -d "/Volumes/RAMDisk" ]; then
                # create ram disk on Darwin systems as gc-log directory
                DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null
                diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null
                echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS."
            fi
            GC_LOG_DIR="/Volumes/RAMDisk"
        ;;
        *)
         # 
            # check if /dev/shm exists on other systems
            # 檢查 /dev/shm 是否存在於其他系統上
            # What Is /dev/shm And Its Practical Usage
            # https://www.cyberciti.biz/tips/what-is-devshm-and-its-practical-usage.html
            if [ -d "/dev/shm" ]; then
                GC_LOG_DIR="/dev/shm"
            else
                GC_LOG_DIR=${BASE_DIR}
            fi
        ;;
    esac
}

在Linux系統中,會檢查 /dev/shm 是否存在系統上,如果是就把GC_LOG掛載到/dev/shm,否則就當前RocketMQ的安裝目錄GC_LOG_DIR=${BASE_DIR}

這裏補充一下/dev/shm 是什麼?在Linux中,這塊空間起很大的作用,因為他不是硬盤而是一塊內存空間,默認為VM的一半大小,使用df -h命令可以看到:

xander@xander:~$ df -h
Filesystem                         Size  Used Avail Use% Mounted on
tmpfs                              389M  1.7M  388M   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv   14G  7.3G  5.8G  56% /
tmpfs                              1.9G     0  1.9G   0% /dev/shm
tmpfs                              5.0M     0  5.0M   0% /run/lock
/dev/sda2                          2.0G  247M  1.6G  14% /boot
tmpfs                              389M  4.0K  389M   1% /run/user/1000

RocketMq為什麼要使用這一塊空間?

tmpfs有以下優勢:

  1. 動態文件系統的大小。
  2. tmpfs 文件系統會完全駐留在 RAM 中,擁有近似內存的讀寫速度。

而缺點僅僅是 tmpfs 數據在重新啓動之後不會保留,可以做一些腳本備份的操作。

tmpfs 和 /dev/shm 解釋

這塊空間的專業名詞叫做:tmpfs(虛擬內存系統),tmpfs最大的特點就是它的存儲空間在VM(virtual memory),VM是由linux內核裏面的vm子系統管理的

VM中又劃分為RM (real memory) 和 swap,RM 就是VM實際可用的內存空間,而swap是用於輔助VM在RM不夠的時候犧牲硬盤作為內存空間使用,同樣RM還會把不常用的數據放到Swap。

tmpfps = RM (real memory) + swap。

tmpfs默認的大小是RM的一半,假如你的物理內存是1024M,那麼tmpfs默認的大小就是512M。可以通過mount命令擴大這塊空間大小。

tmpfps的存在意義是可以動態的擴容和縮小,並且只要不使用這塊空間它本身沒有任何的內存佔用(0字節)(零成本還好用),而一旦使用則可以把讀寫停留在內存保證數據瞬間完成,但是代價是這塊空間不具備記憶功能,重啓之後不會被保留。

參考資料:

  • What Is /dev/shm And Its Practical Usage

JVM核心參數

核心參數的部分就是JVM的啓動參數配置,也是腳本最為核心部分。

小於JDK9的啓動參數

對應了 gc_options 的上半部分,首先判斷JDK版本小於9之前的情況:

    if [ -z "$JAVA_MAJOR_VERSION" ] || [ "$JAVA_MAJOR_VERSION" -lt "9" ] ; then
      # 堆內存(初始堆內存)為 4 g,新生代 2g,其他空間為 2g。元空間初始化128m,最大的擴容元空間為320mb
      JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
      # g1收集器在jdk11得到並行Full GC能力,而zgc在jdk11版本處於實驗狀態,這裏選擇了比較穩妥的 CMS 老年代垃圾回收器
      # UseCMSCompactAtFullCollection:CMS垃圾在進行了Full GC時,對老年代進行壓縮整理,處理掉內存碎片
      # CMSParallelRemarkEnabled 使用CMS老年代收集器
      JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"
      JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
      JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"

首先是JVM堆大小和元空間大小分配:

JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"

-server

啓用-server時新生代默認採用並行收集,其他情況下,默認不啓用。-server策略為:新生代使用並行清除,老年代使用單線程Mark-Sweep-Compact的垃圾收集器。

堆大小設置

設置JVM的堆大小,堆內存(初始堆內存)為 4g,新生代 2g,其他空間為 2g,元空間初始化128M,最大的擴容元空間為320M。

如果是個人機器配置比較低,建議把這幾個值調小一些。

下面的參數比較關鍵,RocketMq在JDK8沒有選擇G1而是使用了CMS,因為G1收集器在jdk11才得到並行Full GC能力,而ZGC在JDK11版本處於實驗狀態,在JDK8 用不成熟的G1不太合適。

# UseCMSCompactAtFullCollection:CMS垃圾在進行了Full GC時,對老年代進行壓縮整理,處理掉內存碎片
# CMSParallelRemarkEnabled 使用CMS老年代收集器
JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"

UseCMSCompactAtFullCollection

CMS垃圾在進行了Full GC時,對老年代進行壓縮整理,處理掉內存碎片

CMSParallelRemarkEnabled

老年代收集器指定為CMS,在進行了Full GC時對老年代進行壓縮整理,處理掉內存碎片。

-XX:+UseCMSCompactAtFullCollection

CMS垃圾收集器

這裏補充一下CMS垃圾收集器的知識點:

CMS是基於標記清除算法實現的多線程老年代垃圾回收器。CMS為響應時間優先的垃圾回收器,適合於應用服務器,如網絡遊戲,電商等和電信領域的應用。Rocketmq本身就是誕生於電商平台使用CMS是比較穩妥的。

CMS垃圾收集器特點

  • FUll GC大部分時間和應用程序並行,代價是增加CPU開銷。
  • 併發FULL GC短暫停頓。
  • 用户線程和回收線程並行。

垃圾回收算法:標記清除算法

之後是對應的CMS常用優化參數。

-XX:+UseCMSCompactAtFullCollection

設置此參數之後,CMS垃圾在進行了Full GC時,對老年代進行壓縮整理,處理掉內存碎片。RocketMq 的腳本也開啓了每次Full Gc之後進行碎片整理。

-XX:CMSFullGCsBeforeCompaction=1

FUll GC 之後對老年代進行壓縮整理,處理掉內存碎片。RocketMq 的默認應對策略是積極的進行內存碎片整理,縮小老年代的大小,因為RocketMq需要的是高響應時間。

CMSInitiatingOccupancyFraction=70

CMSInitiatingOccupancyFraction=70 表示當老年代達到70%時,觸發CMS垃圾回收。

  • 計算老年代最大使用率(\_initiating\_occupancy)
  • 大於等於0則直接取百分號
  • 小於0則根據公式來計算

如果使用默認值,則老年代觸發回收的比例是動態的,不同的JDK版本可能會有不同的默認值,

((100 - MinHeapFreeRatio) + 
 (double) ( CMSTriggerRatio * MinHeapFreeRatio) / 100.0) 
 / 100.0

-XX:SoftRefLRUPolicyMSPerMB=0

官方解釋是:Soft reference 在虛擬機中比在客户集中存活的更長一些。

先説一下結論,個人認為這個參數設置為0是不應該的,至少需要設置個1000或者500(半秒)的緩衝,為什麼呢?

我們假設我們不小心把這個值設置為0有什麼後果呢

​如果-XX:SoftRefLRUPolicyMSPerMB=0,會導致上面clock公式的計算結果為0。

這個結果為0,就是軟飲用被頻繁回收導致觸發頻繁的GC,JVM發現每次反射分配的對象馬上就會被回收掉,然後接着又會通過代理生成代理對象,導致每次soft軟引用的對象一旦分配就會馬上被回收.

​結論就是這個值為0,反射機制導致動態代理類不斷的被新增,但是這部分對象又被馬上回收掉,導致方法區的垃圾對象越來越多,會導致很多垃圾無法完全回收。

為什麼RocketMq默認要把-XX:SoftRefLRUPolicyMSPerMB=0值設置為0?

我們接着分析,-XX:SoftRefLRUPolicyMSPerMB=0,但是使用的是服務器模式(-server),在server模式下,會用 -Xmx 參數獲取空閒空間大小。

空閒的空間越大,軟引用就會活的越久,如果設置的值過大,很可能因為框架反射類創建的軟引用過多但是因為存在空閒時間計算又沒法回收的情況。

把這個值設置為0,基本上就是説不讓反射產生的一些meta對象在GC之後回收不掉,直接通過1次GC就給他擺平了。但是個人角度來看未免過於極端,個人認為設置為0是不合適的。

參考案例:
這裏在網上找到電子表格緩存業務因為設置 -XX:SoftRefLRUPolicyMSPerMB=0 導致的問題例子。
當JVM參數中配置了 -XX:SoftRefLRUPolicyMSPerMB=0 參數,這個參數是控制SoftReference緩存時間,而我們的電子表格的緩存都是存儲SoftReference裏邊的,當設置了這個參數設置為0的時候,任意操作,只要是觸發了gc,這時候就會清空了電子表格緩存,導致即使在內存足夠的情況下,緩存也不生效了。

清除頻率可以用命令行參數 -XX:SoftRefLRUPolicyMSPerMB=<N>來控制,可以指定每兆堆空閒空間的 Soft reference 保持存活(一旦它飲用後不可達了)的毫秒數,這意味着每兆堆中的空閒空間中的 soft reference 會(在最後一個強引用被回收之後)存活1秒鐘。

當然並不是什麼時候 -XX:SoftRefLRUPolicyMSPerMB=0都是錯的,因為 soft reference 只會在垃圾回收時才會被清除,而垃圾回收並不總在發生。系統默認為一秒,如果覺得客户端不需要任何保留,改為 -XX:SoftRefLRUPolicyMSPerMB=0 可以及時清理乾淨數據。

RocketMq的做法個人理解為想要讓垃圾回收儘可能的回收乾淨對象,因為RocketMq並不是十分吃JVM堆內存,更多的是需要頁緩存,況且NameServ本身比較輕量級。

還有一個原因是軟引用這東西能不用就儘量不用,風險比較大。

-XX:+CMSClassUnloadingEnabled

老年代啓用CMS,但默認是不會回收永久代(Perm)的。此處啓用對Perm區啓用類回收,防止Perm區內存垃圾對象堆滿(需要與+CMSPermGenSweepingEnabled同時啓用)。

-XX:+CMSPermGenSweepingEnabled

同上,為了避免Perm區滿引起的Full GC,開啓併發收集器回收Perm區選項。

但是實際上這篇帖子上指出 https://stackoverflow.com/questions/3717937/cmspermgensweepingenabled-vs-cmsclassunloadingenabled

-XX:+CMSClassUnloadEnabled -XX:+CMSPermGenSweepingEnabled 在 Java 1.7 中不可用,但是選項-XX:+CMSClassUnloadEnabled對於Java 1.7仍然有效。換句話説在JDK1.7之後被建議使用-XX:+CMSClassUnloadEnabled

JVM1.7之前是什麼情況?為什麼會需要與+CMSPermGenSweepingEnabled同時啓用

下面這篇文章有評論進行了解釋:

用户對問題“CMSPermGenSweepingEnabled vs CMSClassUnloadingEnabled”的回答 - 問答 - 騰訊雲開發者社區-騰訊雲 (tencent.com)")

1.6 JVM的CMSPermGenSweepingEnabled參數做的唯一事情就是打印消息,它的處理方式與1.5不同。要使CMSClassUnloadingEnabled生效,還必須設置UseConcMarkSweepGC

-XX:SurvivorRatio=8

CMS 用的是標記清除的算法,使用CMS還是傳統的新生代和老年代的分代概念,這裏RocketMq用的是默認的分代分區策略,給了新生代更多的使用空間。它定義了新生代中Eden區域和Survivor區域(From倖存區或To倖存區)的比例,默認為8,也就是説Eden佔新生代的8/10,From倖存區和To倖存區各佔新生代的1/10.

-UseParNewGC

-XX:+UseParNewGC 設置年輕代為多線程收集。可與CMS收集同時使用,ParNew 在Serial基礎上實現的多線程收集器

日誌參數配置

總得來説 RocketMq 在JDK8的版本使用了老牌的-XX:+UseConcMarkSweepGC CMS垃圾收集器 和 -XX:+UseParNewGC CMS垃圾 垃圾收集器。

 -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"

-verbose:gc-XX:+PrintGCDetails在官方文檔中有説明兩者功能一樣,都用於垃圾收集時的信息打印

但是也有不同點:

-verbose:gc 是 穩定版本,參見:http://docs.oracle.com/javase/7/docs/technotes/tools/windows/java.html

-XX:+PrintGC 是非穩定版本。

-XX:+PrintGCDateStamps,日誌中添加時間標誌(日誌每行開頭顯示自從JVM啓動以來的時間,單位為秒)

注意-XX:+PrintGCDateStamps 打印GC發生時的時間戳,搭配 -XX:+PrintGCDetails 使用,不可以獨立使用

日誌輸出示例:

2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0,0959470 secs]

2014-01-03T12:08:38.239-0100: [GC 119125K->114661K(317440K), 0,1421720 secs]

-XX:+UseGCLogFileRotation-XX:NumberOfGCLogFiles=5-XX:GCLogFileSize=30m。UseGCLogFileRotation 會根據後面兩個參數的設置不斷的輪詢替換GC日誌,這裏最多保留了150M的GC日誌,後續再進行寫入就會從第一個文件開始替換。

150M日誌足夠及時處理大部分問題,並且不會出現歷史日誌長期駐留磁盤的問題。但是這個參數需要謹慎設置,如果設置過小容易導致關鍵GC 日誌丟失。

JDK9之後的啓動參數

應該説是比較關鍵的版本,很明顯都是圍繞G1垃圾收集器做的優化:

    else
      JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
      JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"

第一行

堆內存分配基本沒啥區別:堆內存(初始堆內存)為 4g,新生代 2g,其他空間為 2g。元空間初始化128m,最大的擴容元空間為320mb。

第二行

垃圾回收器的關鍵配置:

-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0

-XX:+UseG1GC

使用G1垃圾收集器。需要注意JDK8的G1垃圾收集器是“殘血”版本。

-XX:G1HeapRegionSize

一個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍從1M到32M,且是2的指數。如果不設定,那麼G1會根據Heap大小自動決定。

關鍵部分為region_size = 1 << region_size_log,左移的操作也就是2的倍數的概念。最後的參數判斷會讓region最大為32M,最小為1M,不允許超過這個範圍。

-XX:G1ReservePercent

`-XX:G1ReservePercent G1會保留一部分堆內存用來防止分配不了的情況,默認是10。

官方對於這個參數的介紹如下:

-XX:G1ReservePercent=10
Sets the percentage of reserve memory to keep free so as to reduce the risk of to-space overflows. The default is 10 percent. When you increase or decrease the percentage, make sure to adjust the total Java heap by the same amount. This setting is not available in Java HotSpot VM, build
設置保留內存的百分比,以減少到空間溢出的風險。默認是10%。當你增加或減少這個百分比時,請確保預留足夠的空間並且調整Java堆大小。注意這個設置在Java HotSpot VM中是不可用的。

擴大這個數值可以保證在進行GC 的時候提供更多堆內存保證存活空間存放晉升老年代的Region.

G1為分配擔保預留的空間比例:通過-XX:G1ReservePercent指定,默認10%。也就是老年代會預留10%的空間來給新生代的對象晉升,如果經常發生新生代晉升失敗而導致 Full GC,那麼可以適當調高此閾值。但是調高此值同時也意味着降低了老年代的實際可用空間

-XX:InitiatingHeapOccupancyPercent=30

觸發全局併發標記的老年代使用佔比,默認值45%。

默認值45%的含義是也就是老年代佔堆的比例超過45%。如果Mixed GC週期結束後老年代使用率還是超過45%,那麼會再次觸發全局併發標記過程,這樣就會導致頻繁的老年代GC,影響應用吞吐量。

當然調大這個值的代價是可能導致年輕代謹升失敗而導致FULL GC。RocketMq使用30的設置是讓老年代提早的觸發GC並且回收垃圾。

-XX:SoftRefLRUPolicyMSPerMB=0

-XX:SoftRefLRUPolicyMSPerMB=N 這個參數在是JVM系統參數和垃圾收集器無關。

-XX:SoftRefLRUPolicyMSPerMB參數,可以指定每兆堆空閒空間的軟引用的存活時間,默認值是1000,也就是1秒。可以調低這個參數來觸發更早的回收軟引用。如果調高的話會有更多的存活數據,可能在GC後堆佔用空間比會增加。 對於軟引用,還是建議儘量少用,會增加存活數據量,增加GC的處理時間。

日誌參數配置

配置和之前的版本是一樣的,這裏直接忽略了。

${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"

總結

整個NameServ的啓動腳本不算太複雜,這裏簡單歸納一下比較重要的點:

  • JDK9之前沒有用G1,因為不成熟,選擇傳統的CMS+ParNew經典組合。
  • JDK9之後使用的是G1垃圾收集器,並且參數都是儘可能的給新生代預留空間。
  • NameServ 新生代的壓力會比較大,整體思路是儘可能的減少垃圾,通過積極的GC保證垃圾儘可能被回收。

寫在最後

個人認為這種學習方式一舉多得,還可以看到不少Shell腳本的使用技巧,挺不錯的。

user avatar kimmking 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.