三尺非寒 发表于 2025-1-8 19:51:42

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。如下图示:

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

https://i-blog.csdnimg.cn/img_convert/515217a7942cf3c588d6c3e46a1495b3.png
这阐明什么?这阐明体系代码运行时在产生大量的对象,而且处置惩罚极慢,经常在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,如下所示:​​​​​​​
$ jps1169 Launcher1177 Demo1171 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软件

https://i-blog.csdnimg.cn/img_convert/b5734124bd8122bf09f428261093c5bc.png
二.然后选择其中的"Open a Heap Dump"打开dump快照文件

三.接着会看到如下图示
打开dump快照时会出现提示,要不要进行内存走漏的分析。也就是"Leak Suspects Report",一般勾选是即可。

https://i-blog.csdnimg.cn/img_convert/85792a14292af06ff32b06765f53109f.png
四.接着会看到如下图示

https://i-blog.csdnimg.cn/img_convert/fd8b6b5970f4f79ad2a0936ae508f3c9.png
以及如下图示:

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

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

https://i-blog.csdnimg.cn/img_convert/5c200443dda887f68067c24a3e3d5db7.png
通过这个详细阐明,尤其是"Accumulated Objects in Dominator Tree"。在里面可以看到,main线程中引用了一个java.util.ArrayList。这里面是个java.lang.Object[]数组,数组元素是Demo1$Data对象实例。

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

(9)追踪线程实验堆栈,找到问题代码
一旦发现某个线程在实验过程中创建了大量的对象后,就可以尝试找找这个线程到底实验了哪些代码才创建了这些对象。

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

https://i-blog.csdnimg.cn/img_convert/48a98398bb3d9528d40ea5b61f98ae2c.png
以及:

https://i-blog.csdnimg.cn/img_convert/ca03b324ce4c0dbef625d30727bf4ac7.png
在当时我们就是按照这个方法追踪到了线上体系某个线程的实验堆栈,最终发现的是这个线程实验"String.split()"方法会导致产生大量的对象。

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

(10)为什么"String.split()"会造成内存走漏
其实原因很简单,当时这个体系用的是JDK 1.7。

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

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

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

以是当时谁人线上体系的处置惩罚逻辑,就是加载大量的数据出来。可能偶尔一次性加载几十万条数据,数据重要是字符串。然后会对这些字符串进行切割,每个字符串都会切割为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。如下图示:

https://i-blog.csdnimg.cn/img_convert/1ce8400a44a21a5c2cdba4c1fb0e5bac.png
二.体系步伐会不停在新生代Eden区创建各种对象
体系步伐会不停运行,运行时会不停在新生代的Eden区中创建各种对象,如下图示:

https://i-blog.csdnimg.cn/img_convert/4e55be94efd2ea151e8353d2b8fe634c.png
三.方法运行完毕,其局部变量引用的对象可被回收
一般创建对象都是在各种方法里实验的,一旦方法运行完毕,方法局部变量引用的那些对象就会成为Eden区里的垃圾对象可被回收。如下图示:

https://i-blog.csdnimg.cn/img_convert/a92951af11449e1a0399b0ba001cfdfb.png
四.随着不停创建对象,Eden区就会渐渐被占满
这时可能Eden区里的对象大多数都是垃圾对象,一旦Eden区被占满后,就会触发一次YGC。

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

https://i-blog.csdnimg.cn/img_convert/d983e72e2f994059cb0aa28581553f38.png
五.接着新生代垃圾回收器就会回收掉Eden区里剩余的全部垃圾对象
在整个新生代垃圾回收的过程中全程会进入STW状态。也就是暂停体系工作线程,体系代码全部停止运行,不允许创建新对象。这样才能让新生代垃圾回收器用心工作,找出存活对象然后回收垃圾对象。

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

六.下一次假如Eden区又满了,就会再次触发YGC
把Eden区和S0区里的存活对象转移到S1区里去,然后直接清空掉Eden区和S0区中的垃圾对象。当然这个过程中体系步伐是禁止工作的,处于Stop the World状态,如下图示:

https://i-blog.csdnimg.cn/img_convert/89881e56a7832a0e08fa4f491e6d2d9a.png
七.负责YGC的垃圾回收器有很多种,常用的是ParNew垃圾回收器
它的焦点实验原理就如上所述,只不过ParNew运行时是基于多线程并发实验垃圾回收的。

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

(2)对象什么时间进入老年代
导致对象会进入老年代地区中的情况如下:
情况一:对象在新生代里躲过15次垃圾回收,年龄太大要进入老年代
情况二:对象太大凌驾了肯定的阈值,直接进入老年代,不经过新生代
情况三:YGC后存活对象太多导致S区放不下,存活对象会进入老年代
情况四:可能几次YGC过后,Surviovr地区中的对象占用超50%的内存,此时假如年龄1+年龄2+年龄N的对象总和凌驾了Survivor地区的50%,那么年龄N及以上的对象都进入老年代,即动态年龄判定规则

对象进入老年代的情况阐明:
阐明一:躲过15次YGC的对象究竟是少数
阐明二:大对象一般在特殊情况下会有
阐明三:加载大量数据长时间处置惩罚及高并发,才容易导致存活对象过多

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

https://i-blog.csdnimg.cn/img_convert/09796094a8a7143101eb011575921b20.png
(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:  1425388K->445559K(4910336K), , 2.0355295 secs]
2019-09-05T17:26:17.197+0800: 85781.905: [Full GC (Last ditch collection)
2019-09-05T17:26:17.197+0800: 85781.905:  445559K->382990K(4910336K), , 1.6863552 secs]  
2019-09-05T17:26:18.886+0800: 85783.594:  382992K(4910336K), 0.0134842 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即可,比较可控一些。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: JVM实战—10.MAT的利用和JVM优化总结