jvm调优


2022/03/05

csdn 链接
https://blog.csdn.net/love_yr/article/details/121800595(有图片和目录)
JVM学什么?
(1)源码到类文件
(2)类文件到JVM
(3)JVM各种折腾[内部结构、执行方式、垃圾回收、本地调用等]
什么是JVM优化(对JVM优化的理解)
根据需求进行JVM规划和预调优

优化运行JVM运行环境(慢,卡顿)

解决JVM运行过程中出现的各种问题(OOM)

JVM的性能优化可以分为代码层面和非代码层面。
在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码提
取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。
在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。
VM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身
已经做了很多的内部优化操作。
内存溢出分类
栈内存溢出         java.lang.*Error

堆内存溢出        java.lang.OutOfMemoryError: heap

方法区内存溢出        java.lang.OutOfMemoryError: Metaspace

直接内存溢出

如何分析堆内存
怎么回答

1:设定了参数HeapDump,OOM的时候会自动产生堆转储文件

2:很多服务器备份(高可用),隔离这台服务器,使用其他的服务器

在线分析(2种):
jmap

jmap - histo 4655 | head -20,查找有多少对象产生

arthas   (线上分析)

下载arthas jar包运行,绑定到想检测的进程   

jvm观察jvm信息

thread定位线程问题

dashboard 观察系统情况(查看线程占用,内存占用)

jad反编译动态代理生成类的问题定位

第三方的类(观察代码)

版本问题(确定自己最新提交的版本是不是被使用)

redefine 热替换目前有些限制条件:只能改方法实现(方法已经运行完成),不能改方法名, 不能改属性m() -> mm()

sc - search class

watch - watch method

jconsole       

远程监控(JMX协议),需要在程序启动时指定参数,一般只在测试时候用,在线用严重影响主程序效率

jvisualVm  同jconsole       

离线分析(生成dump堆转储文件)
生成dump堆转储文件的过程占用系统资源,造成严重卡顿,不建议生成堆转储分析

获得堆转储文件的几种方式
1.堆内存溢出时自动获取

java -Xms20M -Xmx20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError com.mg.jvm.gc.T15_FullGC_Problem01

2.使用jmap命令

jmap -dump:format=b,file=xxx pid

3.使用arthas的heapdump命令

分析堆转储文件的几种工具
1.MAT

2.jhat                jhat -J-mx512M xxx.dump(就指定让其使用512M内存)

3.jvisualVm      java自带,打开后导入堆转储文件

如何选择垃圾回收器
关注的两个指标
吞吐量和停顿时间
停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间
吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)
选择收集器时的考虑
优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
如果允许停顿时间超过1秒,选择并行或JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器
何时使用并发收集器g1
(java1.8及以上,内存6G以上,优先考虑G1)

(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
G1详细介绍
G1内存分配策略

将内存分成一个一个的region,且不要求各部分是连续的。
每个Region的大小在JVM启动时就确定,JVM通常生成2000个左右的heap区, 根据堆内存的总大小,区的size范围为1-32Mb,一般4M.

region类型

三种常见: Eden, Survivor, 或 old generation(老年代)区  HumongousHumongous
巨无霸区:保存比标准region区大50%及以上的对象,存储在一组连续的区中.转移会影响GC效率,标记阶段发现巨型对象不再存活时,会被直接回收。
未使用区:未被使用的region
特别说明:某个region的类型不是固定的,比如一次ygc过后,原来的Eden的分区就会变成空闲的可用分区,随后也可能被用作分配巨型对象

基本概念
cardTable(属于堆的概念,不单属于g1)
基于卡表(Card Table)的设计,通常将堆空间划分为一系列2次幂大小的卡页(Card Page)。

卡表(Card Table),用于标记卡页的状态,每个卡表项对应一个卡页。

HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每个标记项为1个字节。

当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。

cardTable带来的2个问题
1.无条件写屏障带来的性能开销

每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作。显然,这会增加一些额外的开销。但是,与YGC时扫描整个老年代相比较,这个开销就低得多了。

不过,在高并发环境下,写屏障又带来了虚共享(false sharing)问题。

2.高并发下虚共享带来的性能开销

在高并发情况下,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。

假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。

HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB。

如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。

一个简单的解决方案,就是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表项未被标记过才将其标记为dirty。

这就是JDK 7中引入的解决方法,引入了一个新的JVM参数-XX:+UseCondCardMark,在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。

待收集集合,CSets
由G1MixedGCLiveThresholdPercent参数控制的,old代分区中的存活对象比,达到阀值时,说明该region可以被回收的对象比较多,这个old分区会被放入CSet,等待被GC。
Collection Sets,有垃圾需要被回收的region的集合。CSet中可能存放着各个分代的Region。CSet中的存活对象会在gc中被移动(复制)。GC后CSet中的region会成为可用分区。
策略:由G1MixedGCLiveThresholdPercent参数控制的,old代分区中的存活对象比,达到阀值时,这个old分区会被放入CSet,后面会被执行回收整理。

已记忆集合,RSets

RememberedSets,存储着其他分区中的对象对本分区对象的引用,每个分区有且只有一个RSet。用于提高GC效率。
YGC时,GC root主要是两类:栈空间和老年代分区到新生代分区的引用关系。所以记录老年代分区对新生代分区的引用
Mixed GC时,由于仅回收部分老年代分区,老年代分区之间的引用关系也将被使用。所以记录老年代分区之间的引用
因此,我们仅需要记录两种引用关系:老年代分区引用新生代分区,老年代分区之间的引用。
因为每次GC都会扫描所有young区对象,所以RSet只有在扫描old引用young,old引用old时会被使用。
 

G1的GC类型
1)Ygc:仅处理年轻代region
2)MixedGc:包含所有年轻代以及部分老年代Region。
3)FullGc:全堆扫描,每个Region

MixGc过程:

1)标记GCroots,一般直接复用YoungGC中的结果
2)根分区扫描(RootRegionScan)。这个阶段GC的线程可以和应用线程并发运行。其主要扫描初始标记以及之前YoungGC对象转移到的Survivor分区,并标记Survivor区中引用的对象。所以此阶段的Survivor分区也叫根分区(RootRegion)
3)并发标记(ConcurrentMark)。会并发标记所有非完全空闲的分区的存活对象,也即使用了SATB算法,标记各个分区。
4)最终标记(Remark)。主要处理SATB缓冲区,以及并发标记阶段未标记到的漏网之鱼(存活对象),会STW,可以参考上文的SATB处理。
5)清除阶段(Clean UP)。整理堆分区,调整相应的RSet(比如如果其中记录的Card中的对象都被回收,则这个卡片的也会从RSet中移除),如果识别到了完全空的分区,则会清理这个分区的RSet。这个过程会STW。
6)对存活对象进行转移(复制算法),转移到其他可用分区,所以当前的分区就变成了新的可用分区。复制转移主要是为了解决分区内的碎片问题。
 

G1的FullGc

G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发FullGC。
开始版本FullGC使用的是stop the world的单线程的Serial Old模式。
JDK10以后,Full GC已经是并行运行,在很多场景下,其表现还略优于 Parallel GC 的并行 Full GC 实现。
但是仍然要避免fgc。

G1调优的一些考虑
1.不要设置年轻代的大小,默认5%到60%,是G1动态调整暂停stw时间的依据

2 设置 XX:MaxGCPauseMillis=
其值不应该使用平均响应时间,应该考虑使用目标时间的90%或者更小作为响应时间指标. 即90%的用户(客户端/?)请求响应时间不会超过预设的目标值;

3 转移失败
survivors 或 promoted objects 进行GC时如果JVM的heap区不足就会发生提升失败(promotion failure). 堆内存不能继续
扩充,因为已经达到最大值了. 当使用 -XX:+PrintGCDetails 时将会在GC日志中显示 to-space overflow (to-空间溢出)。该操作很昂贵,原因如下:
1)GC仍继续所以空间必须被释放. 
2)拷贝失败的对象必须被放到正确的位置(tenured in place). 
3)CSet指向区域中的任何 RSets 更新都必须重新生成(regenerated). 

避免转移失败的方法:
1)增加保留内存大小, 其默认值是 10;G1保留内存大小,非必须不会使用保留内存;即增大-XX:G1ReservePercent=n
2)更早启动标记周期(marking cycle).即InitiatingHeapOccupancyPercent设置的小一点??
3)增加标记线程(marking threads)的数量. 合理设置-XX:ConcGCThreads=n

4 新生代优化-避免短生命对象进入老年代
预估每次Minor GC后存活下来对象的大小,合理的设置Survivor区,同时考虑高峰期间时,动态年龄判断条件的影响,不要让这种短生命周期对象侥幸逃脱进入老年代

G1相关的参数

-XX:MaxGCPauseMillis=200 - 设置最大GC停顿时间指标,JVM会尽力实现,但不保证. 默认值为200毫秒.
-XX:InitiatingHeapOccupancyPercent=45 - 如果老年代占据了堆内存的45%的时候,此时会触发一次mixGc。值为0则表示“一直执行GC循环)'. 默认值为45。
-XX:G1MixedGCLiveThresholdPercent:默认值是85%,确定要回收的Region的时候,必须是存活对象低于85%的Region才可以回收。
-XX:G1ReservePercent=n 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
-XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.
-XX:+UseG1GC - 让 JVM使用G1垃圾收集器, jdk9被设为默认垃圾收集器;所以如果你的版本比较新则不再需要使用该参数
-XX:MetaspaceSize=256M 元空间,默认20M,确实有点小。
-XX:MaxMetaspaceSize=512M 最大元空间

下面参数不建议修改
-XX:G1NewSizePercent=5    设置年轻代占整个堆的最小百分比,默认值是堆的5%。需要开启-XX:UnlockExperimentalVMOptions
-XX:G1MaxNewSizePercent=60    设置年轻代占整个堆的最大百分比,默认值是堆的60%。
-XX:NewRatio=n 新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=n eden/survivor 空间大小的比例(Ratio). 默认值为 8.
-XX:MaxTenuringThreshold=n 年轻代提升到年老代的最大临界值. 默认值为 15.
-XX:G1HeapRegionSize=n region大小  默认值将根据 heap size 算出最优解;1M-32M
-XX:G1MixedGCCountTarget mixed回收执行次数,默认回收次数8。
-XX:G1HeapWastePercent,默认值是5%,就是说空出来的区域大于整个堆的5%,即使未达到回收次数,也会立即停止混合回收了。
如:默认回收次数是8次,但是可能到了4次,发现空闲Region大于整个堆的5%,就不会再进行后续回收了。
 

JVM优化常见问题及工具使用
1.cpu占用过高的解决方法

  1. 使用nohup java 命令运行后台运行java程序
  2. 使用top命令查看各进程对本地内存和cpu的占用情况

3.  找到占用cpu高的进程的pid

4.  使用ps H -eo pid ,tid,%cpu |grep pid 找到这个进程下哪个线程占用cpu的比例较高

         -eo(输出哪些感兴趣的内容),这里找到的线程id是十进制的,转换成十六进制(7f99) 

或者使用top -Hp 命令也能查看进程下id的占用情况(图二)​​​​​​

 

  1. 使用jstack  进程id 查看进程下线程的详细情况

6.  根据十六进制的线程id找到对应的线程(看nid)

 

         找到问题了,原来是这个类的第八行是一个while true的死循环

2.程序迟迟得不到响应结果,也不报错
(可能发生了线程死锁)

  1. 使用nohup java运行java程序(同上)
  2. 使用top命令找到有问题的进程id(同上)

3.使用jstack查看进程里线程的详细信息,在信息的最下方有死锁的信息

found one java-level deadlock    waiting to lock XXXX

堆内存诊断(OOM)

 jamp -heap 进程id  (抓取堆内存快照,堆内存各个区域的使用情况)

 jmap - histo 4655 | head -20(查找有多少对象产生,只看前20行数据,有排序)

通过查看这个类一共产生了多少个对象,占用内存多少来判断应该是哪个类出现了问题

3、垃圾收集发生的时间是什么时候?
    Full GC = Major GC + Minor GC +Metaspace Gc;
    (1)Eden区或者S区的空间不够用的时候 ---->MinorGC
    (2)老年代的空间不够用的时候  ----->Major GC  出发MajorGC往往会伴随着Full GC    
    (3)方法区不够用了也会出发GC
    (4)System.gc()方法调用的时候,但是这只是向虚拟机发出一个 指令。但是什么时候发生回收,还不能确定。只能让虚拟机自己决定。

4、如果Full GC频繁怎么办?或者如何减少Full GC的次数?

    适当的将Yong区增大。 设置YOng和Old的占比。尽可能让对象在Yong区进行回收    

5、如果GC的次数频繁,会怎么办?
    首先拿到gc的日志。然后通过工具进行分析日志。如果是堆内存空间不够用,则要适当的增加堆内存。也许是选择的垃圾回收器不太合适。 如果用的G1垃圾回收器,查看设置的停顿时间是否太严格了。或者堆内存使用比例是不是了。

6、如果cpu飙升怎么办?
    使用top命令查看时哪个进程占用cpu比较大
    (1)因为并发量太大了。导致一些占用cpu的运算一直处于运算中
            解决方案:搭建集群。增加MQ延缓代码的处理
    (2)查看线程是否存在死循环

7、如果发生了OOM怎么办?
    通过dump文件查看oom。分析dump文件  工具MAT
 
内存泄漏和内存溢出有区别嘛?
    内存泄漏是指:哪些对象没法进行回收,持续的占用内存空间。
    内存溢出:OMM是指内存没法装下对象
 
方法去中的回收主要是什么内容?
    没有用的类的信息、常量、静态变量
    
再问:类的信息什么时候被回收?
        (1)堆中不再有该对象
        (2)加载该类的classCloader已经被回收。因为classLoader是可以作为GC Root的
        (3)java.lang.Class对象也不再有任何地方引用了

8、不可达的对象一定会被回收嘛?
    finalize()方法可以自救。

上一篇:MySQL索引下推,原来这么简单!


下一篇:开源的PaaS方案:在OpenStack上部署CloudFoundry (五)常见问题