Java内存泄漏的原因和检测

几次面试都遇到了Java内存泄漏的问题,哪些情况会导致内存泄漏倒还好理解,如何分析内存泄漏基本上都不懂,只知道JDK/bin目录下有几个工具可用。这里简单的总结一下。


内存泄漏及其原因

定义

  • 内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,但有时也会很严重,会提示你OutOfMemoryError异常。

导致内存泄漏的几种原因

静态集合类引起内存泄露

  • 像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放。
1
2
3
4
5
6
7
8
9
public static List<Object> list = new ArrayList<>();
public void test() {
for(int i = 0; i < 1000; i++) {
Object o = new Object();
list.add(o)
// list仍然持有Object对象。
o = null;
}
}

集合中的可变对象修改后,再次移除则不起作用

  • 一般是HashSet, HashMap, 主键的key的hashCode变化以后,添加或者删除都是映射到不同的桶中。所以对于HashSet或者HashMap的Key,都应该是不可变类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class person{
//....
@Override
public int hashCode() {
return name.hashCode() + passwd.hashCode() + age;
}

public static void main(String[] args) {
Set<Person> persons = new HashSet<>();
Person p1 = new Person("AAAA", "aaaa", 25);
Person p2 = new Person("BBBB", "bbbb", 27);
Person p3 = new Person("CCCC", "cccc", 30);
persons.add(p1);
persons.add(p2);
persons.add(p3);
System.out.println("size:" + persons.size()); // 3

p3.setAge(31); // 修改属性。
persons.remove(p3); // 移除不掉.
persons.add(p3); // 添加成功.

System.out.println("size:" + persons.size()); // 4
}
}

监听器

  • addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

各种连接

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

内部类和外部模块等的引用

  • 内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。非静态内部类的对象会隐式强引用其外围对象,所以在内部类未释放时,外围对象也不会被释放,从而造成内存泄漏
  • 使用了第三方的Jar包,内部的对象的引用情况不明晰。

单例模式

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

虚拟机监控工具

jps:虚拟机进程状况工具

  • JDK的很多小工具的名字都参考了UNIX命令的命名方式,jps(JVM Process Status Tool)是其中的典型。 除了名字像UNIX的ps命令之外,它的功能也和ps命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。 虽然功能比较单一,但它是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程。

jstat:虚拟机统计信息监视工具

  • jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

jstat-tools.png-423.7kB

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
1. jstat -gc pid
可以显示gc的信息,查看gc的次数,及时间。
其中最后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。

2.jstat -gccapacity pid
可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小,如:PGCMN显示的是最小perm的内存使用量,PGCMX显示的是perm的内存最大使用量,PGC是当前新生成的perm内存占用量,PC是但前perm内存占用量。其他的可以根据这个类推, OC是old内纯的占用量。

3.jstat -gcutil pid
统计gc信息统计。

4.jstat -gcnew pid
年轻代对象的信息。

5.jstat -gcnewcapacity pid
年轻代对象的信息及其占用量。

6.jstat -gcold pid

old代对象的信息。

7.stat -gcoldcapacity pid
old代对象的信息及其占用量。

8.jstat -gcpermcapacity pid
perm对象的信息及其占用量。

9.jstat -class pid
显示加载class的数量,及所占空间等信息。

10.jstat -compiler pid
显示VM实时编译的数量等信息。

11.stat -printcompilation pid

当前VM执行的信息。
一些术语的中文解释:
S0C:年轻代中第一个survivor(幸存区)的容量 (字节)
S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
EC:年轻代中Eden(伊甸园)的容量 (字节)
EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)
OCOld代的容量 (字节)
OUOld代目前已使用空间 (字节)
PCPerm(持久代)的容量 (字节)
PUPerm(持久代)目前已使用空间 (字节)
YGC:从应用程序启动到采样时年轻代中gc次数
YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
FGC:从应用程序启动到采样时old代(全gc)gc次数
FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
GCT:从应用程序启动到采样时gc用的总时间(s)
NGCMN:年轻代(young)中初始化(最小)的大小 (字节)
NGCMX:年轻代(young)的最大容量 (字节)
NGC:年轻代(young)中当前的容量 (字节)
OGCMNold代中初始化(最小)的大小 (字节)
OGCMXold代的最大容量 (字节)
OGCold代当前新生成的容量 (字节)
PGCMNperm代中初始化(最小)的大小 (字节)
PGCMXperm代的最大容量 (字节)
PGCperm代当前新生成的容量 (字节)
S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
E:年轻代中Eden(伊甸园)已使用的占当前容量百分比
Oold代已使用的占当前容量百分比
Pperm代已使用的占当前容量百分比
S0CMX:年轻代中第一个survivor(幸存区)的最大容量 (字节)
S1CMX :年轻代中第二个survivor(幸存区)的最大容量 (字节)
ECMX:年轻代中Eden(伊甸园)的最大容量 (字节)
DSS:当前需要survivor(幸存区)的容量 (字节)(Eden区已满)
TT:持有次数限制
MTT :最大持有次数限制

jinfo:Java配置信息工具

  • jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。 使用jps命令的-v参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用jinfo的-flag选项进行查询了(如果只限于JDK 1.6或以上版本的话,使用java-XX:+PrintFlagsFinal查看参数默认值也是一个很好的选择),jinfo还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来。 这个命令在JDK 1.5时期已经随着Linux版的JDK发布,当时只提供了信息查询的功能,JDK1.6之后,jinfo在Windows和Linux平台都有提供,并且加入了运行期修改参数的能力,可以使用-flag[+|-]name或者-flag name=value修改一部分运行期可写的虚拟机参数值。

jmap:Java内存映像工具

  • jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。 如果不使用jmap命令,要想获取Java堆转储快照,还有一些比较“暴力”的手段:譬如在第2章中用过的-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成dump文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“吓唬”一下虚拟机,也能拿到dump文件。
  • jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、 当前用的是哪种收集器等

jhat:虚拟机堆转储快照分析工具

  • JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。 jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。 不过实事求是地说,在实际工作中,除非笔者手上真的没有别的工具可用,否则一般都不会去直接使用jhat命令来分析dump文件,主要原因有二:一是一般不会在部署应用程序的服务器上直接分析dump文件,即使可以这样做,也会尽量将dump文件复制到其他机器上进行分析,因为分析工作是一个耗时而且消耗硬件资源的过程,既然都要在其他机器进行,就没有必要受到命令行工具的限制了;另一个原因是jhat的分析功能相对来说比较简陋,VisualVM,以及专业用于分析dump文件的Eclipse Memory Analyzer、 IBM HeapAnalyzer等工具,都能实现比jhat更强大更专业的分析功能。

jstack:Java堆栈跟踪工具

  • jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源

JConsole:Java监视与管理控制台

  • JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、 管理工具。
  • 当JConsole成功建立连接,它从连接上的JMX代理处获取信息,并且以下面几个标签页呈现信息。
    • Summary tab. 监控JVM和一些监控变量的信息。
    • Memory tab. 内存使用信息
    • Threads tab. 线程使用信息
    • Classes tab. 类调用信息
    • VM tab. JVM的信息
    • MBeans tab.所有MBeans的信息

VisualVM:多合一故障处理工具

  • VisualVM基于NetBeans平台开发,因此它一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM可以做到:
    • 显示虚拟机进程以及进程的配置、 环境信息(jps、 jinfo)。
    • 监视应用程序的CPU、 GC、 堆、 方法区以及线程的信息(jstat、 jstack)。
    • dump以及分析堆转储快照(jmap、 jhat)。
    • 方法级的程序运行性能分析,找出被调用最多、 运行时间最长的方法。
    • 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照,可以将快照发送开发者处进行Bug反馈。
  • GC的时序图

gc-pic.jpg-32.8kB

Eclipse Memory Analyzer(MAT)

  • MAT官网
  • MAT可以对堆dump的文件进行分析,可以去detail页看线程各个对象的使用数目等情况。
  • MAT分析dump文件后提供了两个报表
    • 内存泄露报表,自动检查可能存在内存泄露的对象,通过报表展示存活的对象以及为什么他们没有被垃圾收集;
    • 对象报表,对可颖对象的分析,如字符串是否定义重了,空的collection、finalizer以及弱引用等。
  • 详细见后文reference提供的几个链接中MAT的使用情况。

The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption.

Use the Memory Analyzer to analyze productive heap dumps with hundreds of millions of objects, quickly calculate the retained sizes of objects, see who is preventing the Garbage Collector from collecting objects, run a report to automatically extract leak suspects.


一个例子

  • 首先通过jps找到java进程ID。然后top -p [pid]发现内存占用达到了最大值(-Xmx)。开始怀疑是由于频繁Full GC导致的,于是通过jstat -gcutil [pid] 60000查看GC的情况,其中60000表示每隔1分钟输出一次。果然是Full GC次数太多,JVM大部分时间都进行Full GC,而此时JVM会暂停其他一切工作,所以程序运行得非常慢。
  • 那到底的程序的哪一部分导致消耗了这么多的内存呢?通过jmap -histo:live [pid]查看进程中各种类型的对象创建了多少个,以及每种类型的对象占多少内存。当我看到有个对象被创建了5千多万个实例时,我就能定位到是哪儿的问题了。
  • 顺带说一下,通过jmap还可以生成JVM的内存dump文件,命令为jmap -dump:format=b,file=文件名 [pid],然后通过jhat命令在浏览器中查看,或者通过jvisualvm、mat等工具进行查看。使用jhat命令查看的方式为:jhat -J -Xmx1024M [file],等控制台输出Started HTTP server on port 7000. Server is ready.后在浏览器中输入ip:7000就可以查看各上类中各种实例被创建了多少个。

参考