内存溢出、JVM调优
1.内存溢出(Out Of Memory,简称OOM)
应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存,此时程序就运行不了。
(1)堆溢出
这种场景最为常见,报错信息:java.lang.OutOfMemoryError: Java heap space
原因:
代码中可能存在大对象分配
可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
解决方法:
检查是否存在大对象的分配,最有可能的是大数组分配
通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
(2)永久代/元空间溢出
报错信息:java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
字符串常量由永久代转移到堆中
和永久代相关的JVM参数已移除
原因:
1、在Java7之前,频繁的错误使用String.intern()方法
2、运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
3、应用长时间运行,没有重启(没有重启 JVM 进程一般发生在调试时)
解决方法:
因为该OOM原因比较简单,解决方法有如下几种:
1、检查是否永久代空间或者元空间设置的过小
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类
4、放大招,重启JVM
(3)方法栈溢出
报错信息:java.lang.OutOfMemoryError : unable to create new native Thread
原因:
出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。
解决方法:
1、通过 -Xss 降低的每个线程栈大小的容量
2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
/proc/sys/kernel/pid_max
/proc/sys/kernel/thread-max
maxuserprocess(ulimit -u)
/proc/sys/vm/maxmapcount
2.内存溢出和内存泄漏的区别,如何排错:
(1)内存泄漏:对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
(2)内存溢出:内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象等原因导致的。
手动dump
jmap -dump:format=b,file=heap.hprof PID
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
· 内存泄漏:
在Java中,内存泄漏就是存在一些被分配的对象,这些对象不会被GC所回收,然而它却占用内存。
一般来说是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用导致不能被回收。
1 静态集合类 如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。 2 各种连接,如数据库连接、网络连接和IO连接等 比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close() 方法将其连接关闭,否则是不会自动被GC 回收的。
对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try 里面去的连接,在finally里面释放连接。 3 单例模式 不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被 JVM 正常回收,导致内存泄漏, 4 内部对象持有外部类 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
3.JVM调优
Java问题排查:
(1)常见的Linux定位问题的工具
文本操作:文本查找grep、文本分析awk、文本处理sed
文件操作:文件监听tail、文件查找find
网络和进程:网络接口ifconfig、防火墙iptable -L、路由表 route -n、netstat
其它常用:进程ps -ef|grep java、分区大小df -h、内存free -m、硬盘大小 fdisk -l|grep Disk、top、环境变量env
(2)JDK自带的定位问题的工具
jps jdk提供的一个查看当前java进程的小工具
jps –l # 输出输出完全的包名,应用主类名,jar的完全路径名 |
jstack jdk自带的线程堆栈分析工具,使用该命令可以查看或导出Java应用程序中线程堆栈信息。
jstack 2815 jstack -m 2815 # java和native c/c++框架的所有栈信息 jstack -l 2815 # 额外的锁信息列表,查看是否死锁 |
jinfo jdk自带的命令,可以用来查看正在运行的java应用程序的扩展参数,包括Java System属性和JVM命令行参数;也可以动态的修改正在运行的JVM一些参数。当系统崩溃时,jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息
jinfo 2815 # 输出当前 jvm 进程的全部参数和系统属性 |
jmap 一个多功能的命令。它可以生成java程序的dump文件(进程的内存镜像),也可以查看堆内对象示例的统计信息、查看ClassLoader的信息以及finalizer队列。
# 查看堆的情况 jmap -heap 2815 # dump jmap -dump:live,format=b,file=/tmp/heap2.bin 2815 |
jstat 参数众多
jstat -gcutil 2815 1000 #垃圾收集信息 |
(3)如何使用在线调试工具Arthas
查看最繁忙的线程,以及是否有阻塞情况发生?
thread -n 3 # 查看最繁忙的三个线程栈信息 thread # 以直观的方式展现所有的线程情况 thread -b #找出当前阻塞其他线程的线程 |
Linux系统可以通过top命令查看系统的运行状态,包括负载,内存使用(交换分区),CPU使用、当前运行的线程,进程等信息。(找出使用率高的进程的pid)
确认某个类是否已被系统加载?
# 即可以找到需要的类全路径,如果存在的话 sc *MyServlet # 查看这个某个类所有的方法 sm pdai.tech.servlet.TestMyServlet * # 查看某个方法的信息,如果存在的话 sm pdai.tech.servlet.TestMyServlet testMethod |
如何查看一个class类的源码信息?
# 直接反编译出java 源代码,包含一此额外信息的 jad pdai.tech.servlet.TestMyServlet |
如何跟踪某个方法的返回值、入参?
# 同时监控入参,返回值,及异常 watch pdai.tech.servlet.TestMyServlet testMethod "{params, returnObj, throwExp}" -e -x 2 |
如何看方法调用栈的信息?
stack pdai.tech.servlet.TestMyServlet testMethod |
找到最耗时的方法调用?
# 执行的时候每个子调用的运行时长,可以找到最耗时的子调用。 stack pdai.tech.servlet.TestMyServlet testMethod |
运行此命令之后需要即时触发方法才会有响应的信息打印在控制台上
如何临时更改代码运行?
# 先反编译出class源码 jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java # 然后使用外部工具编辑内容 mc /tmp/UserController.java -d /tmp # 再编译成class # 最后,重新载入定义的类,就可以实时验证你的猜测了 redefine /tmp/com/example/demo/arthas/user/UserController.class |
如上,是直接更改线上代码的方式,但是一般好像是编译不成功的。所以,最好是本地ide编译成 class文件后,再上传替换为好!总之,已经完全不用重启和发布了!
我如何测试某个方法的性能问题?
monitor -c 5 demo.MathGame primeFactors |
(4)如何使用Idea的远程调试?
要让远程服务器运行的代码支持远程调试,则启动的时候必须加上特定的JVM参数
-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=127.0.0.1:5555 |
(5)复杂综合类型问题的定位思路
Java 性能调优工作,调优的最终目标是通过参数设置来达到快速、低延时的内存垃圾回收以提高应用吞吐量,尽可能的避免因内存回收不及时而触发的完整 GC(Full GC 会带来应用出现卡顿)。