JVM调优思路与分析实战

项目介绍

 

代码介绍

 

本程序可以大概看一下,首先先启动一个线程池。这个线程池的其实容量与最大容量设置死,也就是说最多可同时容纳开启50个线程执行任务。并且队列使用的无界队列。

在 Linux 服务跑起来

java -cp JVMOTHERS-1.0-SNAPSHOT.jar -XX:+PrintGC -Xms200M -Xmx200M JVM调优/FullGCProblem

(把堆空间设置小,提前抛出OOM)。

CPU占用过高排查实战

1.先通过top命令找到消耗CPU很高的进程id。

top 命令是我们在 Linux 下最常用的命令之一,它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息。有人会问,为什么不直接使用jps查看进程呢?是因为我们此时首先需要判断是不是我们的程序导致电脑变慢变卡。

 

好家伙,结果一看这哥们儿把我CPU抢的裤衩子都快保不住了。

2.执行 top -p 15110 单独监控该进程

3.在第2步的基础上输入H(或者直接输入命令top -Hp 2732),获取该进程下所有的线程信息

4.通过观察,我们找到线程编号为15112,15113,15114,15115的线程非常消耗CPU。(需要运行一段时间)

 

我们此时就需要回忆一下,我们的基础JDK命令行调优工具中,jstack可以为我们截取一个线程快照。在这个快照中,我们或许可以分析出该线程的一些信息。

5.执行jstatck 15110,此时这个快照中保存这线程的信息

 

这些nid其实就是jstack显示出来的进程下115110的线程号。ox代表16进制。因此可以百度查找转换工具,把我们之前得到的线程号转换为16进制。以线程15112为例:

6.定位问题

 

16进制下的值为3b08。我们再到jstack的日志中查询。

 

这一看全明白了。全是GC线程抢我的CPU资源。于是我们使用GC监控指令jstat来统计并查看GC情况。

7.使用jstat监控GC

是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

假设需要每 2.5 s查询一次进程 15110 垃圾收集状况,一共查询 10 次,那命令应当是:jstat-gc 15110 2500 10

 

我们发现2.5s内 就能进行个7,8次。

 

在我们的程序运行界面也出现了OOM的错误。

S0C:第一个幸存区的大小

S1C:第二个幸存区的大小

S0U:第一个幸存区的使用大小

S1U:第二个幸存区的使用大小

EC:伊甸园区的大小

EU:伊甸园区的使用大小

OC:老年代大小

OU:老年代使用大小

MC:方法区大小

MU:方法区使用大小

CCSC:压缩类空间大小

CCSU:压缩类空间使用大小

YGC:年轻代垃圾回收次数

YGCT:年轻代垃圾回收消耗时间

FGC:老年代垃圾回收次数

FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间

怎么办?OOM 了.

我们可以看到,这个里面 CPU 占用过高是什么导致的?

是业务线程吗?不是的,这个是 GC 线程占用过高导致的。JVM 在疯狂的进行垃圾回收,再回顾下之前的知识,JVM 中默认的垃圾回收器是多线程的(回顾下之前的知识),所以多线程在疯狂回收,导致 CPU 占用过高。

内存占用过高的解决思路

用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。jmap 的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalize 执行队列、Java 堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。和 jinfo 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的-dump 选项和用于查看每个类的实例、

空间占用统计的-histo 选项在所有操作系统可以使用

 

打印前20个对象

jmap –histo 15110 | head -20

 

可以看到图中的这些类对应的对象共有80多万个实例。这也就是我们的问题所在

由代码分析原因

一般来说,看到前面这几行,就可以看出到底是哪些对象占用了内存。

这些对象回收不掉么?

 

可以看到回收很频繁,基本没效果。那么为什么发生了FullGC还无法回收掉这些对象呢?我们再回到我们的代码。

 

 

我们可以看到此处使用的是无界队列

 

线程池可以同时执行50个任务

 

但每次产生的任务有100个。也就是说50个任务会在线程池中,而另50个任务只能在阻塞队列中。我们以100笔/0.1秒的速度向线程池中丢任务(第一次还能丢50个到线程池中,第二次直接全部丢到队列中了)。而线程池同时只能保持50个任务,并且每3s完成1个任务。每一个任务又对应了它的UserInfo对象。这些在队列中的对象是不会被回收的。因此营造出一个几乎只能无限增长的堆内存。

 

总结

在JVM出现性能问题的时候(表现上是CPU100%,内存使用率居高不下)

  1. 如果CPU100%,可以从两个角度出发。一个有可能是业务线程疯狂运行,比如死循环。还有一种可能,就是GC线程在疯狂的回收。因为JVM的垃圾回收也是多线程的,很容易导致CPU100%
  2. 在遇到内存溢出的问题时,一般我们要查看系统中哪些对象占用的比较多。本案例是一个很简单的代码。在实际业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因。站在我们的可达性分析算法的角度,JVM内存区域。还有垃圾回收器的基础。当然,如果遇到更复杂的情况,掌握的理论基础远远不止这些。

常见问题分析

超大对象

代码中创建了很多大对象 (例如数据库查询一个超级大的list报表), 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OOM

超过预期访问量

通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。比如如果一个系统高峰期的内存需求需要 2 个 G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至 OOM。

过多使用 Finalizer(一般很少)

过度使用终结器(Finalizer),对象没有立即被 GC,Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的 CPU 时间较少,因此它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出 OutOfMemoryError

内存泄漏

大对象引用没有被释放掉,JVM无法对其自动回收

长生命周期的对象持有短生命周期对象的引用

例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏

连接未关闭

如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。

变量作用域不合理

例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为 null。

内部类持有外部类

Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾回收器不能正常工作)

解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收;

Hash 值改变

在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露

内存泄漏经典案例

 

代码问题

代码问题和内存泄漏很大的关系,如果观察一个系统,每次进行 FullGC 发现堆空间回收的比例比较小,尤其是老年代,同时对象越来越多,这个时候可以判断是有可能发生内存泄漏。内存溢出不一定是代码问题,但是泄漏一定是。

内存泄漏

程序在申请内存后,无法释放已申请的内存空间

内存泄漏和内存溢出辨析

内存溢出:实实在在的内存空间不足导致;

内存泄漏:该释放的对象没有释放,常见于使用容器保存元素的情况下。

如何避免:

内存溢出:检查代码以及设置足够的空间

内存泄漏:一定是代码有问题

往往很多情况下,内存溢出往往是内存泄漏造成的

我们一般优化的思路有一个重要的顺序:

  1. 程序优化,效果通常非常大
  2. 扩容,如果金钱的成本比较小,不要和自己过不去;
  3. 参数调优,在成本、吞吐量、延迟之间找一个平衡点

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/110669470