为了判断 Java 中是否有内存泄漏, 我们首先必须了解 Java 是如何管理内存的. 下面我们先给出一个简单的内存泄漏的例子, 在这个例子中我们循环申请 Object 对象, 并将所申请的对象放入一个 HashMap 中, 如果我们仅仅释放引用本身, 那么 HashMap 仍然引用该对象, 所以这个对象对 GC 来说是不可回收的.
- HashMap mapObj = new HashMap()
- public void myfun() {String obj1 = new String("abcd");
- mapObj.put(obj1, obj1);
- ...
- obj1 = null; // 此时 obj1 指向的物理内存没有释放, 因为 hashmap 引用该地址
- }
JVM 可以自动回收垃圾, 但它只能回收满足条件的垃圾, 有时需要们确保条件的满足. 如果程序中, 存在越来越多不在影响程序未来执行的对象 (也就是不再需要的对象), 而且这些对象和根对象之间存在引用路径, 那么就发生了内存泄漏.
内存泄漏常发生在如下场景:
全局容器类, 对象不再需要时, 忘记从容器中 remove
像 Runnable 对象等被 Java 虚拟机自身管理的对象, 没有正确的释放渠道. Runnable 对象必须交给一个 Thread 去 run, 否则该对象就永远不会消亡
1.1 Java 对象的 Size
在 64 位的平台上, Java 对象的占用内存如下
类型 | 大小 |
---|---|
Object | 16 |
Float | 16 |
Double | 24 |
Integer | 16 |
Long | 24 |
1.2 对象及其引用
为了说明对象和引用, 我们先定义一个简单的类
- class Person {
- String name;
- int age;
- }
Person p1 = new Person() 包含如下几个动作
右边的 new Person 在堆空间分配一块内存, 创建一个 Person 类对象
末尾的 () 意味着创建对象之后, 立即调用构造函数, 进行初始化
左边的 Person p1 创建了一个引用变量, 所谓引用变量, 就是后来用于指向 Person 类示例的引用
= 符号使刚刚创建的对象引用指向刚刚创建的对象
上面的代码如下所示:
如果再将对象赋值给 p2 的话, 变成下面这样的
执行 p2 = new Person() 之后变成
1.3 虚拟机垃圾自动回收机制
垃圾自动回收做两件事情:
标记垃圾
清除垃圾
标记过程现在主要使用 根可达性 分析 (还有引用计数法等), 清除之后可能会有一些小的内存快, 所有还有压缩的过程.
下图中的灰色对象表示可以被回收的对象 (根不可达)
哪些对象可以成为 根 呢? http://help.eclipse.org/luna/index.jsp?topic=/org.eclipse.mat.ui.help/concepts/gcroots.html&cp=37_2_3
没有被任何外部对象引用的栈上的对象
静态变量
JNI handler 包括全局和局部
系统 Class
存活着的监视器
2 内存泄漏的症状
2.1 为什么会发生 OOM 问题?
内存不足会有三种情况:
对内存不足
本地内存不足
Perm 内存不足
发生 OOM 的时候, 可以检查如下几个方面:
应用程序的缓存功能
大量长期活动对象
对内存泄漏
本地内存泄漏
2.2 内存泄漏的症状
内存泄漏一般会有如下几个症状:
系统越来越慢, 并且有 CPU 使用率过高
运行一段时间后, OOM
虚拟机 core dump
3 内存泄漏的定位和分析
内存泄漏的分析并不复杂, 但需要耐心, 一般内存泄漏只能事后分析, 而重现问题需要耐心.
3.1 对内存泄漏定位
当出现 java.lang.OutOfMemoryError: Java Heap Space 异常, 就表示堆内存不足了. 堆内存不足的原因有如下几种:
堆内存设置太小
内存泄漏
设计不足, 缓存了多余的数据
如果怀疑有内存泄漏, 可以添加 -verbose:gc 参数后重现启动 Java 进程, 输出大致如下:
- 8190.813: [GC 164675K->251016K(1277056K), 0.0117749 secs] #8190.813 表示垃圾回收的时间点, 秒为单位. GC/Full GC 表示垃圾回收的类型
- 8190.825: [Full GC 251016K->164654K(1277056K), 0.8142190 secs] # 251016K 表示回收前占用的内存大小, 164654K 表示回收后占用的内存大小, 1277056K 表示当前对内存总大小, 0.8142190 表示回收耗时
- 8191.644: [GC 164678K->251214K(1277248K), 0.0123627 secs]
- 8191.657: [Full GC 251214K->164661K(1277248K), 0.8135393 secs]
- 8192.478: [GC 164700K->251285K(1277376K), 0.0130357 secs]
- 8192.491: [Full GC 251285K->164670K(1277376K), 0.8118171 secs]
- 8193.311: [GC 164726K->251182K(1277568K), 0.0121369 secs]
- 8193.323 : [Full GC 251182K->164644K(1277568K), 0.8186925 secs]
- 8194.156: [GC 164766K->251028K(1277760K), 0.0123415 secs]
- 8194.169: [Full GC 251028K->164660K(1277760K), 0.8144430 secs]
怀疑内存泄漏后, 我们通过 Full GC 日志进一步确认, 检查 Full GC 后的可用内存是否持续增大. 步骤如下:
获取系统稳定后的 GC 日志 (不稳定的日志不可靠)
过滤 FullGC 日志, 可能会有如下两种情况
FullGC 后内存使用量持续增长, 一直到设置的堆内存最大值, 基本可以确定内存泄漏
内存使用量增长后又回落, 出于一个动态平衡区间, 基本排除内存泄漏
GC 日志只能帮忙找到是否有泄漏, 找出内存泄漏的地方, 需要依赖一些其他的工具
- JProfile
- OptimizedIt
- JProbe
- JConsole
- -Xrunhprof
3.2 本地内存泄漏的定位
GC 日志无异常, 但 Java 进程使用内存逐渐增大, 并且无停止上涨的趋势. 本地内存泄漏的原因有如下几个:
JNI 调用中出现内存泄漏 (JNI 调用出现内存泄漏, 可以使用 C/C++ 内存泄漏分析方法定位)
JDK bug
操作系统问题
本地内存泄漏可能伴有如下异常
- java.lang.OutOfMemoryError: unable to create new native thread , Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
- at java.lang.Thread.start0(Native Method)
- at java.lang.Thread.start(Thread.java:574)
- at TestThread.main(TestThread.java:34)
上面这个异常可能的原因有:
创建的线程过多, 可打印总线程数查看
swap 分区不足
堆内存过大, 本地内存不足
3.3 Perm 区内存不足定位
出现 java.lang.OutOfMemoryError: PermGen space Perm , 说明 Perm 区内存不足
依赖注入, 没有卸载
Perm 区太小
来源: http://www.tuicool.com/articles/e2Urqyy