JVM实战—10.MAT的使用和JVM优化总结

大纲

1.线上大促活动导致的老年代内存泄漏和FGC(MAT分析出本地缓存没处理好)

2.百万级数据误处理导致频繁FGC(大数据量加载到内存处理 + String.split())

3.JVM运行原理和GC原理总结

4.JVM性能优化的思路和步骤

5.问题汇总

1.线上大促活动导致的老年代内存泄漏和FGC(MAT分析出本地缓存没处理好)

(1)线上故障场景

(2)初步排查CPU负载过高的原因

(3)初步排查频繁FGC的问题

(4)对线上系统导出一份内存快照

(5)MAT是如何使用的

(6)基于MAT来进行内存泄漏分析

(1)线上故障场景

一.业务的背景

在某个特定节日里,线上推了一个大促销活动。即给所有用户发短信、邮件、APP Push消息,告知有特别优惠的活动。这类大促活动一般会吸引比平时多几倍的用户短时间内登录APP来参与,所以系统一般在这个时候压力会比平时大好几倍。给这个业务的数据库缓存和机器资源都是足够的,通常不应该有问题。

二.出现的问题

但是那次大促活动开始后,线上系统出现了CPU使用率飙升。而且因CPU使用率太高,导致系统陷入卡死状态,无法处理任何请求。重启系统后会好一段时间,但很快CPU使用率又飙升,导致系统又卡死。这就是那次大促活动开始后,那个系统在线上的一个真实的情况。

(2)初步排查CPU负载过高的原因

一.机器CPU负载过高有两个原因

**原因一:**在系统里创建了大量线程,这些线程同时并发运行,且工作负载都很重,过多的线程同时并发运行就会导致机器CPU负载过高

**原因二:**机器上运行的JVM在执行频繁的FGC,FGC会非常耗费CPU资源,它也是一个非常重负载的过程

二.频繁FGC会导致的两个现象

**现象一:**系统可能时不时因为FGC的STW而卡顿

**现象二:**机器的CPU负载很高

三.排查CPU负载过高的原因

知道CPU负载过高的两个原因后,就很容易进行排查了,这时候完全可以使用排除法来做。首先看一下JVM FGC的频率,通过jstat或监控平台可以很容易看到现在FGC的频率。如果FGC频率过高,就是FGC引起的CPU负载过高。如果FGC频率正常,就是系统创建了过多线程并发执行负载很重的任务。

所以当时直接通过监控平台就可以看到:JVM的FGC频率变得极为频繁,几乎是每分钟都有一次FGC。每分钟一次FGC,一次至少耗时几百毫秒,可见这个系统性能很糟糕。

(3)初步排查频繁FGC的问题

出现频繁FGC一般有三个可能:

**可能一:**内存分配不合理或高并发,导致对象频繁进入老年代,引发频繁FGC

**可能二:**存在内存泄漏,即内存里驻留了大量对象塞满了老年代且无法回收,导致稍微有一些对象进入老年代就会引发FGC

**可能三:**Metaspace里的类太多,触发了FGC

当然如果上述三个原因都不存在,但是还是有频繁FGC,也许就是工程师错误的执行System.gc()导致的了。但这个一般很少见,而且JVM参数中可以禁止这种显式触发的GC。

一般排查频繁FGC,核心利器就是jstat了。当时使用jstat分析了一下线上系统的情况,发现并不存在内存分配不合理导致对象频繁进入老年代的问题,而且永久代的内存使用也很正常,所以排除掉了上述三个原因中的两个。

那么接下来考虑最后一个原因:老年代里是不是驻留了大量的对象。是的,当时系统就是这个问题。

通过jstat可以明显发现老年代驻留了大量的对象,几乎快塞满了。所以年轻代稍微有一些对象进入老年代,就会很容易触发FGC。而且FGC后还回收不了老年代里大量的对象,只能回收一小部分而已。所以老年代里驻留了大量本不应该存在的对象,才导致频繁触发FGC。

接下来就是要想办法找到这些对象了,前面介绍过jmap + jhat的组合来分析内存里的大对象,接下来介绍另外一个常用的强有力的工具MAT。

jhat适合快速的去分析一下内存快照,但是功能上不是太强大,所以一般会使用比较强大的而且也特别常用的内存分析工具MAT。

(4)对线上系统导出一份内存快照

既然发现老年代中驻留了过多对象,那么肯定要知道这些对象是什么。所以先用jmap命令导出一份线上系统的内存快照,命令如下:

复制代码
$ jmap -dump:format=b,file=文件名 [服务进程ID]

拿到的内存快照其实就是一份文件,可用jhat、MAT等工具来分析内存。

(5)MAT是如何使用的

如果开发工具是Eclipse,那么可通过Eclipse集成的MAT插件来使用的。如果开发工具是IDEA,那么可以直接下载一个MAT来使用即可。官网的下载地址如下,在这个地址中,可以下载MAT的最新版本。

复制代码
https://www.eclipse.org/mat/downloads.php

下载好MAT后,在其安装目录里可看到一个叫MemoryAnalyzer.ini文件,这个文件里的内容大概如下所示:

复制代码
 -startup
 ../Eclipse/plugins/org.eclipse.equinox.launcher_1.5.0.v20180512-1130.jar
 --launcher.library
 ../Eclipse/plugins/org.eclipse.equinox.launcher.cocoa.macosx.x86_64_1.1.700.v20180518-1200
 -vmargs
 -Xmx1024m
 -Dorg.eclipse.swt.internal.carbon.smallFonts
 -XstartOnFirstThread

需要注意的是:如果dump出来的内存快照很大,比如有几个G,那么务必在启动MAT前在这个配置文件里设置MAT的堆内存大小。比如设置为4个G或者8个G,因为这里默认的-Xmx1024m表示只有1G。

接着直接启动MAT,启动后看到的界面中有一个选型是:Open a Heap Dump。Open a Heap Dump就是打开一个内存快照的意思。选择Open a Heap Dump,然后选择本地的一个内存快照文件打开。

(6)基于MAT来进行内存泄漏分析

使用MAT打开一个内存快照后,MAT上有一个工具栏,里面有一个按钮。这个按钮的英文是:Leak Suspects,就是内存泄漏的分析。

接着MAT会分析选择的内存快照,尝试找出导致内存泄漏的一批对象。这时可以看到它会显示出一个大的饼图,展示哪些对象占用内存过大。

这时直接会看到某种自己系统创建的对象占用量过大,这种对象的实例多达数十万个,占用了老年代一大半的内存空间。

接着就可以找开发工程师去排查这个系统的代码问题了,为什么会创建那么多对象,且始终回收不掉?

这就是典型的内存泄漏,即系统创建了大量的对象占用了内存,很多对象不再使用但又无法回收。

后来找出了原因:就是系统里做了一个JVM本地缓存,把很多数据都加载到内存里缓存,然后提供查询服务时会直接从本地内存里进行查询。但因没有限制本地缓存大小,且没使用LRU算法定期淘汰缓存数据。最终导致缓存在内存里的对象越来越多,最后造成了内存泄漏。

解决问题很简单:只要使用如Ehcache等缓存框架即可,它会固定最多缓存多少个对象,以及定期淘汰一些不常访问的缓存,以便新数据可以进入缓存中。

2.百万级数据误处理导致频繁FGC(大数据量加载到内存处理 + String.split())

(1)事故场景

(2)CPU负载高原因分析

(3)FGC频繁的原因分析

(4)以前那套GC优化策略还能奏效吗

(5)复杂的业务逻辑自己都看不懂怎么办

(6)准备一段示范用的代码

(7)获取JVM进程的dump快照文件

(8)使用MAT分析内存快照

(9)追踪线程执行堆栈,找到问题代码

(10)为什么"String.split()"会造成内存泄漏

(11)代码如何进行优化

(1)事故场景

有一次一个线上系统进行了一次版本升级,结果升级过后才半小时,突然收到运营和客服非常多的反馈。该系统对应的前端无法访问了,所有用户看到的都是一片空白和错误。

这时通过监控报警平台也收到非常多的报警,发现线上系统所在机器CPU负载非常高,甚至导致机器宕机,所以系统对应的前端页面自然是什么都看不到。

(2)CPU负载高原因分析

CPU负载高会有两个原因:一是系统里创建了大量线程并发执行,二是JVM在执行频繁的FGC。

通过查看监控和jstat发现:FGC非常频繁,基本两分钟就会执行一次FGC。而且每次FGC耗时非常长,在10秒左右,所以直接尝试进行FGC的原因定位。

(3)FGC频繁的原因分析

如果有频繁FGC的问题,一般有三个可能:

**可能一:**内存分配不合理或高并发导致对象频繁进入老年代,引发频繁FGC

**可能二:**存在内存泄漏,就是内存驻留了大量对象塞满了老年代且无法回收

**可能三:**Metaspace里的类太多,触发了FGC

其实分析频繁FGC的原因,最好的工具不是监控平台,而是jstat工具。直接通过jstat看线上系统运行时的动态内存变化模型,问题就清楚了。

基于jstat一分析发现了很大的问题:当时这个系统主要是用来处理大量数据,然后提供结果给用户查看的,所以给JVM的堆分配了20G的内存,其中新生代10G、老年代10G。如下图示:

这么大的年轻代,结果却在jstat中看到:Eden区大概1分钟就会塞满,然后就会触发一次YGC,而且YGC过后有几个G的对象都是存活的会进入老年代。如下图示:

这说明什么?这说明系统代码运行时在产生大量的对象,而且处理极慢,经常在1分钟过后YGC以后还有很多对象存活,这才导致大量对象进入老年代。

就是因为这个内存运行模型,才导致了平均两分钟会触发一次FGC,而且老年代因为内存量很大,所以导致一次FGC就要10秒。甚至普通的4核机器根本撑不住这么频繁、这么耗时的FGC,所以这种长时间FGC直接导致机器的CPU频繁打满,负载过高。从而导致了用户经常无法访问系统,页面都是空白的。

(4)以前那套GC优化策略还能奏效吗

这时候按照前面介绍的那套GC优化策略还能奏效吗?即把新生代调大,给Survivor更多的内存空间,避免对象进入老年代。

明显不行,这个运行内存模型告诉我们:即使给新生代更大空间,甚至让每块Survivor区域达到2G或者3G。但一次YGC过后,还是会因为系统处理过慢,导致几G的对象存活下来,这时候Survivor区还是会放不下的。

所以这时就不是简单优化一下JVM参数就可以搞定的,这个系统明显是因为代码层面有一定的改动和升级,直接导致了系统加载过多数据到内存中。而且对过多数据的处理还特别慢,在内存里几个G的数据甚至要处理一分多钟才能处理完毕。

这就是明显的代码层面问题了,要解决这个事故,就必须优化代码,而不是简单的调JVM参数,需要避免代码层面加载过多数据到内存去处理。

(5)复杂的业务逻辑自己都看不懂怎么办

说优化代码,说起来很简单,但是实际做起来呢?有很多系统的代码都特别的复杂,别说别人看不懂了,可能自己写的代码过了几个月,自己都看不懂了,所以直接通过走读代码来分析问题所在是很慢的。

有个办法可以马上定位到系统里什么样的对象太多、占用过多内存,这个办法就是使用一个用来分析dump内存快照的工具:MAT。

(6)准备一段示范用的代码

下面我们来准备一段示范用的代码,在代码里创建大量的对象出来。然后我们尝试获取它的dump内存快照,再用MAT来进行分析;

复制代码
public class Demo {
    public static void main(String[] args) throws Exception {
        List<Data> datas = new ArrayList<Data>();
        for (int i=0; i<10000; i++) {
            datas.add(new Data());
        }
        Thread.sleep(1 * 60 * 60 * 1000);
    }
    static class Data {
    }
}

这段代码非常的简单:就是创建10000个自定义的对象,然后就陷入一个阻塞状态就可以了,接着把这段代码运行起来。

(7)获取JVM进程的dump快照文件

先在本地命令行运行一下jps命令,查看一下启动的jvm进程的PID,如下所示:

复制代码
$ jps
1169 Launcher
1177 Demo
1171 Jps

明显能看到,我们的Demo这个类的进程ID为1177,接着执行下面的jmap命令可以导出dump内存快照:

复制代码
$ jmap -dump:live,format=b,file=dump.hprof 1177

(8)使用MAT分析内存快照

在接下来的一个步骤,务必要注意:如果是线上导出来的dump内存快照,很多时候可能都是几个G的。比如要打开8G左右的内存快照,就务必按照前面说的:在MAT的MemoryAnalyzer.ini文件里,修改MAT的启动堆大小为8G。

一.接着就是打开MAT软件

二.然后选择其中的"Open a Heap Dump"打开dump快照文件

三.接着会看到如下图示

打开dump快照时会出现提示,要不要进行内存泄漏的分析。也就是"Leak Suspects Report",一般勾选是即可。

四.接着会看到如下图示

以及如下图示:

从上图可以很清晰看出,MAT都展示出,可能存在的内存泄漏的问题。尤其是第一个问题"Problem Suspect 1",其英文里很清晰的告知:java.lang.Thread main线程通过局部变量引用了占据24.97%内存的对象。且MAT展示出那是一个java.lang.Object[]数组,该数组占据了大量内存。

那么此时就需要知道,这到底是一个什么样的数组?可以看到"Problem Suspect 1"框的最后一行是一个超链接的"Details",点击进去就可看到详细说明。

通过这个详细说明,尤其是"Accumulated Objects in Dominator Tree"。在里面可以看到,main线程中引用了一个java.util.ArrayList。这里面是个java.lang.Object[]数组,数组元素是Demo1$Data对象实例。

到此为止,就很清楚到底是什么对象在内存里占用了过大的内存。所以想要查清楚系统中那些超大对象到底是什么,可使用MAT分析。

(9)追踪线程执行堆栈,找到问题代码

一旦发现某个线程在执行过程中创建了大量的对象后,就可以尝试找找这个线程到底执行了哪些代码才创建了这些对象。

如下图示,可以点击页面中的一个"See stacktrace",然后就会进入一个线程执行代码堆栈的调用链了。

以及:

在当时我们就是按照这个方法追踪到了线上系统某个线程的执行堆栈,最终发现的是这个线程执行"String.split()"方法会导致产生大量的对象。

那么到底是为什么呢,接下来分析一下"String.split()"这个方法。

(10)为什么"String.split()"会造成内存泄漏

其实原因很简单,当时这个系统用的是JDK 1.7。

在JDK 1.6时,String.split()方法的实现是这样子的:比如有个字符串"Hello World",然后按照空格来切割这个字符串,应该会出来四个字符串:"Hello"、"World"。

在JDK 1.6时:"Hello World"这个字符串底层是基于一个数组来存放那些字符的,比如[H,e,l,l,o,,W,o,r,l,d]这样的数组,然后切割出来的"Hello"字符串它不会对应一个新的数组,而是直接映射到原来那个字符串的数组,采用偏移量表明自己是对应原始数组中的哪些元素,比如"Hello"可能对应[H,e,l,l,o,W,o,r,l,d]数组中0~4位置的元素。

在JDK 1.7时:它的实现是给每个切分出来的字符串都创建一个新的数组,比如"Hello"字符串就对应一个全新的数组[H,e,l,l,o]。

所以当时那个线上系统的处理逻辑,就是加载大量的数据出来。可能有时一次性加载几十万条数据,数据主要是字符串。然后会对这些字符串进行切割,每个字符串都会切割为N个小字符串,这就瞬间导致字符串数量暴增几倍甚至几十倍,这就是系统为什么会频繁产生大量对象的根本原因。

因为在本次系统升级之前,是没有String.split()这行代码的。所以当时系统基本运行还算正常,其实一次加载几十万条数据量也很大。当时基本上每小时都会有几次FGC,不过基本都还算正常。

而系统升级后代码加入String.split()操作,瞬间导致内存使用量暴增N倍。引发了上面说的每分钟一次YGC,两分钟一次FGC,所以根本原因就在于这行代码的引入。

(11)代码如何进行优化

后来紧急对这段代码逻辑进行了优化,避免对几十万数据每条都执行String.split()方法让内存使用量暴增N倍,然后再对那暴增N倍的字符串进行处理。就当时而言,String.split()这个代码逻辑可用可不用,所以直接去除了。但是如果从根本而言就是:这种处理大数据量的系统,一次性就不要加载过多数据到内存里来。

所以比较核心的思路就是:开启多线程并发处理大量的数据,尽量提升数据处理完毕的速度,这样在触发YGC的时候也可以避免过多的对象存活下来。

3.JVM运行原理和GC原理总结

(1)JVM和YGC的运行原理

(2)对象什么时候进入老年代

(3)老年代的GC是如何触发的

(4)正常情况下系统的GC频率

(5)CPU负载高原因总结

(6)FGC频繁的原因总结

(1)JVM和YGC的运行原理

首先必须要明白,JVM是如何运行起来的。

一.JVM的内存区域划分

最核心的就是这几块:新生代、老年代、Metaspace(永久代)。其中新生代又分成了Eden区和2个Survivor区,默认比例是8 : 1 : 1。如下图示:

二.系统程序会不停在新生代Eden区创建各种对象

系统程序会不停运行,运行时会不停在新生代的Eden区中创建各种对象,如下图示:

三.方法运行完毕,其局部变量引用的对象可被回收

一般创建对象都是在各种方法里执行的,一旦方法运行完毕,方法局部变量引用的那些对象就会成为Eden区里的垃圾对象可被回收。如下图示:

四.随着不断创建对象,Eden区就会逐步被占满

这时可能Eden区里的对象大多数都是垃圾对象,一旦Eden区被占满后,就会触发一次YGC。

首先从GC Roots(方法局部变量、类静态变量)开始追踪,标记存活对象。然后用复制算法把存活对象放入第一个Survivor区中,也就是S0区。如下图示:

五.接着新生代垃圾回收器就会回收掉Eden区里剩余的全部垃圾对象

在整个新生代垃圾回收的过程中全程会进入STW状态。也就是暂停系统工作线程,系统代码全部停止运行,不允许创建新对象。这样才能让新生代垃圾回收器专心工作,找出存活对象然后回收垃圾对象。

一旦新生代垃圾回收全部完毕,存活对象都进入了Survivor区域。然后Eden区都清空了,那么YGC就会执行完毕。此时系统程序恢复工作,继续在Eden区里创建对象。

六.下一次如果Eden区又满了,就会再次触发YGC

把Eden区和S0区里的存活对象转移到S1区里去,然后直接清空掉Eden区和S0区中的垃圾对象。当然这个过程中系统程序是禁止工作的,处于Stop the World状态,如下图示:

七.负责YGC的垃圾回收器有很多种,常用的是ParNew垃圾回收器

它的核心执行原理就如上所述,只不过ParNew运行时是基于多线程并发执行垃圾回收的。

以上就是最基本的JVM和YGC的运行原理。

(2)对象什么时候进入老年代

导致对象会进入老年代区域中的情况如下:

情况一:对象在新生代里躲过15次垃圾回收,年龄太大要进入老年代

情况二:对象太大超过了一定的阈值,直接进入老年代,不经过新生代

情况三:YGC后存活对象太多导致S区放不下,存活对象会进入老年代

情况四:可能几次YGC过后,Surviovr区域中的对象占用超50%的内存,此时如果年龄1+年龄2+年龄N的对象总和超过了Survivor区域的50%,那么年龄N及以上的对象都进入老年代,即动态年龄判定规则

对象进入老年代的情况说明:

说明一:躲过15次YGC的对象毕竟是少数

说明二:大对象一般在特殊情况下会有

说明三:加载大量数据长时间处理及高并发,才容易导致存活对象过多

对于这些情况,都会导致对象进入老年代中,老年代对象会越来越多。如下图示:

(3)老年代的GC是如何触发的

一旦老年代对象过多,就可能会触发FGC。FGC必然会带着Old GC,也就是针对老年代的GC,而且FGC一般也会跟着一次YGC,也会触发一次永久代GC。

触发FGC的几个条件如下:

条件一:可以设置老年代内存使用阈值,有一个JVM参数可以控制。老年代内存使用达到阈值就会触发FGC,一般建议调大一些,如92%。

条件二:在执行YGC前,如果发现老年代可用空间小于历次YGC后升入老年代的平均对象大小。那么就会在YGC前触发FGC,先回收掉老年代一批对象,再执行YGC。

条件三:在执行YGC后,如果YGC过后的存活对象太多,Survivor区放不下,要放入老年代。但是此时老年代也放不下,就会触发FGC,回收老年代一批对象,然后再把这些年轻代的存活对象放入老年代。

触发FGC几个比较核心的条件就是这几个,总结起来就是:老年代一旦快要满了,空间不够了,必然要进行FGC垃圾回收。

老年代的垃圾回收通常建议使用CMS垃圾回收器。此外老年代GC的速度是很慢的,少则几百毫秒,多则几秒。所以一旦FGC很频繁,就会导致系统性能很差。因为频繁FGC会频繁停止系统工作线程,导致系统一直有卡顿的现象。而且频繁FGC还会导致机器CPU负载过高,导致机器性能下降。

所以优化JVM的核心就是减少FGC的频率。

(4)正常情况下系统的GC频率

正常YGC频率是几分钟或几十分钟一次,一次耗时几毫秒到几十毫秒。

正常FGC频率是几十分钟一次或几小时一次,一次耗时大概几百毫秒。

所以如果观察线上系统就是这个性能表现,基本上问题都不太大。实际线上系统很多时候会遇到一些JVM性能问题:比如FGC过于频繁,每次耗时很多,此时就需要进行优化了。

(5)CPU负载高原因总结

CPU负载高的两个原因:

原因一:系统里创建了大量线程并发执行

原因二:JVM在执行频繁的FGC

(6)FGC频繁的原因总结

频繁FGC问题的三个可能:

可能一:内存分配不合理或高并发,导致对象频繁进入老年代,引发频繁FGC

可能二:存在内存泄漏,就是内存里驻留了大量对象塞满了老年代且无法回收

可能三:Metaspace里的类太多,触发了FGC

4.JVM性能优化的思路和步骤

(1)一个新系统开发完毕后应如何设置JVM参数

(2)在压测之后合理调整JVM参数

(3)线上系统的监控和优化

(4)线上频繁FGC的几种表现

(5)频繁FGC的几种常见原因

(6)一个统一的JVM参数模板

(1)一个新系统开发完毕后应如何设置JVM参数

一个新系统开发完毕后,到底该如何预估及合理设置JVM参数呢?毕竟直接用默认的JVM参数部署上线再观察,是非常的不靠谱的,而很多公司其实也没有所谓的JVM参数模板。

一.首先应估算一下新系统每秒占用多少内存

每秒多少次请求、每次请求创建多少对象、每个对象大概多大、每秒使用多少内存空间。

二.接着估算Eden区大概多长时间会占满

三.然后估算出多长时间会发生一次YGC

四.接着估算YGC时有多少对象存活而升入老年代

五.然后估算老年代对象的增长速率+多久触发FGC

通过一连串估算就能合理分配新生代、老年代、Eden、Survivor空间。原则就是:让YGC后存活对象远小于S区,避免对象频繁进入老年代触发FGC。

最理想的状态就是:系统几乎不发生FGC,老年代应该就是稳定占用一定的空间。就是那些长期存活的对象在躲过15次YGC后升入老年代占用的,然后平时主要就是几分钟发生一次YGC,耗时几毫秒。

(2)在压测之后合理调整JVM参数

任何一个新系统上线都得进行压测,在模拟线上压力的场景下,用jstat等工具去观察JVM的运行指标:

一.Eden区的对象增长速率多快

二.YGC频率多高

三.一次YGC多长耗时

四.YGC过后多少对象存活

五.老年代的对象增长速率多高

六.FGC频率多高

七.一次FGC耗时多少

压测时可以完全精准的通过jstat观察出上述JVM运行指标,然后就可以优化JVM的内存分配:尽量避免对象频繁进入老年代,尽量让系统只有YGC。

(3)线上系统的监控和优化

系统上线后,务必要进行一定的监控。一般通过Zabbix等工具来监控机器和JVM的运行,频繁FGC就要告警。没这些工具,就在机器上运行jstat,把监控信息写入文件,定时查看。

一旦发现频繁FGC的情况就要进行优化,优化的核心思路是类似的:通过jstat分析出来系统的JVM运行指标,找到FGC的核心问题。然后优化一下JVM的参数,尽量让对象别进入老年代,减少FGC的频率。

(4)线上频繁Full GC的几种表现

一旦系统发生频繁Full GC,可能会看到:

一.机器CPU负载过高

二.频繁FGC报警

三.系统无法处理请求或者处理过慢

所以一旦发生上述几个情况,第一时间应该想到是不是发生了频繁FGC。

(5)频繁FGC的几种常见原因

频繁FGC的常见原因有下面几个:

原因一:系统承载高并发请求,或者处理数据量过大,导致YGC很频繁

如果每次YGC后存活对象太多,内存分配不合理,Survivor区过小,必然会导致对象频繁进入老年代,频繁触发FGC;

原因二:系统一次性加载过多数据进内存,创建出来很多大对象

导致频繁有大对象进入老年代,必然频繁触发FGC;

原因三:系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收

大量的对象一直占用在老年代里,必然频繁触发FGC;

原因四:Metaspace因加载类过多触发FGC

原因五:误调用System.gc()触发FGC

其实常见的频繁FGC原因无非就上述几种,所以处理FGC时,可以从这几个角度入手使用jstat分析。

如果jstat分析发现FGC原因是第一种:新生代升入老年代多且频繁,但老年代并没有大量对象一直无法回收。那么就合理分配内存,调大Survivor区即可。

如果jstat分析发现是第二种或第三种原因:也就是老年代一直有大量对象无法回收,新生代升入老年代的对象不多。那么就dump出内存快照,用MAT工具进行分析,找出占用过多的对象。通过分析对象的引用和线程执行堆栈,找到导致那么多对象的那块代码,接着优化代码即可。

如果jstat分析发现内存使用不多但频繁触发FGC,必然是第四第五种,此时进行对应优化即可。

(6)一个统一的JVM参数模板

为简化JVM的参数设置和优化,建议各团队做一份JVM参数模板出来,设置一些常见参数。核心就是一些内存区域的分配、垃圾回收器的指定、CMS性能优化的一些参数(比如压缩、并发等)、常见的一些参数、包括禁止System.gc()、打印出来GC日志等。

网上有很多博客会让我们设置一些非常少见的JVM参数,比如前面有个案例就介绍了,有人设置了软引用的一个参数,还有一些奇怪的参数,比如PageCache参数之类的,以为JVM优化就是调节奇怪的参数。

其实完全不是如此,真正的JVM优化:其实是内存分配 + 垃圾回收器的选择 + 垃圾回收器的常见参数设置,还有就是一些代码层面的内存泄漏问题。搞定这些问题,99%的JVM性能问题都能搞定了。所以千万别胡乱设置一些奇怪的参数,很可能会适得其反。

5.问题汇总

问题一:

总结一下关于CMS的几个参数:

一.-XX:+CMSParallelInitialMarkEnabled

在初始标记时多线程执行,减少STW。

二.-XX:+CMSScavengeBeforeRemark

在重新标记之前执行YGC减少重新标记时间。

三.-XX:+CMSParallelRemarkEnabled

在重新标记的时候多线程执行,降低STW。

四.CMSInitiatingOccupancyFraction=92和-XX:+UseCMSInitiatingOccupancyOnly

这两个参数需要配套使用,如果没有后者,JVM第一次会用92%但后续会根据运行时的数据来调整,如果设置后者则JVM每次都会在92%时进行GC。

五.-XX:+PrintHeapAtGC

在每次GC前都要进行GC堆的概况输出。

问题二:

当时使用jxl导出Excel时,jxl会默认调用GC方法,当时花了不少时间才发现原来是System.gc()问题。

问题三:

CMS存在的问题总结:

一.浮动垃圾是因为并发清除

二.空间碎片是因为标记整理算法

三.并发执行失败是因为并发清除的设计可能存在预留的老年代空间不足

但是CMS对空间碎片进行了优化,提供了内存的整理。这个操作可以通过参数去控制,默认是开启的,并且FGC后去整理内存时,需要STW。

FGC的发生情况总结:

一.老年代可用内存小于新生代大小,又没开启空间担保,就会触发FGC

二.如果新生代大小大于老年代空间,且老年代可用空间,小于历次YGC后升入老年代的平均对象大小,也会触发FGC

三.大对象或者动态年龄进入老年代,而老年代空间不足,也会触发FGC

四.如果是CMS回收器,那么老年代内存使用到92%后,就会触发FGC,因为并发清除阶段需要给用户线程预留内存空间

问题四:

一般公司线上系统是禁用dump内存快照的吗?

答:是的,线上机器一般来说会禁止执行dump,因为dump的时候可能会导致系统停机几秒钟,或者几百毫秒。

问题五:

JVM在什么情况下会加载一个类?

答:JVM在如下情况下会加载一个类:一.JVM进程启动后,代码中包含main()方法的主类一定会被加载到内存;二.执行main()方法代码的过程中:遇到别的类也会从对应的".class"字节码文件加载对应的类到内存里面;

问题六:

一个类从加载到使用,一般会经历哪些过程?

答:加载-验证-准备-解析-初始化-使用-卸载

一.加载:将编译好的.class字节码文件加载到JVM

二.验证:根据JVM规范校验加载进来的.class文件

三.准备:给类和类变量分配一定的内存空间

四.解析:把符号引用替换为直接引用的过程

五.初始化:根据类初始化代码给类变量赋值

注意:执行new函数来实例化类对象会触发类加载到初始化的全过程,包含main()方法的主类,必须是马上初始化的。如果初始化一个类时,发现其父类还没初始化, 那么要先初始化其父类。

问题七:

Java里有哪些类加载器?

答:Java有如下类加载器:

(1)启动类加载器

负责加载在机器上安装的Java目录(lib目录)下的核心类库。

(2)扩展类加载器

负责加载Java目录下"lib/ext"目录中的类。

(3)应用程序类加载器

负责加载"ClassPath"环境变量所指定的路径中的类,大致可以理解为加载我们写好的Java代码。

(4)自定义类加载器

根据自己的需求加载类。

问题八:

什么是双亲委派机制?

答:JVM的类加载器是有亲子层级结构的,启动类加载器最上层,扩展类加载器第二层,应用程序类加载器第三层,自定义类加载器第四层。

当应用程序类加载器需要加载一个类时:首先委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载,如果父类加载器在自己负责加载的范围内没找到这个类,那么就下推加载权利给子类加载器。

问题九:

线上出现了FUll GC的告警,日志如下:

复制代码
2019-09-05T17:26:15.161+0800: 85779.869: [Full GC (Metadata GC Threshold)
2019-09-05T17:26:15.161+0800: 85779.869: [CMS: 472256K->445559K(3022848K), 2.0333919 secs] 1425388K->445559K(4910336K), [Metaspace: 277217K->277217K(1511424K)], 2.0355295 secs] [Times: user=2.01 sys=0.01, real=2.03 secs]
2019-09-05T17:26:17.197+0800: 85781.905: [Full GC (Last ditch collection)
2019-09-05T17:26:17.197+0800: 85781.905: [CMS: 445559K->382990K(3022848K), 1.6770458 secs] 445559K->382990K(4910336K), [Metaspace: 276037K->276037K(1511424K)], 1.6863552 secs] [Times: user=1.63 sys=0.01, real=1.68 secs] 
2019-09-05T17:26:18.886+0800: 85783.594: [GC (CMS Initial Mark) [1 CMS-initial-mark: 382990K(3022848K)] 382992K(4910336K), 0.0134842 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 

初步确认是每次发布Groovy脚本,ClassLoad的时候才会飙高,这种问题基本定位到了,但是需要怎么去优化和解决呢?

答:这个很明确了,就是Metaspace太小了,所以ClassLoad太多的时候会触发Full GC,所以只要给Metaspace区域更大空间就可以了。

问题十:

JVM中有哪些内存区域?

答:JVM的内存区域有:

一.方法区

在JDK1.8+,这块区域的名字改叫Metaspace,主要存放类相关的信息。

二.程序计数器

字节码指令通过字节码执行引擎被一条条执行,来实现代码的执行逻辑,程序计数器是用来记录当前线程执行的字节码指令位置的,也就是记录目前线程执行到哪一条字节码指令。JVM支持多个线程,所以就会有多个线程来并发执行不同的代码指令。因此每个线程都会有自己的一个程序计数器,线程的程序计数器会专门记录当前线程执行到哪一条字节码指令。

三.Java虚拟机栈

保存每个方法内的局部变量等数据,每个线程会有自己的Java虚拟机栈。如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧。栈帧里就有这个方法的局部变量表、操作数栈、动态链接、方法出口等。然后压入线程的Java虚拟机栈,方法执行完毕后就从Java虚拟机栈出栈。因此每个线程在执行代码时,会有一个程序计数器 + 一个Java虚拟机栈,Java虚拟机栈会存放每个方法中的局部变量。

四.Java堆内存

存放我们在代码中创建的各种对象,对象实例里面会包含一些数据。而Java虚拟机栈的栈帧局部变量表里的对象,是个引用类型的局部变量,里面存放了对应Java堆内存对象的地址。

问题十一:

以后如果都用G1垃圾回收器,那是不是在JVM优化上就得靠边站了?

答:是的,使用G1的时候,其实能做的事情很少。因为它所有的内存分配和GC时机都是动态变化的,很难去调优。实际上它一切都是自动运行的,只要它能保证每次GC的耗时在指定范围就可以了,一般还是用CMS + ParNew即可,比较可控一些。

相关推荐
1.01^10001 天前
[5-01-01].第04节:初识字节码文件 - 字节码文件作用
jvm
找不到、了1 天前
JVM核心知识整理《1》
jvm
L.EscaRC1 天前
面向 Spring Boot 的 JVM 深度解析
jvm·spring boot·后端
学到头秃的suhian2 天前
JVM-类加载机制
java·jvm
NEFU AB-IN2 天前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海2 天前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗2 天前
JVM整理
jvm
echoyu.2 天前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考3 天前
JVM中内存管理的策略
java·jvm
thginWalker3 天前
深入浅出 Java 虚拟机之进阶部分
jvm