性能调优之JVM调优(四)GC性能指标及调优实战

什么是jvm调优

调优简单说就是把整个系统进行优化:

根据需求进行JVM规划和预调优,比如为程序设置合适的内存、选择合适的垃圾回收器等
当程序出现慢或者卡顿现象时,优化JVM运行环境
解决JVM运行中出现的各种问题,比如OOM

GC性能指标

  • 吞吐量: 工作线程运行时间占比总运行时间之比
    用户程序执行时间/(用户程序执行时间+垃圾回收时间)

单位时间内,STW 的时间最短 (发生2次STW,0.2+0.2=0.4),垃圾回收时间占比最低,这样就称吞吐量高.
业务场景:比如科学计算就要求吞吐量优先。
垃圾回收器:PS+PO

  • 暂停时间: 在执行垃圾回收的时候, 工作线程被暂停的时间
    单位时间内,可能发生5次STW,但是单次的STW时间最短(0.1+0.1+0.1+0.1+0.1=0.5)
    业务场景:电商网站,CSDN,对外提供的API等这类服务响应时间要优先。
    垃圾回收器:CMS,G1
  • 内存占用: Java堆内存占用的大小
  • 收集频率: 垃圾收集器工作的频率
  • 收集效率: 一个对象诞生到死亡的时间
    前三条是GC的矛盾之处, 如果要提高工作线程的吞吐量, 就必须降低工作线程的暂停时间, 那么垃圾收集的时间就必然降低, 内存占用就会提高
    高吞吐量这会让应用程序的用户感觉只有应用程序线程在做“生产性”工作。 所以直觉上,吞吐量越高程序运行越快。
    低暂停时间是 从用户的角度来看不管是GC还是其他原因导致一个工作线程被挂起始终是不好的。
    这于这种矛盾的取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。 因此,具有低的最大暂停时间是非常重要的,特别是对于一个交互式应用程序。
    综上所述,在设计(或使用)GC算法时​​,我们必须确定我们的目标:一个GC算法​​只可能针对两个目标之一(即只专注于最大吞吐量和最小合理暂停时间),或尝试找到一个二者的折衷。

HotSpot虚拟机上的垃圾收集

现有的垃圾回收器的目标是: 在保证最大吞吐量优先的情况下,合理降低停顿时间
对于一个对象HotSpot抛弃了使用引用计数的方式判断一个普通对象的生死, 而是使用可达性分析算法来判断一个对象的生死
其核心思想就是: 通过一系列的GC Roots对象作为起点开始向下搜素, 搜索走过的的路程称之为引用链, 对于一个对象, 如果没有一个引用链与GCRoot相连, 那么它就是一个不可达的对象, 虚拟机就会证明此对象不可用
但是在可达性分析算法中不可达的对象,也并非"非死不可"的,这时候他们暂时处在"缓刑"阶段。要宣告一个对象的真正死亡,至少要经历两次标记过程 :
如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是真正"死"的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。
但是对于类对象的删除的要求的非常严格的, 删除一个类对选哪个要满足下面三个条件

  • 该类的所有实例都已经回收
  • 该类的类加载器都已经删除
  • 该类的对应Class对象没有在任何地方引用, 并且这个类对象没有在任何地方通过反射访问过该类
    只有满足上述三个条件. 回收器才会考虑回收回收这个类对象
    而对于真正的回收算法. 有
  • 标记清除算法, 但是会产生大量的内存碎片
  • 标记复制算法, 需要较大的空间, 而且复制起来比较耗时, 但是对于分代回收思想的年轻代, 可以使用复制算法
  • 标记整理算法, 对于老年代来说很友好, 因为其没有较大的内存空间, 他的思想就是将有用对象进行整理,防止出现内存碎片, 所以在实际的老年代回收提出了标记-清整算法, 就是先进行标记清除, 害怕出现内存碎片, 就再进行一次标记整理算法
  • 分代收集算法, 对于这个算法而言就是把堆区分为年轻代和老年代, 年轻代存放一些刚生产的对象, 老年代放置一些大对象或者经年轻代没杀死的对象, 对于年轻代他们使用Minor GC, 该思想采用复制算法, 收集频率也高, 回收速度也快
    虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间,如果大于的话,直接执行minorGC。
    对于老年代和永久代他们使用full GC , 这个的收集频率就比较慢, 一般是快满的时候才进行一次, 采用标记整理算法, 速度会比Minor GC慢10倍左右
    显示调用System.gc()方法也是直接调用Full GC

如何进行调优

1、熟悉业务场景,选择合适的垃圾回收器

如果是响应时间优先,选择CMS、G1、ZGC;
如果是吞吐量优先,就选择ParallelGC(PS+PO)
2、计算内存需求,给JVM设置合适的内存大小

新生代能容纳所有【并发量 * (请求-响应)】的数据
幸存区大到能保留【当前活跃对象+需要晋升对象】
年轻代应该占整个堆内存的25%到50%
晋升阈值配置得当,让长时间存活对象尽快晋升
3、选定CPU,越高越好

4、设定日志参数
-xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFile=5
-XX:GCLogFileSize=20M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCCause
在生产环境中日志文件,日志名字按照系统时间产生,循环产生,日志个数5个,每个大小20M,这样整体大小100M,便于控制,方便查找问题。

5、进行压测监控JMeter

系统cpu经常100%,如何调优

1、cpu100%那么一定是有线程在占用系统资源,先找到是哪个进程cpu占用高(top)
2、找到该进程中的哪个线程cpu高(top -H -p )
3、导出该线程的堆栈,需要将线程id从十进制转为十六进制,因为java线程栈文件中的线程id是十六进制(jstack PID 或 jstack PID > pid.tdump 导出文件)
4、查找哪个方法(栈帧)消耗时间(jstack)
5、工作线程占比高还是垃圾回收线程占比高

系统内存飙高,如何查找问题

1、通过jmap命令导出堆文件( jmap -dump:live,format=b,file=dump.hprof PID)
2、使用jhat、MAT、jvisualvm工具对dump文件进行分析

如何处理大对象?

大对象对于JVM来说是个噩耗。如果对象过大,当前新生代的剩余空间装不下它,那么就需要使用分配担保机制,将当前新生代的对象都复制到老年代中,给大对象腾出空间。分配担保涉及到大量的复制,因此效率很低。

那么,如果将大对象直接放入老年代,虽然避免了分配担保过程,但该对象只有当Full GC时才能被回收,而Full GC的代价是高昂的。如果大对象过多时,老年代很快就装满了,这时就需要进行Full GC,如果Full GC频率过高,程序就会变得很卡。

因此,对于大对象,有如下几种处理方法:

  1. 在写程序的时候尽量避免大对象
    从源头降低大对象的出现,尽量选择空间利用率较高的数据结构存储。
  2. 尽量缩短大对象的有效时间
    对象用完后尽快让它失效,好让垃圾收集器尽快将他回收,避免因在新生代呆的时间过长而进入老年代。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值