Java问题定位与深度调试技术(三)——Java内存泄漏分析和堆内存设置

282 阅读7分钟

一、内存泄漏的常见情况

  1. 全局容器诸如HashMap这样的没有及时remove
  2. Runnable对象等被Java虚拟机自身管理的对象,并没有被正确的释放渠道。Runnable对象必须交给一个Thread去执行,否则该对象就永远不会消亡。因为这种对象虽然不会被应用程序中的其他用户对象访问,但是会被虚拟机内部引用。
  3. Thread如果忘记调用start,会导致内存泄漏。
  4. 打开文件忘记关闭会导致内存泄漏。

二、对象和引用的关系

Person p1 = new Person();

p1作为引用本身也是占内存空间的,Person()作为对象肯定也占内存空间。

三、JVM内存类型

Java进程内存是指整个Java进程占用的内存,等于Java堆内存+Perm内存+本地内存。(这是JDK1.8之前的内存模型,不同版本的JDK有不同的内存模型,一定要确认好你是哪个JDK版本,否则有些JDK参数-XX是失效的

内存类型作用
Java堆内存通过-Xmx -Xms设置。大量长期存活的大对象是常见的OOM的原因。
Perm内存(Permanent Generation space,内存的永久保存区域)通过-XX:PermSize设置初始的内存,-XX:MaxPermSize设置最大Perm内存。这块内存是虚拟机用来加载class字节码文件的内存。一般情况下比较稳定,在AOP编程中,会动态进行代码织入。可能会导致类改变或者增加新的类,类需要重新加载。
本地内存JVM用于虚拟机内部运作的内存。JVM使用的本地内存数量取决于Java用户代码创建的线程数量、对象引用管理的内存,以及JIT本地代码生成等过程中使用的临时内存空间。如果有一个第三方本地模块,那么它也可能使用本地内存,如本地JDBC驱动程序就会分配本地内存。本地内存的大小是不需要设置的。

        最大本地内存既受特定操作系统上虚拟进程大小的限制的约束,也受到-Xmx标志指定用于Java堆内存量限制。例如,如果操作系统允许应用程序最大为3GB的内存使用量,并且Java堆(-Xmx)的大小设定为1GB,那么本地内存量的最大值可能在2GB左右。

        本地内存泄漏导致的OOM,原因一般有三

  • 如果系统中存在JNI调用,本地内存泄漏可能存在于JNI代码当中。
  • JDK的bug
  • 操作系统的bug

四、内存泄漏定位和分析

4.1 Java堆内存泄漏

当怀疑有内存泄漏的时候,在Java命令行中增加

-verbose:gc //在控制台输出GC情况
-XX:+PrintGCDetails  //在控制台输出详细的GC情况
-Xloggc: filepath  //将GC日志输出到指定文件中

然后重启Java进程。在系统运行过程中,JVM进行垃圾回收的同时,会将垃圾回收的日志打印出来。

1707900540237.png GC分为普通GC和Full GC,我们分析内存泄漏,更关注Full GC。判断系统是否存在内存泄漏的依据是,如果系统存在内存泄漏,那么完全垃圾回收完之后的内存使用值应该持续上升。

        步骤如下:

  1. 截取系统稳态运行以后的GC信息(如初始化已经完成,相应的缓存已经建立等)。这点非常重要,非稳态运行期的信息无分析价值,因为无法确认内存的增长是正常的增长,还是由于内存泄漏导致的非正常增长。

  2. 过滤出Full GC的行。只有Full GC才具有分析价值。因为Full GC后的内存是当前Java对象真正使用的内存数量。一般会出现两种情况。 ①当前如果Full GC之后内存持续增长,朝着Xmx的趋势,那么可以判定系统存在内存泄漏。②有回落,处于动态平衡,那么可能是系统周期缓存引起的,是正常的。

4.2 本地内存泄漏

从GC信息输出来看,不存在Java堆内存的泄漏,但使用内存工具(windows的资源管理器、UNIX的prstat等)发现Java进程的总内存越来越大,一直到崩溃。无法创建线程导致的"java.lang.OutOfMemoryError unable to create new native thread"。另外32位的系统进程最多可以利用4G(2的32次方)大小。而且不能叠加,多个进程也只能用4G。64位是4G x 4G

本地内存泄漏导致的OOM,原因一般有三

  • 如果系统中存在JNI调用,本地内存泄漏可能存在于JNI代码当中:通过C/C++的内存泄漏分析方法可以进行定位(结合pmap等进行初步确认)。
  • JDK的bug:如果JNI中不存在内存泄漏,更新JDK排除法发现问题
  • 操作系统的bug

4.3 Perm内存泄漏

一般是Web应用引入了太多的三方jar,通常设置Perm大小可以解决。 Java支持动态修改类和动态生成类也可能导致。

-verbose:class

在Java启动命令行中添加这个可以查到具体虚拟机所加载的类。如果系统持续不断地打印加载某些类,就需要多关注一下了。

五、定位内存泄漏工具介绍

JProfiler、Optimizelt、JProbe、Jconsole、-Xrunhprof(详情见runhprof使用)、JDK1.6自带工具、其他工具,如MDD4J、BEA JRockit虚拟机自带分析工具等。

这些工具从JVM获取系统内存信息的方法有两种。

技术原理
JVMTI外部工具与JVM通信并从JVM收集信息的标准化接口
字节码技术(byte code instrumentation)用探测器处理字节码以获得工具所需信息的技术

这两种技术都不适合用于生产环境:造成的CPU和内存开销大。只要是JVM是使用-Xmanagement选项(允许通过远程JMX接口监控和管理JVM)启动的,Memory Leak Detector就可以与运行中的JVM进行连接或断开。分析的方式一般都是增加压力挂上Jprofiler在快到Xmx之前发现问题,否则系统core dump则无法分析了。

5.1 真实环境下的内存泄漏定位

在SUN的JDK下,可以采用收集内存分配数据进行分析

# 1.可以打印当前对象的个数和大小
jmap -histo <java pid> > objhist.log
# 2.如果系统已经出现OOM并停止工作可以通过以下命令获取内存信息
jmap -heap: format=b <java pid> 
# 3.在启动期间添加
-XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpPath="具体的路径"
# 一旦发生OOM就会将内存信息和堆信息收集到具体的路径下面

5.2 top陷阱

top观察的是整个JVM所占用的总内存,包括Java堆内存、permsize和jvm自身运行需要的本地内存。要看堆内存,得看GC日志。

其他

java -verbose[:class|gc|jni] 在输出设备上显示

1、 -verbose:class  

在程序运行的时候究竟会有多少类被加载呢,一个简单程序会加载上百个类的!
你可以用verbose:class来监视,在启动参数中加上 -verbose:class 可以查看到加载的类的情况。

2、 –verbose:gc

在启动参数中加上 -verbose:gc 当发生gc时,可以打印出gc相关的信息;该信息不够高全面,等同于-XX:+PrintGC。
其实只要设置-XX:+PrintGCDetails 就会自动带上-verbose:gc和-XX:+PrintGC

3、–verbose:jni

输出native方法调用的相关情况,一般用于诊断jni调用错误信息。
在虚拟机调用native方法时输出设备显示信息,格式如下: [Dynamic-linking native method HelloNative.sum ... JNI] 
该参数用来监视虚拟机调用本地方法的情况,在发生jni错误时可为诊断提供便利。
# 查看java的实际堆内存配置
jmap -heap pid

# linux系统上可以输出到具体的文件里面
java -verbose:class 程序名 > log.log 

一篇关于java内存问题的排查 cloud.tencent.com/developer/a…