项目介绍
代码介绍
本程序可以大概看一下,首先先启动一个线程池。这个线程池的其实容量与最大容量设置死,也就是说最多可同时容纳开启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%,内存使用率居高不下)
- 如果CPU100%,可以从两个角度出发。一个有可能是业务线程疯狂运行,比如死循环。还有一种可能,就是GC线程在疯狂的回收。因为JVM的垃圾回收也是多线程的,很容易导致CPU100%
- 在遇到内存溢出的问题时,一般我们要查看系统中哪些对象占用的比较多。本案例是一个很简单的代码。在实际业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因。站在我们的可达性分析算法的角度,JVM内存区域。还有垃圾回收器的基础。当然,如果遇到更复杂的情况,掌握的理论基础远远不止这些。
常见问题分析
超大对象
代码中创建了很多大对象 (例如数据库查询一个超级大的list报表), 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OOM
超过预期访问量
通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。比如如果一个系统高峰期的内存需求需要 2 个 G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至 OOM。
过多使用 Finalizer(一般很少)
过度使用终结器(Finalizer),对象没有立即被 GC,Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的 CPU 时间较少,因此它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出 OutOfMemoryError
内存泄漏
大对象引用没有被释放掉,JVM无法对其自动回收
长生命周期的对象持有短生命周期对象的引用
例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
变量作用域不合理
例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为 null。
内部类持有外部类
Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾回收器不能正常工作)
解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收;
Hash 值改变
在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露
内存泄漏经典案例
代码问题
代码问题和内存泄漏很大的关系,如果观察一个系统,每次进行 FullGC 发现堆空间回收的比例比较小,尤其是老年代,同时对象越来越多,这个时候可以判断是有可能发生内存泄漏。内存溢出不一定是代码问题,但是泄漏一定是。
内存泄漏
程序在申请内存后,无法释放已申请的内存空间
内存泄漏和内存溢出辨析
内存溢出:实实在在的内存空间不足导致;
内存泄漏:该释放的对象没有释放,常见于使用容器保存元素的情况下。
如何避免:
内存溢出:检查代码以及设置足够的空间
内存泄漏:一定是代码有问题
往往很多情况下,内存溢出往往是内存泄漏造成的
我们一般优化的思路有一个重要的顺序:
- 程序优化,效果通常非常大
- 扩容,如果金钱的成本比较小,不要和自己过不去;
- 参数调优,在成本、吞吐量、延迟之间找一个平衡点