【Java面试高频-JVM】- Java内存泄露是怎么样的呢?

首先先来理解一下内存泄露和内存溢出。

内存溢出:申请内存时,没有足够的内存可以使用了;

内存泄露:申请了内存用完了不释放,比如一共有1024M的内存,分配了521M的内存一直不回收。

例子来通俗易懂的了解Java中内存泄露

java内存泄露demo java内存泄露的图片_jvm内存泄露

如上图:

对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长;

那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;

如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

1 内存泄露的常见原因

  • 循环过多或死循环,产生大量对象;
  • 静态集合类引起内存泄露,因为静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放;下面这个例子中,list 是静态的,只要 JVM 不停,那么 obj 也一直不会释放。
public class OOM {   
    static List list = new ArrayList();   
    public void oomTests(){    
        Object obj = new Object();    
        list.add(obj);   
}}
  • 单例模式:和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
  • 数据库连接、IO、Socket连接,必须显示释放,否则不会被GC回收;
try {   
 Connection conn = null;  
 Class.forName("com.mysql.jdbc.Driver");   
 conn = DriverManager.getConnection("url","", "");  
 Statement stmt = conn.createStatement() ;   
 ResultSet rs = stmt.executeQuery("....") ; 
 } catch (Exception e) { 
 //异常日志
 } finally {   
 //1.关闭结果集 Statement   
 //2.关闭声明的对象 ResultSet   
 //3.关闭连接 Connection
 }
  • 内部类的对象被长期持有,那么内部类对象所属的外部类对象也不会被回收。

2 如何避免Java的内存泄露

  • 特别注意使用static,把它的生命周期跟JVM本身的生命周期绑定在一起,这使得对象本身无法被回收;
  • 未关闭流**,java7中引入了try-with-resource语句**
  • 对要放入hashset或者hashmap中作为key存在的对象添加hashcode()andequals()方法。还有一种非常普遍的内存泄露场景就是对要放入HashSet中的对象,缺少hashCode或者equals方法
    具体的,当我们将重复的对象放入集合中的时候–这将导致集合增长,而不是忽略重复对象。这也将导致内存泄露

3.项目中服务内存泄露排查(重点)

首先明确什么是内存泄露。

在Java中,内存泄露就是存在一些被分配的对象,这些对象有下面两个特点:

  • 首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;
  • 其次,这些对象是无用的,即程序以后不会在使用这些对象。

如果对象满足这两个 条件,这些对象就可以判定为Java中的内存泄露,这些对象不会被GC所回收,然后它却占用内存。

原因是:长生命周期的引用了短生命周期的对象;

解决步骤:

  • free -h命令来查看服务器中内存使用情况是怎么样的;

java内存泄露demo java内存泄露的图片_内存泄露_02

  • top命令查看实时的进程数;top -Hp pid查看该进程下所有线程数量的内存使用情况

java内存泄露demo java内存泄露的图片_内存泄露_03

问题分析:

要分析是否存在内存泄露的问题,需要分析该Java进程的堆内存的使用情况,GC情况,需要分析该Java进程的堆内存的老年代空间占比大对象的成因,继而避免这些对象无法被回收,从而解决内存溢出问题。

第一步:查看堆情况

使用下述命令来查看进程堆内存使用情况

jhsdb jmap --pid your-java-serverce-pi --heap

java内存泄露demo java内存泄露的图片_java内存泄露_04

这个命令只能反映当前堆内存情况,用来查看堆大小、代分配是否合理还行,更多的信息则无法很直观的看出来。

第二步:查看GC情况

使用命令查看进程 GC 情况:

jstat -gcutil your-java-service-pid 1000 100

java内存泄露demo java内存泄露的图片_java内存泄露_05

这个命令可以查看进程启动以来 Young GC / Full GC 的次数及时间,并且会间隔很短展示最新数据,主要用于判断系统 GC 频率是否有问题,GC 时间是否过长影响系统正常运行等。

第三步:查看GC历史

要查看GC历史,需要打印GC日志,应用启动命令类似如下:

nohup java -XX:+PrintGCDetails \  -Xloggc:log/gc.log \  -XX:+HeapDumpOnOutOfMemoryError \  -XX:HeapDumpPath=log/dump.log \      -jar ${linkname} > nohup.out 2>&1 &

查看服务运行两三天后的GC日志,过滤出其中 Full GC 的信息

java内存泄露demo java内存泄露的图片_jvm内存泄露_06

可以观察到,经过多次 Full GC,箭头右边的反映 GC 后老年代堆内存大小的数值在增加。这说明有老年对象一直没有被释放,某个对象对这些对象的引用一直维持着。这种趋势下去,OutOfMemory 错误的出现不可避免,完全实锤了该服务的内存泄漏问题。

第四步:查看堆实例对象分别

查看进程堆中当前实例数前 20 类排名:

jmap -histo:live your-java-service-pid | head -n 20

java内存泄露demo java内存泄露的图片_生命周期_07

查看进程堆中当前实例数前 20 且为该项目包路径下类排名:

jmap -histo:live your-java-service-pid | grep your-project-package-typical-word | head -n 20

这里笔者犯了一个严重错误,认为只有项目包路径下的对象个数异常才说明有内存泄漏,但是忽略了写一个项目本身就使用了很多第三方类库的事实,所以上述两个查询中展示的对象都可以用于排查内存泄漏问题。

从查询一可以看出,堆内存中 HashMap$Node 这一 HashMap 的静态内部类实例数量非常多,引起了笔者同事的怀疑。查遍项目代码中,并没有直接使用 HashMap 的地方,笔者同事再显神威,指出项目中使用的 LinkedMultiValueMap 内部正是套了一个 HashMap 来实现的。

Full GC问题排查 & 调优

  • Metaspace调优,比如目前的生产环境Metaspace基本上会设置为256M或者512M,可以根据应用的类型和机器内存配置来决定。如果机器内存允许的情况下,可以适当调大Metaspace;
  • 如果dump GC日志之后发现很多classloader名称前缀相同,排查是否有这种动态代理技术的使用,可能在不断生成代理对象;
  • 如果内存缓慢增长,GC回收不掉,dump GC日志,查看是否有类被重复加载;

常见的内存泄露的原因:

  • 循环过多或死循环,产生大量对象;
  • 静态集合对象引起内存泄露,因为静态集合的生命周期和JVM周期一一致;
  • 单例模式下如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄露;
  • 数据库连接、IO、Socket连接,必须显示释放,否则不会被GC回收;
  • 内部类的对象被长期持有;

避免Java的内存泄露

  • 特别注意使用static,把它的生命周期跟JVM本身的生命周期绑定在一起,这使得对象本身无法被回收;
  • 未关闭流,java7中引入了try-with-resource语句**
  • 对要放入hashset或者hashmap中作为key存在的对象添加hashcode()andequals()方法。还有一种非常普遍的内存泄露场景就是对要放入HashSet中的对象,缺少hashCode或者equals方法
    具体的,当我们将重复的对象放入集合中的时候–这将导致集合增长,而不是忽略重复对象。这也将导致内存泄露。