前言:上篇【jvm】面试官求你别再问了-死锁,内存溢出及泄漏如何监控及解决(上) 介绍了通过基础命令的方式来监控及解决死锁,内存溢出及泄露问题,本篇主要介绍通过工具监控及解决这些问题,毕竟工欲善其事,必先利其器,好的工具可以让监控和解决问题事半功倍.好的监控工具有很多,中大型公司基本都有自己的监控工具和体系,本篇主要介绍开源的工具,常见的开源监控工具有jdk自带的jconsole,jvisovm以及Jprofiler等,Jdk自带的监控工具我在之前的博客中有总结(https://blog.csdn.net/lovexiaotaozi/article/details/82862196?spm=1001.2014.3001.5501),本篇的主角是Jprofiler.
目录
1.工具监控的优劣
2.Jprofiler简介
3.Jprofiler安装及IDEA集成
4.Jprofiler如何监控并解决死锁
5.Jprofiler如何监控并解决内存溢出 (建议直接跳过)
1.栈溢出stackOverflow
2.堆溢出
3.永久代溢出(jdk8以后为MetaSpace元空间,堆外内存)
6.Jprofiler如何监控及解决内存泄露(划重点)
7.Jprofiler还能做哪些事
8.总结
优:高效,直观,使用简单,门槛低,效果好,小白也能轻松上手并实现大师级(吹牛皮)的效果...
劣:需要额外的安装及学习成本,如果与应用服务器直连的话,会多暴露端口,带来风险及少量性能开销(不过好在都有相应的解决方案)
Jprofilers是针对JAVA开发的剖析工具,通过它可以监控到java程序的内存,CPU,线程,GC,锁等进行监控和分析,功能非常强大,不过要收费,好在有10天免费试用期,有需要的也可以购买正版,或者...你懂的
本篇仅介绍Jprofiler的安装及如何Intelij IDEA中的集成,如有用Elicplise的同学这边也不建议你另找教程,建议尝试用IDEA.
这里我的IDEA版本是2020.2.2,选择的Jprofiler版本是12.0(早期的版本是纯英文的,12.0支持中文了,对新手比较友好,不过无关紧要,英文一样容易上手,主要考虑是否与IDEA插件兼容即可)
安装主要分为两步:
第一步:进入Jprofiler官网下载 -> Jprofiler 版本这边建议选择最新的或者次新的(前提是你的IEDA版本也比较新)
第二步:打开IDEA->File->settings->plugins->marketplace->搜索 Jprofiler
插件安装后,在IDEA工具栏可以找到如图所示的两个图标,点击箭头指的那个,进行配置,主要就是配置个路径,让插件能找到第一步中下载好的Jprofiler:
我是在IDEA安装目录的插件目录下,新建了个Jprofiler文件夹,然后把刚刚下好的Jprofiler拖到里面了,唯一值得注意的是需要重命名,把带版本号的Jprofiler_windowsxxx.exe重命名成Jprofiler.exe,否则你选不中,Idea插件对它的名称作了死限制...
选中后然后同意安装协议,一路next确认就好了,最后选试用10天激活,如果你有秘钥可以选永久激活,至此就安装好了,然后可以通过该按钮启动应用
启动后就可以获取到启动应用的各种信息了,当然你也可以连接远程服务器上的java应用,或者通过dump出来的文件分析等:
先写了一段死锁代码,用于演示死锁:
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
Thread t1 = new Thread(()->{
synchronized (a){
try {
System.out.println("已获取到a锁,尝试获取b锁===>");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("尝试获取b锁...");
}
}
});
Thread t2 = new Thread(()->{
synchronized (b){
try {
System.out.println("<===已获取到b锁,尝试获取a锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println("尝试获取a锁...");
}
}
});
t1.start();
t2.start();
}
通过Jprofiler启动这段代码,然后可以在锁菜单界面直接看到当前锁的状态图:
然后可以在当前Monitor监视器设置一个观察的阈值,如果项目中的锁比较多,可以把超过一定时间的锁给筛选出来,进一步观察是否死锁,因为有些长时间阻塞的锁也可能是饿锁
然后针对筛选出来的几个可能是死锁的进行深入分析,首先可以看到这些锁的类型是处于阻塞的,然后可以在任意一个上面点击鼠标右键,选择在堆遍历器中显式所选内容:
然后在对遍历器中点击图表,就可以看到这个锁所在的代码位置,通过此工具就可以快速定位到一些可能出现死锁的代码位置,然后看一看代码就知道到底有没有死锁,还是饿锁了
通常来说产生死锁的原因主要有:
①没有设置锁的失效时间,然后代码里可能因为异常没有处理,锁没有在finaly代码块中释放导致其它线程无法获取锁而陷入等待;
②类似上图这种,锁互相竞争,线程t1持有A锁尝试获取B锁,线程t2持有B锁尝试获取A锁,谁也不让谁,互相干等着,这种情况要注意加锁的顺序,如果t1线程是先获得了A锁,再去获取B锁,然后t2线程去尝试获取A锁,再获取B锁,按这样的顺序就不会产生死锁.
③另外就是线程由线程优先级产生的饿锁,饿锁不算死锁,但也要引起重视,否则同样会造成资源浪费,可以通过JUC提供的公平锁解决.
按照上面的思路,去排查对应的代码,基本上死锁都能被解决.
内存溢出其实比较好监控也比较好解决,用Jprofiler有点大材小用了,毕竟内存溢出可以在应用的日志中直接看到,稍微大点的公司也都有自己的一套监控工具,这里仅作简单介绍.
内存溢出主要分为三类,针对三种,分别有不同的解决方案
public static void main(String[] args) {
Test test = new Test();
test.recursion();
}
private void recursion(){
recursion();
}
栈溢出最容易出现在递归中,如果递归没有出口,或者递归的太深了,会容易爆栈,解决方案就是review报错位置的代码,看看是否因为代码写的有问题,导致递归找不到出口,一直递归下去... 如果代码没什么问题,就是需要很深的栈调用才能得出结果,这种极端情况可以适当调大调用线程的栈大小,或者将递归代码改为循环体去解决(推荐).
创建过多的对象导致的堆空间溢出
public static void main(String[] args) {
List
创建过多的线程导致堆内存被耗尽,无法创建更更多的线程
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger();
ExecutorService executorService = Executors.newCachedThreadPool();
for (;;){
executorService.execute(()->{
System.out.println(count.addAndGet(1));
});
}
}
这两种情况导致的堆溢出比较常见,解决起来也比较好解决,可以直接根据日志定位到相关代码位置,然后review,代码有问题就改代码,代码没问题的话,可以根据物理机或者容器的内存值,适当调大最大堆大小,-Xmx=xxx
至于创建过多的线程导致的,这种就建议多读读阿里代码规范...
这里我电脑只装了JDK8,所以只能演示MetaSpace内存溢出,元空间一般用来存放Class信息,所以可以通过cglib模拟元空间溢出,由于元空间默认是可伸缩的,上限是物理机内存的上限,为了加快让内存溢出,我把上限手动调到20兆(或更小),以便让内存溢出快速出现,
添加VM参数:-XX:MaxMetaspaceSize=20m 启动下面这段代码:
public static void main(String[] args) {
ClassLoadingMXBean mxBean = ManagementFactory.getClassLoadingMXBean();
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setCallbackTypes(new Class[]{Dispatcher.class, MethodInterceptor.class});
enhancer.setCallbackFilter(new CallbackFilter() {
@Override
public int accept(Method method) {
return 1;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
});
Class clazz = enhancer.createClass();
System.out.println(clazz.getName());
System.out.println("total:"+mxBean.getTotalLoadedClassCount());
}
}
效果:
解决办法:通常情况下,元空间溢出不太会出现,元空间的内存已经是堆外内存了,而且可以动态分配,除非部署的容器内存上限给少了,或者是物理机内存实在不够用(部署了太多其它应用等)
JDK8把永久代用元空间替换,主要也是为了减少FullGC和永久代溢出,所以只要我们代码写的没啥问题,也不要轻易限制元空间大小,通常不会有元空间溢出.
至于永久代的溢出,一般可以优化代码,调大PermGenSize去解决,当然首推升级JDK版本.
内存泄漏相对于死锁和OOM会比较难定位,而且对整个应用的危害程度比较大,一旦发生内存泄漏,可能会导致整个应用不可用.
内存泄漏可以通过jdk自带的jconsole或者jvisovm都可以监控到,这里演示下通过Jprofiler如何监控内存泄漏.
通常情况下,内存泄漏会拖垮整个应用,如果应用突然不可用了,且网络正常可以登录服务器查看应用日志,如果看到有OOM,可以初步怀疑有内存泄漏发生,对于比较明显或者比较短的时间内产生的内存泄漏,可以通过本地IDEA直连Jprofile启动应用的方式进行监控,对于在特定场合下才会发生的,可以加上VM参数: -XXHeapdump重启线上应用,以便在下次内存泄漏OOM时,导出dump日志,然后导入Jprofile进行分析. 这里我以直连本地的方式进行演示(偷个懒)
写了一段会造成内存泄漏的代码:
public class Test {
private final static ThreadLocal> threadLocal = new ThreadLocal<>();
private final static List list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(()->{
while (true){
for (int i = 0; i < 1000000; i++) {
list.add("oom");
if (i%100==0){
threadLocal.set(list);
}
}
//让过程慢一点,留点时间去分析,否则OOM之后与Jprofiler的连接就断开了
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
添加启动VM参数 :--Xmx1024m 指定最大堆为1024兆 ,可以让上面程序运行久一点,不因为OOM中断影响分析 当然你也可以通过重写上面这段代码来控制,总之演示为了尽快的看到内存泄漏又能不中断与jprofiler的连接即可
(上图是程序运行了很久之后)
用IDEA的Jprofiler插件启动Jprofiler,切换到遥测的内存界面,内存使用量每增长一段时间后,点击一次运行GC,观察每次GC后已使用内存部分(下图中蓝色部分),不难发现每次垃圾回收之后,已使用的内存变得越来越多,如此积累下去,直到已使用内存超过最大堆大小,应用就无法再创建对象,然后疯狂触发GC,但GC后又没有效果,最终GC过载,CPU和内存资源被榨干,服务处于不可用状态.
简而言之就是:观察前一次GC和当前GC后剩余已用内存是否处于不断递增之中,如果是,则极有可能发生了内存泄漏.
通过线上服务不可用和监控到的数据,我们已经几乎可以确认是发生了内存泄漏,下面开始排查,到底是哪里发生了内存泄漏?写了什么禽兽代码让内存泄漏了???
第一步:通过Jprofiler连接到本地jvm或者远程(考虑到安全问题,可以用远程dump的日志导入Jprofiler)
第二步:点击实时内存菜单下的所有对象,然后点击标记当前,然后持续观察 实例计数列,相差列,大小列变化
可以从以下几个线索去缩小范围:
①可能发生内存泄漏的一般有char[],String,Map,List,Object[]等,可以重点去看看
②占据大量内存空间或是实例计数超大的(一般会导致内存泄漏且影响到服务正常吞吐的,大小列的值都不会太小,毕竟一个应用的最大堆至少也会指定/默认在256Mb以上,像这种几百kb级别的,即便在持续增长,想要让系统不可用,也得积累很久很久)
③多次点击运行GC,观察每次GC后剩余的大小,如果大小列的值增长很快,但GC仍剩余很大,可以怀疑此处可能有内存泄漏(比如下图被我框了的地方,GC前有400多Mb,GC后仍有186MB剩余,整个堆区在GC后剩余的总垃圾也不过189MB,这玩意就独占鳌头186Mb,回收不掉,隔一阵子再点GC,GC后这个值比186MB还大,再次印证该类很可能存在内存泄漏)
通过上面三种排查方式,逐步缩小排查范围,目前看下来就只有char[],String,Object[]三种类型的数据可能存在数据泄露
第三步:选中可疑项,点击鼠标右键,选择在堆遍历器中显式
在打开的新界面中,点击最大对象,然后选中保留大小最大的(有的时候有好几个都挺大的,只能一一尝试,因为保留大小偏大的一般都可能导致内存泄漏),然后点击鼠标右键,使用选中对象
然后选择传入引用,点击确定
在下图中的界面,可以先展开树形结构的数据看一下,这个Oject[]底下是什么东西,一般来说如果点开是大公司打头的包名,基本可以直接跳过不用看了,因为一般大公司的产品不大会出现内存泄漏,发布之前他们应该也有测过了.
如果看了下包名,是自己应用的包名,就可以继续排查,点击在图表中显示
在弹出的新页面中选中上一步中看到的ArrayList对象,点击显示到GC根的路径:
然后可以点开+号,瞅瞅,基本上到这一步,就能确认到发生内存泄漏的代码所在的包在哪,类名叫啥了,然后重点去排查这段代码即可发现问题
很显然,我这里是因为ThreadLocal持有List
至此,内存泄漏的定位就完成了,如果没有这个工具,在生产环境发生内存泄漏,去一行一行review所有代码是不现实的,通过此工具我们可以在较短的时间内定位到导致内存泄漏出现的代码位置,然后review该位置的代码即可.
内存泄漏的难的主要是定位,解决起来一般比较简单,先止血,重启生产环境应用,让服务恢复正常,然后在把导致内存泄漏的代码优化(该释放的释放,强引用显示赋值为null...),改好之后测试并重新发布即可解决.
如果只拿Jprofiler作内存和CPU以及线程的监控分析工具就太大材小用了,我看了EJ官方的文档,Jprofiler可以做的事情有很多,比如监控和分析数据库,网络,文件,进程等,上面只是其中的一部分常用功能,如果想了解更多可以访问EJ官方文档深入学习,
这里仅是抛转引玉.EJ官方文档:https://www.ej-technologies.com/resources/jprofiler/v/12.0/help_zh_CN/doc/main/probes.html
关于死锁,内存溢出及泄漏,其实和看病一样,防患于未然是最好的,在平时开发中养成良好的习惯,尽量去规避死锁,内存溢出,及内存泄漏,毕竟当这些问题出现在生产环境时,一般都会带来严重的损失,而且排查起来难度非常大,线上服务器通常各种权限和资源,也不是开发能轻易搞得到的,到时候再去分析就很困难了,当然技多不压身,多学点监控和解决方式,或许有一天还真能派上用场.像这类低频使用的知识,最好能总结一遍,留下文档,下次再碰到时,可以快速回忆,这也是我坚持写博客的原因之一,做技术,总得有点沉淀,互勉!
文中若有不正之处,或是针对内存泄漏定位有更好的方式,希望读者可以赐教,非常感谢!