关于rocketmq:RocketMqNameServ启动脚本分析Ver494

3次阅读

共计 17686 个字符,预计需要花费 45 分钟才能阅读完成。

NameServ 启动脚本剖析

概览

  • mqnamesrv 命令
  • mqnamesrv 脚本
  • runserver.sh 脚本

    • 大于等于 JDK9 的启动参数
    • 等于 JDK8 的启动参数

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
      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 版本大于 8 之后的状况:

    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"

首先是 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 版本处于试验状态。

集体了解是思考到后续的版本垃圾收集器变动比拟大,这里抉择了 G1 之前比拟稳当的 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"

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 日志失落。

等于 JDK8 的启动参数

应该说是比拟要害的版本,上面的参数对应 JDK8 的版本,很显著都是围绕 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"

第一行

比照发现 JDK9 版本没啥区别:堆内存(初始堆内存)为 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 经典组合,不过 NameServ 大部分时候干的不是什么轻活,从保障高可用的角度是没啥问题的。
  • JDK8 应用的是 G1 垃圾收集器,并且参数都是尽可能的给新生代预留空间。
  • NameServ 新生代的压力会比拟大,整体思路是尽可能的缩小垃圾,通过踊跃的 GC 保障垃圾尽可能被回收。

写在最初

集体认为这种学习形式一举多得,还能够看到不少 Shell 脚本的应用技巧,挺不错的。

正文完
 0