GVKun编程网logo

一次 Java 内存泄漏的排查(java内存泄漏如何排查)

31

想了解一次Java内存泄漏的排查的新动态吗?本文将为您提供详细的信息,我们还将为您解答关于java内存泄漏如何排查的相关问题,此外,我们还将为您介绍关于javajvm内存溢出和内存泄漏的区别、java

想了解一次 Java 内存泄漏的排查的新动态吗?本文将为您提供详细的信息,我们还将为您解答关于java内存泄漏如何排查的相关问题,此外,我们还将为您介绍关于java jvm 内存溢出和内存泄漏的区别、java – 如何识别JNI全局引用内存泄漏的原因?、Java 中关于内存泄漏出现的原因以及如何避免内存泄漏、Java 内存泄漏原因、解决办法及泄漏排查的新知识。

本文目录一览:

一次 Java 内存泄漏的排查(java内存泄漏如何排查)

一次 Java 内存泄漏的排查(java内存泄漏如何排查)

由来


前些日子小组内安排值班,轮流看顾我们的服务,主要做一些报警邮件处理、Bug 排查、运营 issue 处理的事。工作日还好,无论干什么都要上班的,若是轮到周末,那这一天算是毁了。

不知道是公司网络广了就这样还是网络运维组不给力,网络总有问题,不是这边交换机脱网了就是那边路由器坏了,还偶发地各种超时,而我们灵敏地服务探测服务总能准确地抓住偶现的小问题,给美好的工作加点料。好几次值班组的小伙伴们一起吐槽,商量着怎么避过服务保活机制,偷偷停了探测服务而不让人发现(虽然也并不敢)。

前些天我就在周末处理了一次探测服务的锅。

转载随意,文章会持续修订,请注明来源地址:https://zhenbianshu.github.io 

问题


网络问题?

晚上七点多开始,我就开始不停地收到报警邮件,邮件显示探测的几个接口有超时情况。 多数执行栈都在:

java.io.BufferedReader.readLine(BufferedReader.java:371)
java.io.BufferedReader.readLine(BufferReader.java:389)
java_io_BufferedReader$readLine.call(Unknown Source)
com.domain.detect.http.HttpClient.getResponse(HttpClient.groovy:122)
com.domain.detect.http.HttpClient.this$2$getResponse(HttpClient.groovy)

这个线程栈的报错我见得多了,我们设置的 HTTP DNS 超时是 1s, connect 超时是 2s, read 超时是 3s,这种报错都是探测服务正常发送了 HTTP 请求,服务器也在收到请求正常处理后正常响应了,但数据包在网络层层转发中丢失了,所以请求线程的执行栈会停留在获取接口响应的地方。这种情况的典型特征就是能在服务器上查找到对应的日志记录。而且日志会显示服务器响应完全正常。 与它相对的还有线程栈停留在 Socket connect 处的,这是在建连时就失败了,服务端完全无感知。

我注意到其中一个接口报错更频繁一些,这个接口需要上传一个 4M 的文件到服务器,然后经过一连串的业务逻辑处理,再返回 2M 的文本数据,而其他的接口则是简单的业务逻辑,我猜测可能是需要上传下载的数据太多,所以超时导致丢包的概率也更大吧。

根据这个猜想,群登上服务器,使用请求的 request_id 在近期服务日志中搜索一下,果不其然,就是网络丢包问题导致的接口超时了。

当然这样 leader 是不会满意的,这个结论还得有人接锅才行。于是赶紧联系运维和网络组,向他们确认一下当时的网络状态。网络组同学回复说是我们探测服务所在机房的交换机老旧,存在未知的转发瓶颈,正在优化,这让我更放心了,于是在部门群里简单交待一下,算是完成任务。

问题爆发

本以为这次值班就起这么一个小波浪,结果在晚上八点多,各种接口的报警邮件蜂拥而至,打得准备收拾东西过周日单休的我措手不及。

这次几乎所有的接口都在超时,而我们那个大量网络 I/O 的接口则是每次探测必超时,难道是整个机房故障了么。

我再次通过服务器和监控看到各个接口的指标都很正常,自己测试了下接口也完全 OK,既然不影响线上服务,我准备先通过探测服务的接口把探测任务停掉再慢慢排查。

结果给暂停探测任务的接口发请求好久也没有响应,这时候我才知道没这么简单。

解决


内存泄漏

于是赶快登陆探测服务器,首先是 top free df 三连,结果还真发现了些异常。

我们的探测进程 CPU 占用率特别高,达到了 900%。

我们的 Java 进程,并不做大量 CPU 运算,正常情况下,CPU 应该在 100~200% 之间,出现这种 CPU 飙升的情况,要么走到了死循环,要么就是在做大量的 GC。

使用 jstat -gc pid [interval] 命令查看了 java 进程的 GC 状态,果然,FULL GC 达到了每秒一次。

这么多的 FULL GC,应该是内存泄漏没跑了,于是 使用 jstack pid > jstack.log 保存了线程栈的现场,使用 jmap -dump:format=b,file=heap.log pid 保存了堆现场,然后重启了探测服务,报警邮件终于停止了。

jstat

jstat 是一个非常强大的 JVM 监控工具,一般用法是: jstat [-options] pid interval

它支持的查看项有:

  • -class 查看类加载信息
  • -compile 编译统计信息
  • -gc 垃圾回收信息
  • -gcXXX 各区域 GC 的详细信息 如 -gcold

使用它,对定位 JVM 的内存问题很有帮助。

排查


问题虽然解决了,但为了防止它再次发生,还是要把根源揪出来。

分析栈

栈的分析很简单,看一下线程数是不是过多,多数栈都在干嘛。

> grep ''java.lang.Thread.State'' jstack.log  | wc -l
> 464

才四百多线程,并无异常。

> grep -A 1 ''java.lang.Thread.State'' jstack.log  | grep -v ''java.lang.Thread.State'' | sort | uniq -c |sort -n

     10 	at java.lang.Class.forName0(Native Method)
     10 	at java.lang.Object.wait(Native Method)
     16 	at java.lang.ClassLoader.loadClass(ClassLoader.java:404)
     44 	at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
    344 	at sun.misc.Unsafe.park(Native Method)

线程状态好像也无异常,接下来分析堆文件。

下载堆 dump 文件。

堆文件都是一些二进制数据,在命令行查看非常麻烦,Java 为我们提供的工具都是可视化的,Linux 服务器上又没法查看,那么首先要把文件下载到本地。

由于我们设置的堆内存为 4G,所以 dump 出来的堆文件也很大,下载它确实非常费事,不过我们可以先对它进行一次压缩。

gzip 是个功能很强大的压缩命令,特别是我们可以设置 -1 ~ -9 来指定它的压缩级别,数据越大压缩比率越大,耗时也就越长,推荐使用 -6~7, -9 实在是太慢了,且收益不大,有这个压缩的时间,多出来的文件也下载好了。

使用 MAT 分析 jvm heap

MAT 是分析 Java 堆内存的利器,使用它打开我们的堆文件(将文件后缀改为 .hprof), 它会提示我们要分析的种类,对于这次分析,果断选择 memory leak suspect

 从上面的饼图中可以看出,绝大多数堆内存都被同一个内存占用了,再查看堆内存详情,向上层追溯,很快就发现了罪魁祸首。

分析代码

找到内存泄漏的对象了,在项目里全局搜索对象名,它是一个 Bean 对象,然后定位到它的一个类型为 Map 的属性。

这个 Map 根据类型用 ArrayList 存储了每次探测接口响应的结果,每次探测完都塞到 ArrayList 里去分析,由于 Bean 对象不会被回收,这个属性又没有清除逻辑,所以在服务十来天没有上线重启的情况下,这个 Map 越来越大,直至将内存占满。

内存满了之后,无法再给 HTTP 响应结果分配内存了,所以一直卡在 readLine 那。而我们那个大量 I/O 的接口报警次数特别多,估计跟响应太大需要更多内存有关。

给代码 owner 提了 PR,问题圆满解决。

小结


其实还是要反省一下自己的,一开始报警邮件里还有这样的线程栈:

groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParserCharArray.java:166)
groovy.json.internal.JsonParserCharArray.decodeJsonObject(JsonParserCharArray.java:132)
groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParserCharArray.java:186)
groovy.json.internal.JsonParserCharArray.decodeJsonObject(JsonParserCharArray.java:132)
groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParserCharArray.java:186)

看到这种报错线程栈却没有细想,要知道 TCP 是能保证消息完整性的,况且消息没有接收完也不会把值赋给变量,这种很明显的是内部错误,如果留意后细查是能提前查出问题所在的,查问题真是差了哪一环都不行啊。

关于本文有什么疑问可以在下面留言交流,如果您觉得本文对您有帮助,欢迎关注我的 微博  GitHub 。您也可以在我的 博客 REPO 右上角点击 Watch 并选择 Releases only 项来 订阅 我的博客,有新文章发布会第一时间通知您。

java jvm 内存溢出和内存泄漏的区别

java jvm 内存溢出和内存泄漏的区别

java jvm 内存溢出和内存泄漏的区别

1、概念的区分

1.1、 内存泄露 memory leak

程序运行结束后,没有释放 所占用的内存空间。
一次内存泄漏 似乎不会有大的影响,但内存泄漏 不断累积,最终可用内存会变得越来越少。

在这里插入图片描述

比如说,总内存大小是100 MB,有40MB的内存一直无法回收,那么可用的只有60MB 。这40MB的就是内存泄漏。
内存泄漏,就是 程序运行结束后,没有释放的内存。

1.2、内存溢出 out of memory

程序运行时,在申请内存空间时,没有足够的内存空间供其正常使用,程序运行停止,并抛出 out of memory
比如程序运行时申请了一个10MB 空间, 但是当前可用内存只有5MB,程序无法正常执行,这就是内存溢出。

在这里插入图片描述

内存溢出 ,可以理解为 程序运行需要的内存 大于当前可用的内存。

1.3 举例

1)单例模式中,单例的生命周期和应用程序是一样长的,所以单例程序中如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致 内存泄露 的产生。

2)一些提供close的资源未闭导致 内存泄漏 。数据库连接(dataSource.getConnection() ),网络连接(socket)和 IO流的连接必须在finally中 close,否则不能被回收的。

3)读取大文件,一次读取的文件大于可用内存,会导致 内存溢出 。可用内存是1G,怎么读取2G的文件呢?建一个100MB的字节数组,读10次。

2、二者的区别和联系:

2.1、区别

内存泄露: 程序运行结束后,所占用的内存没有全部释放。
内存溢出:程序运行时,需要的内存大于当前可用的内存, 内存不足,程序无法继续执行,抛出 “内存溢出”,程序运行中断,结束。

2.2、联系

一次 内存泄露 可能对程序运行没有明显的影响,多次 内存泄露 最终会导致 内存溢出

比如总内存大小是100MB,一次程序运行结束有,有10MB 没有释放,当前可用内存还有90MB,程序还可以运行。但是多次运行后, 可用内存 最终为0, 没有可以内存或内存不足时,程序在下一次运行时,会因为内存不足,而出现 内存溢出

3、内存溢出的原因以及解决方法

3.1、内存溢出的原因

引起内存溢出的原因有很多种,小编列举一下常见的有以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小 。

3.2、内存溢出的解决方案:

第一步,修改JVM启动参数,直接增加内存。(-Xms-Xmx 参数一定不要忘记加)

第二步,检查错误日志,查看 “OutOfMemory” 错误前是否有其它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

重点排查以下几点:

1.检查对数据库查询中,是否有一次获得全部数据的查询。
	一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。
	这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,
	数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
 
2.检查代码中是否有死循环或递归调用。

3.检查是否有大循环重复产生新对象实体。

4.检查对数据库查询中,是否有一次获得全部数据的查询。
	一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。
	这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,
	上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

5.检查List、MAP等集合对象是否有使用完后,未清除的问题。
	List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

第四步,使用内存查看工具动态查看内存使用情况。

4、转载

https://www.cnblogs.com/Sharley/p/5285045.html

java – 如何识别JNI全局引用内存泄漏的原因?

java – 如何识别JNI全局引用内存泄漏的原因?

我正在使用Tomcat,在停止我的Web应用程序之后,仍然引用了我的Web应用程序的classloader实例.
结果是显着的内存量(主要与静态数据相关)不会被释放.迟早会导致OutOfMemoryError.

我拿了一个堆栈,我意识到它由一个JNI global reference持有,这阻止了类加载器将是garbage collected.

我的应用程序不使用JNI.我也不使用Apache Tomcat Native Library.我正在使用Sun / Oracle JDK.
我想跟踪这个全球参考的原因/起源.
(我的猜测是JVM内部引用了类加载器 – 但是为什么/在哪里?).

题:

有哪些方法/工具集可以实现?

UPDATE

看来bestsss是正确的,JNI全局引用已经被jvm调试模式引入.这有助于我,但它并没有回答这个问题,所以我仍然好奇地回答了将来可能会有帮助的问题.

解决方法

除了明显的情况:线程还有一个:

您是否在调试模式下使用应用程序?

除了系统之外,JVM不支持任何类加载器的引用,但它不涉及您.其余的JNI引用是线程或调试持有的对象(如果您不使用JNI并自己锁定对象).

JNI引用只是根,编辑你的答案,并发布这些引用所持有的对象.

Java 中关于内存泄漏出现的原因以及如何避免内存泄漏

Java 中关于内存泄漏出现的原因以及如何避免内存泄漏

转账自:http://blog.csdn.net/wtt945482445/article/details/52483944

Java 内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

  • 栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

栈与堆的区别:

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample mSample3 = new Sample();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。 
mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被 new 出来使用的。

了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。

Java 是如何管理内存

Java 的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap) 中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了 JVM 的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC 将不回收这些对象。如果某个对象 (连通子图) 与这个根顶点不可达 (注意,该图为有向图),那么我们认为这个 (这些) 对象不再被引用,可以被 GC 回收。 
以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示 JVM 的内存分配情况。以下右图,就是左边程序运行到第 6 行的示意图。

Java 使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么 GC 也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如 COM 模型采用计数器方式管理构件,它与有向图相比,精度行低 (很难处理循环引用的问题),但执行效率很高。

什么是 Java 中的内存泄露

在 Java 中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。

在 C++ 中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于 C++ 中没有 GC,这些内存将永远收不回来。在 Java 中,这些不可达的对象都由 GC 负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于 C++,程序员需要自己管理边和顶点,而对于 Java 程序员只需要管理边就可以了 (不需要管理顶点的释放)。通过这种方式,Java 提高了编程的效率。

因此,通过以上分析,我们知道在 Java 中也有内存泄漏,但范围比 C++ 要小一些。因为 Java 从语言上保证,任何对象都是可达的,所有的不可达对象都由 GC 管理。

对于程序员来说,GC 基本是透明的,不可见的。虽然,我们只有几个函数可以访问 GC,例如运行 GC 的函数 System.gc (),但是根据 Java 语言规范定义, 该函数不保证 JVM 的垃圾收集器一定会执行。因为,不同的 JVM 实现者可能使用不同的算法管理 GC。通常,GC 的线程的优先级别较低。JVM 调用 GC 的策略也有很多种,有的是内存使用到达一定程度时,GC 才开始工作,也有定时执行的,有的是平缓执行 GC,有的是中断式执行 GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC 的执行影响应用程序的性能,例如对于基于 Web 的实时系统,如网络游戏等,用户不希望 GC 突然中断应用程序执行而进行垃圾回收,那么我们需要调整 GC 的参数,让 GC 能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun 提供的 HotSpot JVM 就支持这一特性。

同样给出一个 Java 内存泄漏的典型例子,

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这个例子中,我们循环申请 Object 对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到 Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

详细 Java 中的内存泄漏

1.Java 内存回收机制

不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java 中对象是采用 new 或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由 Java 虚拟机通过垃圾回收机制完成的。GC 为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java 会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在 Java 语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值 null,以下再没有调用过,另一个是给对象赋予了新值,这样重新分配了内存空间。

2.Java 内存泄漏引起的原因

内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你 Out of memory。j

Java 内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。具体主要有如下几大类:

1、静态集合类引起内存泄漏:

像 HashMap、Vector 等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象 Object 也不能被释放,因为他们也将一直被 Vector 等引用着。

例如

Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这个例子中,循环申请 Object 对象,并将所申请的对象放入一个 Vector 中,如果仅仅释放引用本身(o=null),那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到 Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

2、当集合里面的对象属性被修改后,再调用 remove () 方法时不起作用。

例如:

public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变

set.remove(p3); //此时remove不掉,造成内存泄漏

set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

3、监听器

在 java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如 addXXXListener () 等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

4、各种连接

比如数据库连接(dataSourse.getConnection ()),网络连接 (socket) 和 io 连接,除非其显式的调用了其 close()方法将其连接关闭,否则是不会自动被 GC 回收的。对于 Resultset 和 Statement 对象可以不进行显式回收,但 Connection 一定要显式回收,因为 Connection 在任何时候都无法自动回收,而 Connection 一旦回收,Resultset 和 Statement 对象就会立即为 NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭 Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的 Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在 try 里面去的连接,在 finally 里面释放连接。

5、内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员 A 负责 A 模块,调用了 B 模块的一个方法如: 
public void registerMsg(Object b); 
这种调用就要非常小心了,传入了一个对象,很可能模块 B 就保持了对该对象的引用,这时候就需要注意模块 B 是否提供相应的操作去除引用。

6、单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被 JVM 正常回收,导致内存泄漏,考虑下面的例子:

class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

显然 B 采用 singleton 模式,它持有一个 A 对象的引用,而这个 A 类的对象将不能被回收。想象下如果 A 是个比较复杂的对象或者集合类型会发生什么情况

Android 中常见的内存泄漏汇总


集合类泄漏

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际上我们在项目中肯定不会写这么 2B 的代码,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。

单例造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,

public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个 Context,所以这个 Context 的生命周期的长短至关重要:

1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。

2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

正确的方式应该改为下面这种方式:

public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

或者这样写,连 Context 都不用传进来了:

在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,

...

context = getApplicationContext();

...
   /** * 获取全局的context * @return 返回全局context对象 */
    public static Context getContext(){
        return context;
    }

public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static AppManager getInstance() {
if (instance == null) {
instance = new AppManager();
}
return instance;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

匿名内部类 / 非静态内部类和异步线程

非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的 Activity 中,为了避免重复创建相同的数据资源,可能会出现这种写法:

        public class MainActivity extends AppCompatActivity {
        private static TestResource mResource = null;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mManager == null){
        mManager = new TestResource();
        }
        //...
        }
        class TestResource {
        //...
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这样就在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该 Activity 的引用,导致 Activity 的内存资源不能正常回收。正确的做法为:

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用 Context,请按照上面推荐的使用 Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于 Application,Service,Activity 三者的 Context 的应用场景如下:

其中: NO1 表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。而对于 Dialog 而言,只有在 Activity 中才能创建

匿名内部类

android 开发经常会继承实现 Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露

    public class MainActivity extends Activity {
    ...
    Runnable ref1 = new MyRunable();
    Runnable ref2 = new Runnable() {
        @Override
        public void run() {

        }
    };
       ...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

ref1 和 ref2 的区别是,ref2 使用了匿名内部类。我们来看看运行时这两个引用的内存:

可以看到,ref1 没什么特别的。

但 ref2 这个匿名类的实现对象里面多了一个引用:

this$0 这个引用指向 MainActivity.this,也就是说当前的 MainActivity 实例会被 ref2 持有,如果将这个引用再传入一个异步线程,此线程和此 Acitivity 生命周期不一致的时候,就造成了 Activity 的泄露。

Handler 造成的内存泄漏

Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等 api 都借助 Handler 来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

由于 Handler 属于 TLS (Thread Local Storage) 变量,生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

举个例子:

    public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
    }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在该 SampleActivity 中声明了一个延迟 10 分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish () 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish () 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

public class SampleActivity extends Activity {

  /** * Instances of static inner classes do not hold an implicit * reference to their outer class. */
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  /** * Instances of anonymous classes do not hold an implicit * reference to their outer class when they are "static". */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

综述,即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

前面提到了 WeakReference,所以这里就简单的说一下 Java 对象的几种引用类型。

Java 对引用的分类有 Strong reference, SoftReference, WeakReference, PhatomReference 四种。

在 Android 应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软 / 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软 / 弱引用的对象列表,从而为缓冲器清除已失效的软 / 弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生 OutOfMemory 异常。这时,我们可以考虑使用软 / 弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

首先定义一个 HashMap,保存软引用对象。

private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
  • 1
  • 1

再来定义一个方法,保存 Bitmap 的软引用到 HashMap。

使用软引用以后,在 OutOfMemory 异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免 Crash 发生。

如果只是想避免 OutOfMemory 异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

ok,继续回到主题。前面所说的,创建一个静态 Handler 内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,但是这样做虽然避免了 Activity 泄漏,不过 Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息。

下面几个方法都可以移除 Message:

public final void removeCallbacks(Runnable r);

public final void removeCallbacks(Runnable r, Object token);

public final void removeCallbacksAndMessages(Object token);

public final void removeMessages(int what);

public final void removeMessages(int what, Object object);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

尽量避免使用 static 成员变量

如果成员变量被声明为 static,那我们都知道其生命周期将与整个 app 进程生命周期一样。

这会导致一系列问题,如果你的 app 进程设计上是长驻内存的,那即使 app 切到后台,这部分内存也不会被释放。按照现在手机 app 内存管理机制,占内存较大的后台进程将优先回收,yi’wei 如果此 app 做过进程互保保活,那会造成 app 在后台频繁重启。当手机安装了你参与开发的 app 以后一夜时间手机被消耗空了电量、流量,你的 app 不得不被用户卸载或者静默。

这里修复的方法是:

不要在类初始时初始化静态成员。可以考虑 lazy 初始化。 
架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

避免 override finalize ()

1、finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是: 
虚拟机调用 GC 的时间不确定 
Finalize daemon 线程被调度到的时间不确定

2、finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:

含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该 Object 的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活 (即用强引用引用住该 object),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。

3、含有 Finalize 方法的 object 需要至少经过两轮 GC 才有可能被释放。

资源未关闭造成的内存泄漏

对于使用了 BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap 等资源的使用,应该在 Activity 销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

一些不良代码造成的内存压力

有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。

比如: 
Bitmap 没调用 recycle () 方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle () 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle () 就是针对 C 部分的内存释放。 
构造 Adapter 时,没有使用缓存的 convertView , 每次都在创建新的 converView。这里推荐使用 ViewHolder。

总结

对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。

尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括 context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

对于生命周期比 Activity 长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

    将内部类改为静态内部类
    静态内部类中使用弱引用来引用外部类的成员变量

Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message 和 Runnable.

在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完 Bitmap 后先调用 recycle (),再赋为 null, 清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear () ; array = null)等,最好遵循谁创建谁释放的原则。

正确关闭资源,对于使用了 BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap 等资源的使用,应该在 Activity 销毁时及时关闭或者注销。

保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。


Java 内存泄漏原因、解决办法及泄漏排查

Java 内存泄漏原因、解决办法及泄漏排查

1、什么是内存泄漏

内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。随着垃圾回收器活动的增加以及内存占用的不断增加,程序性能会逐渐表现出来下降,极端情况下,会引发OutOfMemoryError导致程序崩溃。

2、内存泄漏的原因

JVM 虚拟机是使用引用计数法和可达性分析来判断对象是否可回收,本质是判断一个对象是否还被引用,如果没有引用则回收。在开发的过程中,由于代码的实现不同就会出现很多种内存泄漏问题,让gc 系统误以为此对象还在引用中,无法回收,造成内存泄漏

3、内存泄漏有哪些情况

3.1 代码中没有及时释放,导致内存无法回收。

下面的代码,因为是双向链表,但是断开的不够彻底,prev节点依然引用这当前正在使用的节点,导致无法回收

public class ListNode {
    int val;
    ListNode next;
    ListNode prev;
    ListNode() {
    }
    ListNode(int val) {
        this.val = val;
    }
    public ListNode(int val, ListNode next, ListNode prev) {
        this.val = val;
        this.next = next;
        this.prev = prev;
    }
    public static void main(String[] args) {
        ListNode curr = new ListNode(1);
        ListNode prev = new ListNode(2);
        ListNode next = new ListNode(3);
        curr.prev = prev;
        curr.next = next;
        curr.prev = null;
    }
}

3.2 资源未关闭造成的内存泄漏

各种连接,如数据库连接、网络连接和IO连接等,文件读写等,可以使用 try-with-resources 读取完文件,自动资源释放

try (RandomAccessFile raf = new RandomAccessFile(filePath, "r");) {
        Image image = null;
while((image = parseImage(raf)) != null){
            imageList.add(image);
        }
        return imageList;
} catch(Exception e){
    log.error("parse file error, path: {},", path, e);
    return null;
}

3.3 全局缓存持有的对象不使用的时候没有及时移除,导致一直在内存中无法移除

3.4 静态集合类

如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。生命周期长的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

3.5 堆外内存无法回收

堆外内存不受gc的管理,可能因为第三方的bug出现内存泄漏

4、内存泄漏的解决办法

1.尽量减少使用静态变量,或者使用完及时 赋值为 null。

2.明确内存对象的有效作用域,尽量缩小对象的作用域,能用局部变量处理的不用成员变量,因为局部变量弹栈会自动回收;

3.减少长生命周期的对象持有短生命周期的引用;

4.使用StringBuilder和StringBuffer进行字符串连接,Sting和StringBuilder以及StringBuffer等都可以代表字符串,其中String字符串代表的是不可变的字符串,后两者表示可变的字符串。如果使用多个String对象进行字符串连接运算,在运行时可能产生大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。

5.对于不需要使用的对象手动设置null值,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象;

6.各种连接(数据库连接,网络连接,IO连接)操作,务必显示调用close关闭。

5、内存问题排查

没有任何一个程序员想要出现这种问题,但是出现了问题也要解决,内存泄漏的主要表象就是内存不足,内存告警之后如何判断是否有内存泄漏。

第一步 首先确认逻辑问题,

查看内存中对象的数量和大小,判断是否在合理的范围,如果在合理的范围内,增大内存配置,调整内存比例就可以了。

命令:

jmap -heap pid

第二步:分析gc是否正常执行

命令:

jstat -gcutil <pid> 1000

S0 — Heap上的 Survivor space 0 区已使用空间的百分比    
 
S1 — Heap上的 Survivor space 1 区已使用空间的百分比    
E — Heap上的 Eden space 区已使用空间的百分比    
O   — Heap上的 Old space 区已使用空间的百分比    
P   — Perm space 区已使用空间的百分比
YGC — 从应用程序启动到采样时发生 Young GC 的次数
YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒)    
FGC — 从应用程序启动到采样时发生 Full GC 的次数
FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒)    
GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
LGCC - 进行GC的原因(低版本jdk可能没有这一列)

从这里观察gc是否异常,也可以根据这个进行jvm内存分配调优,来提高性能降低gc对性能的损耗

第三步 确认下版本新增代码的改动,尽快从代码上找出问题。

第四步:开启各种命令行和 导出 dump 各种工具分析

-XX:+HeapDumpOnOutOfMemoryError
-XX:OnError
-XX:+ShowMessageBoxOnError

推荐使用jprofile 进行本地分析,可以不用记住那么多命令。

总结:

现在的服务器内存虽然很大,但是且用且珍惜,不要等到出现问题了才知道后果,在开发中规范自己代码,用完的对象及时释放,减少垃圾对象。出现问题了也不要慌,仔细分析代码,一切都是有原因的。

我们今天的关于一次 Java 内存泄漏的排查java内存泄漏如何排查的分享就到这里,谢谢您的阅读,如果想了解更多关于java jvm 内存溢出和内存泄漏的区别、java – 如何识别JNI全局引用内存泄漏的原因?、Java 中关于内存泄漏出现的原因以及如何避免内存泄漏、Java 内存泄漏原因、解决办法及泄漏排查的相关信息,可以在本站进行搜索。

本文标签: