最近在看深入理解 Java 虚拟机: JVM 高级特性与最佳实践(第二版)这本书, 理论 + 实践结合, 深入浅出, 强烈推荐给大家.
这两天在 "小怪的 java 群" 里面也对 JVM 内容进行了一个讨论, 讨论的内容主要包括如下几个方面:
1)内存溢出和内存泄露的介绍?
2)如何排查和处理内存泄露?
一, 内存溢出和内存泄露
一种通俗的说法.
1, 内存溢出: 你申请了 10 个字节的空间, 但是你在这个空间写入 11 或以上字节的数据, 出现溢出.
2, 内存泄漏: 你用 new 申请了一块内存, 后来很长时间都不再使用了(按理应该释放), 但是因为一直被某个或某些实例所持有导致 GC 不能回收, 也就是该被释放的对象没有释放.
下面具体介绍.
1.1 内存溢出
java.lang.OutOfMemoryError, 是指程序在申请内存时, 没有足够的内存空间供其使用, 出现 OutOfMemoryError.
产生原因
产生该错误的原因主要包括:
JVM 内存过小.
程序不严密, 产生了过多的垃圾.
程序体现
一般情况下, 在程序上的体现为:
内存中加载的数据量过于庞大, 如一次从数据库取出过多数据.
集合类中有对对象的引用, 使用完后未清空, 使得 JVM 不能回收.
代码中存在死循环或循环产生过多重复的对象实体.
使用的第三方软件中的 BUG.
启动参数内存值设定的过小.
错误提示
此错误常见的错误提示:
- tomcat:java.lang.OutOfMemoryError: PermGen space
- tomcat:java.lang.OutOfMemoryError: Java heap space
weblogic:Root cause of ServletException java.lang.OutOfMemoryError
- resin:java.lang.OutOfMemoryError
- java:java.lang.OutOfMemoryError
解决方法
增加 JVM 的内存大小
对于 tomcat 容器, 找到 tomcat 在电脑中的安装目录, 进入这个目录, 然后进入 bin 目录中, 在 window 环境下找到 bin 目录中的 catalina.bat, 在 linux 环境下找到 catalina.sh.
编辑 catalina.bat 文件, 找到 JAVA_OPTS(具体来说是
set "JAVA_OPTS=%JAVA_OPTS% %LOGGING_MANAGER%"
)这个选项的位置, 这个参数是 Java 启动的时候, 需要的启动参数.
也可以在操作系统的环境变量中对 JAVA_OPTS 进行设置, 因为 tomcat 在启动的时候, 也会读取操作系统中的环境变量的值, 进行加载.
如果是修改了操作系统的环境变量, 需要重启机器, 再重启 tomcat, 如果修改的是 tomcat 配置文件, 需要将配置文件保存, 然后重启 tomcat, 设置就能生效了.
优化程序, 释放垃圾
主要思路就是避免程序体现上出现的情况. 避免死循环, 防止一次载入太多的数据, 提高程序健壮型及时释放. 因此, 从根本上解决 Java 内存溢出的唯一方法就是修改程序, 及时地释放没用的对象, 释放内存空间.
1.2 内存泄露
Memory Leak, 是指程序在申请内存后, 无法释放已申请的内存空间, 一次内存泄露危害可以忽略, 但内存泄露堆积后果很严重, 无论多少内存, 迟早会被占光.
在 Java 中, 内存泄漏就是存在一些被分配的对象, 这些对象有下面两个特点:
1)首先, 这些对象是可达的, 即在有向图中, 存在通路可以与其相连;
2)其次, 这些对象是无用的, 即程序以后不会再使用这些对象.
如果对象满足这两个条件, 这些对象就可以判定为 Java 中的内存泄漏, 这些对象不会被 GC 所回收, 然而它却占用内存.
关于内存泄露的处理页就是提高程序的健壮型, 因为内存泄露是纯代码层面的问题.
1.3 内存溢出和内存泄露的联系
内存泄露会最终会导致内存溢出.
相同点: 都会导致应用程序运行出现问题, 性能下降或挂起.
不同点: 1) 内存泄露是导致内存溢出的原因之一, 内存泄露积累起来将导致内存溢出. 2) 内存泄露可以通过完善代码来避免, 内存溢出可以通过调整配置来减少发生频率, 但无法彻底避免.
二, 一个 Java 内存泄漏的排查案例
某个业务系统在一段时间突然变慢, 我们怀疑是因为出现内存泄露问题导致的, 于是踏上排查之路.
2.1 确定频繁 Full GC 现象
首先通过 "虚拟机进程状况工具: jps" 找出正在运行的虚拟机进程, 最主要是找出这个进程在本地虚拟机的唯一 ID(LVMID,Local Virtual Machine Identifier), 因为在后面的排查过程中都是需要这个 LVMID 来确定要监控的是哪一个虚拟机进程.
同时, 对于本地虚拟机进程来说, LVMID 与操作系统的进程 ID(PID,Process Identifier)是一致的, 使用 Windows 的任务管理器或 Unix 的 ps 命令也可以查询到虚拟机进程的 LVMID.
jps 命令格式为:
jps [ options ] [ hostid ]
使用命令如下:
使用 jps:jps -l
使用 ps:
ps aux | grep tomat
找到你需要监控的 ID(假设为 20954), 再利用 "虚拟机统计信息监视工具: jstat" 监视虚拟机各种运行状态信息.
jstat 命令格式为:
jstat [ option vmid [interval[s|ms] [count]] ]
使用命令如下:
jstat -gcutil 20954 1000
意思是每 1000 毫秒查询一次, 一直查. gcutil 的意思是已使用空间站总空间的百分比.
结果如下图:
jstat 执行结果
查询结果表明: 这台服务器的新生代 Eden 区 (E, 表示 Eden) 使用了 28.30%(最后)的空间, 两个 Survivor 区 (S0,S1, 表示 Survivor0,Survivor1) 分别是 0 和 8.93%, 老年代 (O, 表示 Old) 使用了 87.33%. 程序运行以来共发生 Minor GC(YGC, 表示 Young GC)101 次, 总耗时 1.961 秒, 发生 Full GC(FGC, 表示 Full GC)7 次, Full GC 总耗时 3.022 秒, 总的耗时 (GCT, 表示 GC Time) 为 4.983 秒.
2.2 找出导致频繁 Full GC 的原因
分析方法通常有两种:
1)把堆 dump 下来再用 MAT 等工具进行分析, 但 dump 堆要花较长的时间, 并且文件巨大, 再从服务器上拖回本地导入工具, 这个过程有些折腾, 不到万不得已最好别这么干.
2)更轻量级的在线分析, 使用 "Java 内存影像工具: jmap" 生成堆转储快照(一般称为 headdump 或 dump 文件).
jmap 命令格式:
jmap [ option ] vmid
使用命令如下:
jmap -histo:live 20954
查看存活的对象情况, 如下图所示:
存活对象
按照一位 IT 友的说法, 数据不正常, 十有八九就是泄露的. 在我这个图上对象还是挺正常的.
我在网上找了一位博友的不正常数据, 如下:
image.png
可以看出 HashTable 中的元素有 5000 多万, 占用内存大约 1.5G 的样子. 这肯定不正常.
2.3 定位到代码
定位带代码, 有很多种方法, 比如前面提到的通过 MAT 查看 Histogram 即可找出是哪块代码.-- 我以前是使用这个方法. 也可以使用 BTrace, 我没有使用过.
如果你也想进群讨论, 加我微信: huangtao1052
来源: https://juejin.im/entry/5b2c9a376fb9a00e5326e05e