NightPxy 个人技术博客

JVM-1.8-Hotspot 参数

Posted on By NightPxy

概述

JVM 的参数共分三种

  • -参数 标准参数
    这是JVM规范中定义的参数,会强制要求每个JVM实现都必须实现并保持兼容
    最常见的标准参数就是 java -version
    常用的还有verbos命令 -verbose:class 打印载入类相关信息,在JVM报告找不到类或类冲突时非常有用
  • -X参数 非标准参数
    也是JVM规范中定义,但不强制要求必须实现
    最常见的非标准参数 -Xmx100m(堆最大) -Xms100m(堆初始) -Xmn100(年轻代)
  • -xx参数 不稳定参数
    这就是纯粹由每个JVM实现的自定义参数了
    以Hotspot为例, -XX:+PrintGCDetails(打印GC日志)

关于参数赋值

  • bool型 +打开 -关闭
    -XX:+PrintGCDetails(打开) -XX:-PrintGCDetails(关闭)
  • 数字型 直接设
    -Xmx100m
  • 字符串 =
    -XX:HeapDumpPath=./java_pid.hprof

非标准参数

参数 描述
-Xmx100m 堆最大上限
-Xms100m 堆初始大小,一般设置与最大相同,避免GC回收后JVM重新分配内存来伸缩堆
-Xmn200m 年轻代大小(Eden+2个Survivor共计),官方推荐3/8,老年代自动=堆-年轻代
-Xss128k 线程栈大小,1.5后默认1M.更少的线程栈可以产生更多线程(但受操作系统上限)
-Xloggc:file 将每个状态记录如文件中(带时间戳)
-Xdebug  

不稳定参数

参数 描述
-XX:SurvivorRatio 用于设置Eden和两个Survivor的比值,默认为8(Eden):1:1(1/10)
-XX:MaxMetaspaceSize=10M 设置元空间最大为10M,默认无限制
-XX:-UseParallelGC 启用并行GC
-XX:-UseSerialGC 启用串行GC
-XX:-UseConcMarkSweepGC 对老年代采用并发标记交换算法进行GC
-XX:-DisableExplicitGC 禁止代码调用System.gc,JVM的GC依然有效
-XX:MaxNewSize=N 新生代对象能占用内存的最大值
-XX:MaxPermSize=64M 老年代对象能占用内存的最大值
-XX:NewSize=2.125m 新生代对象生产时占用内存的默认值
-XX:ThreadStackSize=512 设置线程栈大小,为0表示使用系统默认值
-XX:HeapDumpPath=./java_pid.hprof 指定导出堆信息时的路径或文件名
-XX:-HeapDumpOnOutOfMemoryError 当首次遭遇OOM时导出此时堆中相关信息

SurvivorRadio
-XX:SurvivorRadio=4,表示Eden区与Survivor区的比值为4,但因为Survivor实际有两个,所以其具体比例为4:1:1,即Eden占4/6,每一个Survivor占1/6

GC执行三种方式,串行(Serial),并行(Parallel),并发(ConcMarkSweep)

  • 串行是client版的默认,GC与App互斥,即整个GC都会停顿
  • 并行是Server版的默认,GC与App两者线程并行执行,但此时GC线程本身是单线程
  • 并发是并行的升级版,GC本身就以多线程执行

java信息展示

jinfo

  • 查看所有参数 jinfo -flags pid
  • -XX:+PrintFlagsFinal 打印所有可设置的参数及它们的值
  • XX:+PrintTenuringDistribution 打印年龄分布
  • java -XX:+PrintFlagsFinal -version |grep manageable
    查看所有manageable参数,这代表可以通过jinfo运行时调整的
  • jinfo -flag -PrintGCDetails pid 打印GC明细

jmap

jmap -heap pid 查看目前堆情况
jmap -histo:live pid | head -7 查看对象分布

调优经验

通用

-XX:+DisableExplicitGC 这是一个非常难得的,个人觉得可以无脑添加的JVM参数
这个参数的意义非常简单,让来自应用程序的System.gc无效.

堆代空间

堆代空间本身会极大的影响到GC,并且调优相对GC参数更为简单,如果堆代调优后能解决问题最好,所以首先考虑的是堆代空间调优

堆代空间最重要的是堆代平衡.过于极端的堆代空间调整往往效果都非常差

  • 堆代过小
    年轻代过小在晋升时因为自身无法存入时跳过年龄线直接晋升老年代.这会到导致一些可能快速丢弃的瞬时对象被错误晋升到老年代并积压老年代空间.又会继续造成MajorGC甚至FullGC
    老年代过小也是同理,只要老年代自身无法继续存入就会触发MajorGC甚至FullGC,甚至会直接出现OOM
  • 堆代过大
    堆代也不应设置过大.无论年轻代过大还是老年代过大,越大意味着单次GC扫描的对象越多,那么每次停顿时间也会变长

所以堆代调优的核心在于平衡,在于根据应用程序的用途和执行过程,配合GC日志反复实验找出平衡点

一些可以参考的关于堆代参数的原则如下

  • 依据JVM的目标机器,给定一个合适的堆空间
    因为堆并非越大越好,JVM的目标机器也是堆设定的一个重要参考
    一般来说,32位机堆上限不超过2G,64位的4G起步.原则上单个JVM上限不超过32G(超过32G会造成压缩指针失效,造成极大的内存浪费,后文详述)
  • -Xmx与-Xms 即堆的最大空间和初始空间设为一致
    这样的目的在于防止GC内存回收后,JVM再行依据初始空间伸缩堆
  • 如果有大量的反射与运行时加载情况,必须将方法区消耗纳入考虑范围

年轻代
年轻代是影响响应速度最重的地方

  • 设置年轻代的总大小 -XX:NewSize=N -XX:MaxNewsize=N -XX:NewRatio=N
    基于同样的原因,年轻代的初始和最大可以设置为一样.
    原则上年轻代配置必须小于老年代.因为必须考虑一个极端情况就是年轻代的整体晋升
    主要的权衡点在于,调低年轻代可以降低MinorGC的单次时长,但会增加MinorGC的频次.
    注意在某些场合比如G1,应该刻意避开手动干预年轻代防止覆盖G1的自适应优化(后文详述)
  • 还可以更加细化的干预年轻代的Eden和两个Survivor -XX:SurvivorRatio
    伊甸园区和幸存者区比例也是一个比较重要的优化点.理想情况下,我们当然希望在一次MinorGC后同时清理掉伊甸园和幸存者区.但事实每次MinorGC总会剩下相当于一部分.此时伊甸园区剩下的和幸存者区会进入另一个幸存者区,而每一个幸存者在经历一次转区后年龄增长一岁,直到达到年龄线后晋升入老年代
    而问题往往体现在.如果存在大量幸存者而幸存者区又放不下的时,会将幸存者直接忽略年龄直接晋升,其次如果幸存者是一个相对瞬时对象(相对不长又相对长的),也会导致大量幸存者因为达到年龄线也自动晋升老年代.这两种情况的后果都是,让一些我们不想其升入老年代的对象大量涌入老年代
    解决这个问题的办法就是干预幸存者区和伊甸园的大小.
    结合年龄对象分布-XX:+PrintTenuringDistribution
    如果分布大量的低年龄对象,说明很多幸存者区设小了,此时就可以调大幸存者区
    如果分布大量的高年龄对象,说明幸存者足够是自然晋升,此时可以调大晋升年龄线
  • 晋升年龄界限
    -XX:MaxTenuringThreshold=N(15) (一共四位最大15,默认15,CMS默认为6),注意该参数是最晚晋升年龄.也就是说达到15次必然晋升,但不一定必须15次才晋升.
    XX:InitialTenuringThreshold,这个是对象进入初始年龄 -XX:TargetSurvivorRatio=N(50) 存活率
    注意一点是没有直接设置年龄界限的方法.我们能设置的是初始年龄界限和最大年龄界限.
    事实上,年龄界限是动态计算的.动态计算还有一个参考指标就是存活率
    年龄界限晋升 TargetAge?>min(Maxage,MinAveAge)
    TargetAge表示对象当前年龄(可以通过初始年龄设置,每倒腾一次加一)
    Maxage表示MaxTenuringThreshold设置的最晚晋升生命
    MinAveAge表示存活率以内的平均年龄

也就说如果某一个年龄以上的对象总大小占据占比超过存活率,那么在这个年龄之上的对象无视最大年龄直接晋升
这样做的目的是为了尽可能稳定的让对象晋升.试想一下,超过5岁占比超过50%,如果依然强制15岁晋升,幸存者区会滞留大量的5-14岁,导致MinorGC大量触发

**方法区 ** 方法区(1.7永生代或1.8元空间),有时也是一个需要加以考虑的地方
从某种意义上说,方法区是一个比年轻代老年代更加脆弱的地方.首先方法区一般相对偏小(一般都不如年轻代大,跟老年代更没法比),其次方法区一般又不是重点关注对象.而方法区满的后果又相当严重,默认情况下,方法区满会直接触发FullGC
方法区存储的是常量区和类元数据,所以如果在系统中存在大量反射或运行时加载应用时,就应该把方法区纳入视线中
方法区的优化主要是避免FullGC,解决思路一般有两个
调整方法区大小,让方法区可以容纳更多的存在
元空间 -XX:MaxMetaspaceSize=N -XX:MetaspaceSize=N 永生代 -XX:PermSize=N -XX:MaxPermSize=N
将方法区纳入GC,让方法区满时单独GC而不是FullGC.(这需要特定的GC产品配合)

老年代
老年代除了代空间大小调整外,还可以尝试换一些更好的GC产品诸如CMS,G1,这些产品在自己适应的场景中都拥有更加优秀的表现,而不同的GC产品基于不同的策略也会有不同设定,并且这些设定往往需要堆代一起配合(后文详述)

GC产品

G1
G1(详细的G1专章描述)是GC的最前沿成功,也是个人理解从目前来说最优秀的GC产品
G1的核心思路用频次换停顿.即不断的只对老年代执行部分回收(分区,然后谁垃圾多就怼谁)来换取GC速度.因为提前分区和每次部分回收,G1GC正常既不会STW,单次速度也很快.但需要注意的是G1对年轻代始终保持STW回收
G1的优化思路两个: 尽可能用G1特有的MixGC代替FullGC以及尽可能让G1自适应调优工作更好

  • -XX:MaxGCPauseMillis=N(200) 来设置期望的GC最大停顿时间,然后让G1自行在运行时决定年轻代调整和回收目标,这也是G1的一大特点:可期望GC停顿
    在G1中,应该避开对年轻代调整,让G1在运行时自行调整年轻代比率(手动调整会覆盖G1这种自适应不建议使用,如果需要手动调整,G1应该刻意压低年轻代,详见后文)
  • -XX:G1HeapRegionSize=N 在应用程序存在大量大对象时(比如Spark),应该调高G1的Region (注意G1分区最小1M,最大32M,且必须是2的幂次数)
  • 为G1解决标记失败(concurrent-mark-abort)
    G1标记失败的最大可能是堆小了而应用程序的对象申请太高,这时最应该的就是调大堆空间.
    解决标记失败还可以调高GC线程数,但一般很少使用.(效果难名,提高线程不见得能解决更快)
  • 为G1解决晋升失败(to-space exhausted)或疏散失败( to-space overflow )
    这两种都会让G1的MixGC失败而直接启动FullGC
    可以调大堆空间,调高预留空间比例 -XX:G1ReserverPercent=N(10)
    或者让G1更早的启动MixGC -XX:InitiatingHeapOccupancyPercent=N(45)

  • CMS CMS是一种极限追求停顿时间的GC产品

CMS需要压低年轻代.(CMS是年轻代STW,老年代近似并发)

为CMS解决 晋升失败(promotion failed) 和 并发模式失败(concurrent mode failure)
这是两个截然不同,但可以放到一起解决的问题. 因为这两个问题的本质都是老年代爆了
解决办法
提升堆大小或降低年轻代来调高老年代大小
-XX:CMSInitiatingOccupancyFraction=N(80) 调小老年代触发GC的阈值

CMS需要解决内存碎片问题
CMS默认使用内存标记算法而非内存整理算法,所以CMS会有大量内存碎片,这会导致总有一天堆中没对象但就是分配不出对象的情况
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
在FullGC之后强制一次内存整理 + 在5次内存标记之后强制一次内存整理

CMS是一个可以应用与方法区GC的产品,如果启用CMS可以连带处理方法区
-XX:+CMSClassUnloadingEnabled

分享网上一个牛人的参数调优

该牛人主要使用的CMS,Web项目每天几百万PV感觉不到任何停顿,值得借鉴和参考

-Dresin.home=$SERVER_ROOT
-server
-Xms6000M  
-Xmx6000M #堆初始和最大都设置6G,这样可以避免GC之后内存释放导致JVM重新伸缩堆
-Xmn500M #年轻代设置相对偏小,用大量微MinorGC来换取停顿时间变短
-XX:PermSize=500M  
-XX:MaxPermSize=500M # 永生代初始和默认都500M,也可以减少堆伸缩
-XX:SurvivorRatio=65536  # 类似关闭幸存者区
-XX:MaxTenuringThreshold=0 # 去除年龄段,也就是年轻代直接晋升  
-Xnoclassgc # 抛弃方法区GC
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC  # CMSGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0 # 每次GC执行都执行内存整理
-XX:+CMSClassUnloadingEnabled # GC方法区  
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=90 # 老年代阈值90%  
-XX:SoftRefLRUPolicyMSPerMB=0 # 软连接对象即时删除
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log

可以看出这位大神的JVM调的非常极端.
可以逆推出这个应该程序应该是一个架构和业务相对简单的,理解中这应该是一个80%瞬时对象+剩余几乎都是不回收的静态引用之类的Web系统