JVM调优实战

400 阅读5分钟

实际项目中遇到的JVM问题?

tech.meituan.com/2017/12/29/…
juejin.cn/post/720514…

当线上发生OOM时怎么排查?

首先要知道会发生OOM的内存区域是哪些,一般java.lang.OutOfMemoryError后面都会跟具体哪个区发生的,比如
java.lang.OutOfMemoryError: Java heap space

java.lang.OutOfMemoryError: Metaspace

java.lang.OutOfMemoryError: unable to create new native thread

java.lang.OutOfMemoryError:GC overhead limit exceeded

java heap

这个区域可能发生OOM的原因大概有这些:

  • 堆内存分配太小了
  • 有死循环且循环内部有创建对象
  • 集合里应用了大量对象且没有及时清理
  • 某处创建了大量的大对象,比如数组
  • 短时间内QPS过大,超过了可承受的极限,QPS>1000

针对上述情况,先用jstat -gc看下堆容量是否配置太小了,第二步是分析dump文件,配置了**-XX:+HeapDumpOnOutOfMemoryError**会在发生OOM时自动保存dump文件,我一般用jdk自带的visualvm查看,然后分析具体是哪个对象占了这么多内存,再去程序里找问题。

优化方案:
1、修改配置:增加堆内存
2、通过分析dump文件定位程序问题
3、引入MQ削峰填谷,服务限流、降级(令牌桶),redis预减库存,增加机器cpu和内存

MetaSpace

这部分区域负责存储class元数据信息的,就是class类型、方法名之类的,当这部分发生OOM,那肯定是在大量的生成类,一般都是使用动态代理的一些框架造成的,javassist和JDK自带的都可以动态生成字节码在自己用classLoader加载进JVM。

unable to create new native thread

JVM可以创建的线程数由两个部分决定

  1. 最大线程数=(操作系统内存-堆内存-metaspace size)/线程栈大小
  2. 操作系统设置的最大进程数:max user processes

当出现这种情况时,先看操作系统内存使用情况free -m,排除是内存不够时,就是第二种情况了,可以先用ulimit -a查看操作系统限制,然后用grep app pthreads.log | wc -l查看已用线程数就明白了。

mp.weixin.qq.com/s/34GVlaYDO…?

GC overhead limit exceeded

先看下官方解释: 并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收 了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。
这是jdk后加的一种OOM类型,目的是为了在OOM heap space之前抛出异常,可以用这个参数让异常延后到堆内存撑满-XX:-UseGCOverheadLimit。

当线上出现CPU占用过高怎么快速定位?

  1. top命令找到CPU占用过高的进程PID
  2. top -Hp pid查看该进程中线程的资源占用情况,找到占用CPU高的线程
  3. 将第二步找到的线程id转换为16进制,printf '%x\n' tid
  4. jstack PID|grep tid(16进制) -A90 查看该线程的堆栈信息,定位具体代码

所有对象都分配在堆上吗?

不是,满足特定条件时,它们可以在(虚拟机)栈上分配内存。
这是因为Java JIT(just-in-time)编译器进行的两项优化,分别称作逃逸分析(escape analysis)和标量替换(scalar replacement)。

简单来讲,JVM中的逃逸分析可以通过分析对象引用的使用范围(即动态作用域),来决定对象是否要在堆上分配内存,也可以做一些其他方面的优化。

所谓标量,就是指JVM中无法再细分的数据,比如int、long、reference等。相对地,能够再细分的数据叫做聚合量。仍然考虑上面的例子,MyObject就是一个聚合量,因为它由两个标量a、b组成。通过逃逸分析,JVM会发现myObject没有逃逸出allocate()方法的作用域,标量替换过程就会将myObject直接拆解成a和b,也就是变成了:

  static void allocate() {
    int a = 2019;
    double b = 2019.0;
  }

可见,对象的分配完全被消灭了,而int、double都是基本数据类型,直接在栈上分配就可以了。所以,在对象不逃逸出作用域并且能够分解为纯标量表示时,对象就可以在栈上分配。

gc调优经验有吗?项目中堆大小设置多少?

内存分配

JVM内存分配可以根据活跃数据大小来定。
活跃数据:长期存活在堆的对象,也就是fullGc后老年代的大小,可以通过gc日志查看

空间倍数
总大小3-4 倍活跃数据的大小
新生代1-1.5 活跃数据的大小
老年代2-3 倍活跃数据的大小
元空间1.2-1.5 倍Full GC后的元空间占用

gc调优

通过案例分析了解到,由于跨代引用的存在,CMS在Remark阶段必须扫描整个堆。CMS为了避免扫描时新生代有很多对象,增加了可中断的预清理阶段用来等待Minor GC的发生,只是该阶段有时间限制(5s),如果超时等不到Minor GC,Remark时新生代仍然有很多对象。我们的调优策略是,通过参数CMSScavengeBeforeRemark强制Remark前进行一次Minor GC,从而降低Remark阶段的时间(remark需要STW)。
分析GC日志可以看到remark阶段的耗时和此时的新生代占比,通过这个可以得出是否需要优化

tech.meituan.com/2017/12/29/…

www.cnblogs.com/hirampeng/p…

参考美团案例tech.meituan.com/2017/12/29/…

JVM是如何避免Minor GC时扫描全堆的?

JVM引入了卡表(card table)来实现这一目的。
卡表的具体策略是将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表3被标记为脏(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。