JVM参数及调优
Posted ~无关风月~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM参数及调优相关的知识,希望对你有一定的参考价值。
JVM控制参数
服务器的 Xmx 和 Xms 设置一般应该设置相同避免每次 GC 后都要调整虚拟机堆的大小
参数 | 含义 | 说明 |
---|---|---|
-XX:InitialHeapSize=100M | 初始化堆大小 | 简写-Xms100M |
-XX:MaxHeapSize=100M | 最大堆大小,默认是物理内存的 1/4 | 简写-Xmx100M |
-XX:NewSize=20M | 新生代最小空间大小 | |
-XX:MaxNewSize=50M | 新生代最大空间大小 | |
-Xmn | 设置堆内新生代的大小 | 相当于设置了相同的-XX:NewSize和-XX:MaxNewSize |
-XX:SurvivorRatio | 两个S区和Eden区的比值 | 比如-XX:SurvivorRatio=8,也就 是(S0+S1):Eden=2:8,也就是一个S占整个新生代的1/10 |
-XX:OldSize=50M | 设置老年代大小 | |
-XXPermSize=8m -XXMaxPermSize=8m | 设置方法区大小 | JDK1.8以后失效 |
-XX:MetaspaceSize=50M | 设置方法区大小 | |
-XX:MaxMetaspaceSize=50M | 设置方法区最大大小 | |
-XX:NewRatio | 新老生代的比值 | 比如-XX:Ratio=4,则表示新生代: 老年代=1:4,也就是新生代占整 个堆内存的1/5 |
-XX:MaxTenuringThreshold=6 | 提升年老代的最大临界值 | 默认值为 15 |
-Xss128k | 设置每个线程可使用的栈空间大小,每个线程都有他自己的 Stack | 经验值是3000-5000最佳 |
-XX:G1HeapWastePercent | 允许的浪费堆空间的占比 | 默认是10%,如果并发标记可回收的空间小于10%,则不会触发 MixedGC。 |
垃圾收集器相关:
参数 | 含义 | 说明 |
---|---|---|
-XX:+UseParallelGC | 使用UseParallelGC | 新生代,吞吐量优先 |
-XX:+UseParallelOldGC | 使用UseParallelOldGC | 老年代,吞吐量优先 |
-XX:+UseConcMarkSweepGC | 使用CMS | 老年代,停顿时间优先 |
-XX:+UseG1GC | 使用G1 | 新生代,老年代,停顿时间优先 |
-XX:MaxGCPauseMillis=200ms | G1最大停顿时间 | 暂停时间不能太小,太小的话就会导致出现G1跟不上垃圾产生的 速度。最终退化成Full GC。所以 对这个参数的调优是一个持续的 过程,逐步调整到最佳状态。 |
-XX:InitiatingHeapOccupancyPercent | 启动并发GC周期时堆内存使用占比 | G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示”一直执行GC循环”. 默认值为 45. |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量 | 默认值随JVM运行的平台不同而不同 |
-XX:G1MixedGCLiveThresholdPercent=65 | 混合垃圾回收周期中要包括的旧区域设置占用率阈值 | 默认占用率为 65% |
-XX:G1MixedGCCountTarget=8 | 设置标记周期完成后,对存活数据上限为 G1MixedGCLIveThresholdPercent 的旧区域执行混合垃圾回收的目标次数 | 默认8次混合垃圾回收,混合回收的目标是要控制在此目标次数以内 |
-XX:G1OldCSetRegionThresholdPercent=1 | 描述Mixed GC时,Old Region被加入到CSet中 | 默认情况下,G1只把10%的Old Region加入到CSet中 |
其他:
参数 | 含义 | 说明 |
---|---|---|
-XX:+HeapDumpOnOutOfMemoryError | 启动堆内存溢出打印 | 当JVM堆内存发生溢出时,也就 是OOM,自动生成dump文件 |
-XX:HeapDumpPath=heap.hprof | 指定堆内存溢出打印目录 | 表示在当前目录生成一个 heap.hprof文件 |
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:g1- gc.log | 打印出GC日志 | 可以使用不同的垃圾收集器,对 比查看GC情况 |
-XX:CICompilerCount=3 | 最大并行编译数 | 如果设置大于1,虽然编译速度会提高,但是同样影响系统稳定性,会增加JVM崩溃的可能 |
标准参数
-version
-help
-server
-cp
-X参数
非标准参数,也就是在JDK各个版本中可能会变动
-Xint 解释执行
-Xcomp 第一次使用就编译成本地代码
-Xmixed 混合模式,JVM自己来决定
-XX参数
使用得最多的参数类型
非标准化参数,相对不稳定,主要用于JVM调优和Debug
a.Boolean类型
格式:-XX:[±] +或-表示启用或者禁用name属性
比如:
-XX:+UseConcMarkSweepGC 表示启用CMS类型的垃圾回收器
-XX:+UseG1GC 表示启用G1类型的垃圾回收器
b.非Boolean类型
格式:-XX=表示name属性的值是value 比如:-XX:MaxGCPauseMillis=500
-Xms1000M等价于-XX:InitialHeapSize=1000M
-Xmx1000M等价于-XX:MaxHeapSize=1000M
-Xss100等价于-XX:ThreadStackSize=100
查看参数
java -XX:+PrintFlagsFinal -version > flags.txt
设置参数的常见方式
- 开发工具中设置比如IDEA,eclipse
- 运行jar包的时候:java -XX:+UseG1GC xxx.jar
- web容器比如tomcat,可以在脚本中进行设置
- 通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)
JVM调试命令
jps
JVM Process Status Tool,列出指定系统内所有的HotSpot虚拟机进程。
jstat
JVM statistics monitoring,用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
查看类装载信息:
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
查看垃圾收集信息:
jstat -gc PID 1000 10
jmap
JVM Memory Map 用于生成 heap dump文件,还可以查询 finalize 执行队列、java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
打印出堆内存相关信息:
jmap -heap PID
dump出堆内存相关信息:
jmap -dump:format=b,file=heap.hprof PID
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
设置堆内存大小: -Xms20M -Xmx20M
启动,然后访问localhost:9090/heap,使得堆内存溢出
jhat
JVM Heap Analysis Tool 与 jmap 搭配使用,分析jmap生成的dump ,jhat 内置了一个微型html服务器,可以在浏览器查看dump分析结果。
jstack
stack trace for java 用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁,死循环,请求外部资源导致的长时间等待等。
用法
jstack PID
把打印信息拉到最后可以发现
排查死锁案例:
public class DeadLockDemo
public static void main(String[] args)
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
//定义锁对象
class MyLock
public static Object obj1 = new Object();
public static Object obj2 = new Object();
class DeadLock implements Runnable
private boolean flag;
DeadLock(boolean flag)
this.flag = flag;
public void run()
if (flag)
while (true)
synchronized (MyLock.obj1)
System.out.println(Thread.currentThread().getName() + "---if获得obj1锁");
synchronized (MyLock.obj2)
System.out.println(Thread.currentThread().getName() + "---if获得obj2锁");
else
while (true)
synchronized (MyLock.obj2)
System.out.println(Thread.currentThread().getName() + "---else获得obj2锁");
synchronized (MyLock.obj1)
System.out.println(Thread.currentThread().getName() + "---else获得obj1锁");
jinfo
实时查看和调整JVM各项参数。
查看:
jinfo -flag name PID 查看某个java进程的name属性的值
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
修改:
参数只有被标记为manageable 的 flags可以被实时修改
jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID
查看曾经赋过值的一些参数
jinfo -flags PID
JVM调试工具
jdk/bin目录下
-
jconsole
(Java Monitoring and Management Console)是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控,是一个基于JMX(java management extensions)的GUI性能监测工具。jconsole使用jvm的扩展机制获取并展示虚拟机中运行的应用程序的性能和资源消耗等信息。
VisualVM(从 Oracle JDK 9 开始,VisualVM 已经不再包含在 JDK 安装包中)
VisualVM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机上运行的Java 应用程序的详细信息。VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查看本地应用程序以及远程主机上运行的应用程序的相关数据。此外,还可以捕获有关 JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。 -
VisualVM,JDK发布的功能强大的运行监视和故障处理、性能分析程序。支持插件扩展。包括概述(JVM参数、系统属性)、监视(CPU使用情况、内存信息、类装载图、线程图)、线程监控(各线程运行状态)、VirtualGC(查看堆各部分内存使用情况)、生成查看堆转储快照、Btrace动态日志跟踪,在字节码层面上对代码进行跟踪,编写代码获取程序方法的参数、返回值等信息(在运行环境特别有用)。
第三方:
- MAT ,Eclipse Memory Analysis Tools ,eclipse插件
- gcviewer,用gcviewer 打开gc log可以很直观的查看gc log
这里有一个相对特殊的部分,就是是堆外内存中的直接内存,前面的工具基本不适用,可以使用 JDK 自带的 Native Memory Tracking(NMT)特性,它会从 JVM 本地内存分配的角度进行解读。
JVM性能调优
JVM的性能优化可以分为代码层面和非代码层面。
在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。
在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。
注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身 已经做了很多的内部优化操作。
内存
内存分配
场景:60s 秒杀活动
预估:订单数 3000/s
预算:3台4核8G服务器
每台服务器:8G内存,分配4-6G给堆(最多不要超过总内存的80%)。
我们按照分4G给堆算:新生代分1333M,老年代2667M。
新生代 Eden区 = 1333M * 0.8 = 1066M。
三台机器做负载均衡,每台机器按要承担 1200/s 算。
一个订单对象Order占多大内存=对象头+实例数据+对齐填充=1KB 算。
订单会关联查商品、购物车等,假设一共关联20个对象,1KB * 20 * 1200 = 24M,及一个服务器一秒钟会产生24M大小的对象。
堆内存第一次使用时:1066M/24M = 44,及第44秒后会触发yong GC ,可能产生对象晋升,对象晋升可能产生Full GC。
增大新生代到3000M,3000M * 0.8 / 24 = 100 ,可以抗100s ,满足业务需求。
本方式不适用于G1,G1 不要设置新生代大小,G1的新生代和老年代会动态变化,固定的新生代大小会阻碍设置的停顿时间的目标。
内存溢出(OOM)
一般会有两个原因:
(1)大并发情况下
(2)内存泄露导致内存溢出
大并发[秒杀]
浏览器缓存、本地缓存、验证码
CDN静态资源服务器
集群+负载均衡
动静态资源分离、限流[基于令牌桶、漏桶算法]
应用级别缓存、接口防刷限流、队列、Tomcat性能优化
异步消息中间件
Redis热点数据对象缓存
分布式锁、数据库锁
5分钟之内没有支付,取消订单、恢复库存等
内存泄露导致内存溢出
ThreadLocal 引起的内存泄露,最终导致内存溢出
public class TLController
@RequestMapping(value = "/tl")
public String tl(HttpServletRequest request)
ThreadLocal<Byte[]> tl = new ThreadLocal<Byte[]>();
// 1MB
tl.set(new Byte[1024*1024]);
return "ok";
(1)上传到阿里云服务器
jvm-case-0.0.1-SNAPSHOT.jar
(2)启动
java -jar -Xms1000M -Xmx1000M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=jvm.hprof jvm-case-0.0.1-SNAPSHOT.jar
(3)使用jmeter模拟10000次并发
39.100.39.63:8080/tl
(4)top命令查看
top
top -Hp PID
(5)jstack查看线程情况,发现没有死锁或者IO阻塞的情况
jstack PID
java -jar arthas.jar —> thread
(6)查看堆内存的使用,发现堆内存的使用率已经高达88.95%
jmap -heap PID
java -jar arthas.jar —> dashboard
(7)此时可以大体判断出来,发生了内存泄露从而导致的内存溢出,那怎么排查呢?
jmap -histo:live PID | more
获取到jvm.hprof文件,上传到指定的工具分析,比如heaphero.io
GC
这里以G1垃圾收集器调优为例
是否选用G1
官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases
(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
G1调优
(1)使用G1GC垃圾收集器: -XX:+UseG1GC
修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
99.16% 0.00016s 0.0137s 0.00559s 12
(2)调整内存大小再获取gc日志分析
-XX:MetaspaceSize=100M
-Xms300M
-Xmx300M
比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
98.89% 0.00021s 0.01531s 0.00538s 12
(3)调整最大停顿时间
-XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标
比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
98.96% 0.00015s 0.01737s 0.00574s 12
(4)启动并发GC时堆内存占用百分比
-XX:InitiatingHeapOccupancyPercent=45
G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行 GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
比如设置该百分比参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
98.11% 0.00406s 0.00532s 0.00469s 12
G1调优最佳实战
官网 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
(1)不要手动设置新生代和老年代的大小,只要设置整个堆的大小
why:https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc
G1收集器在运行过程中,会自己调整新生代和老年代的大小 其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标 如果手动设置了大小就意味着放弃了G1的自动调优
(2)不断调优暂停时间目标
一般情况下这个值设置到100ms 或者 200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。
(3)使用 -XX:ConcGCThreads=n 来增加标记线程的数量
IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。
IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高 ConcGCThreads。
(4)MixedGC调优
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarger
-XX:G1OldCSetRegionThresholdPercent
(5)适当增加堆内存大小
(6)不正常的Full GC
有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是由Metaspace 区域引起的。可以通过MetaspaceSize适当增加其大小,比如256M。
常见问题
(1)内存泄漏与内存溢出的区别
内存泄漏:程序在申请内存后,无法释放已申请的内存空间 (程序实际不用了,但是引用还在,JVM不能回收)。内存泄漏堆积后的后果就是内存溢出。
内存溢出:指程序申请内存时,没有足够的内存供申请者使用。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。
(2)young gc会有stw吗?
不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关 系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS 和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时 间相对来说就小很多了。
(3)major gc和full gc的区别
Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC 和 Full GC。
一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。
触发 Full GC 的原因有很多:
- 当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时;
- 当老年代的空间使用率超过某阈值时;
- 当元空间不足时(JDK1.7 永久代不足);
- 当调用 System.gc() 。
(4)什么是直接内存
Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通 常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内 存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx 指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
(5)垃圾判断的方式
1、引用计数法:每一个对象有一个引用计数属性,每新增一个引用,该计数加1,引用释放时,计数减1,计数为0时,表示可回收。
弊端:无法解决对象相互引用的问题,两对象相互引用,他们的计数属性都大于1,但两对象都不再使用,却无法被回收。
2、可达性分析:从GCRoot开始,向下搜索,当一个对象到GCRoot有引用链相连时,是可达的,不用回收。
可作为GC Roots的对象包括:
- JVM桟(桟帧中的本地变量表)中引用的对象;
- 本地方法桟中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象。
(6)不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;
可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
(7)为什么要区分新生代和老年代?
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
(8)G1与CMS的区别是什么
CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC; G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。
(9)方法区中的无用类回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。
类需要同时满足下面 3 个条件才能算是 “无用的类” :
a-该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
b-加载该类的 ClassLoader 已经被回收。
c-该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
以上是关于JVM参数及调优的主要内容,如果未能解决你的问题,请参考以下文章