Java 内存泄漏分析和对内存设置
1 内存泄漏的背景知识
为了判断 Java 中是否有内存泄漏,我们首先必须了解 Java 是如何管理内存的。下面我们先给出一个简单的内存泄漏的例子,在这个例子中我们循环申请 Object 对象,并将所申请的对象放入一个 HashMap 中,如果我们仅仅释放引用本身,那么 HashMap 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。
|
|
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 对象及其引用
为了说明对象和引用,我们先定义一个简单的类
|
|
Person p1 = new Person() 包含如下几个动作
- 右边的 new Person 在堆空间分配一块内存,创建一个 Person 类对象
- 末尾的 () 意味着创建对象之后,立即调用构造函数,进行初始化
- 左边的 Person p1 创建了一个引用变量,所谓引用变量,就是后来用于指向 Person 类示例的引用
- = 符号使刚刚创建的对象引用指向刚刚创建的对象
上面的代码如下所示:
如果再将对象赋值给 p2 的话,变成下面这样的
执行 p2 = new Person() 之后变成
1.3 虚拟机垃圾自动回收机制
垃圾自动回收做两件事情:
- 标记垃圾
- 清除垃圾
标记过程现在主要使用 根可达性 分析(还有引用计数法等),清除之后可能会有一些小的内存快,所有还有压缩的过程。
下图中的灰色对象表示可以被回收的对象(根不可达)
哪些对象可以成为 根 呢? http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.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 进程,输出大致如下:
|
|
怀疑内存泄漏后,我们通过 Full GC 日志进一步确认,检查 Full GC 后的可用内存是否持续增大。步骤如下:
- 获取系统稳定后的 GC 日志(不稳定的日志不可靠)
- 过滤 FullGC 日志,可能会有如下两种情况
- FullGC 后内存使用量持续增长,一直到设置的堆内存最大值,基本可以确定内存泄漏
- 内存使用量增长后又回落,出于一个动态平衡区间,基本排除内存泄漏
GC 日志只能帮忙找到是否有泄漏,找出内存泄漏的地方,需要依赖一些其他的工具
- JProfile
- OptimizedIt
- JProbe
- JConsole
- -Xrunhprof
3.2 本地内存泄漏的定位
GC 日志无异常,但 Java 进程使用内存逐渐增大,并且无停止上涨的趋势。本地内存泄漏的原因有如下几个:
- JNI 调用中出现内存泄漏(JNI 调用出现内存泄漏,可以使用 C/C++ 内存泄漏分析方法定位)
- JDK bug
- 操作系统问题
本地内存泄漏可能伴有如下异常
上面这个异常可能的原因有:
- 创建的线程过多,可打印总线程数查看
- swap 分区不足
- 堆内存过大,本地内存不足
3.3 Perm 区内存不足定位
出现 java.lang.OutOfMemoryError: PermGen space Perm ,说明 Perm 区内存不足
- 依赖注入,没有卸载
- Perm 区太小
3.4 分析方法
- jmap -histo
> objhist.log, - 如果系统已经 OutOfMemory,可以使用 jmap-heap:format=b
获取内存信息 - 添加参数 -XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=”$PATH” 当发生 OOM 时会收集相应信息