文章内容
一、什么是内存泄漏
java的优势之一就是内置了垃圾回收器GC,它实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?

也就是一部分内存空间明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。
正规点的理解:
动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。
二、内存泄漏的危害
- 长时间运行,程序变卡,性能严重下降
- 程序莫名其妙挂掉
- OutOfMemoryError错误
- 乱七八糟的错误,还不易排查
三、内存泄漏的原因
内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。
1、大量使用静态变量
静态变量的生命周期与程序一致。因此常驻内存。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | public class StaticTest { public static List<Integer> list = new ArrayList<>(); public void populateList() { for ( int i = 0 ; i < 10000000 ; i++) { list.add(( int )Math.random()); } System.out.println( "running......" ); } public static void main(String[] args) { System.out.println( "before......" ); new StaticTest().populateList(); System.out.println( "after......" ); } } |
使用jvisualvm运行一遍,查看内存:
- 带static关键字(使用静态变量)

从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。
- 不使用static关键字(不使用静态变量)

由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。
总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏
2、连接资源未关闭
每当建立一个连接,jvm就会为这些资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。
1 2 3 4 5 6 7 8 | public class FileTest { public static void main(String[] args) throws IOException { File f= new File( "G:nginx配套资料笔记资料.zip" ); System.out.println(f.exists()); System.out.println(f.isDirectory()); } } |
使用jvisualvm运行一遍,查看内存:

可以看出,在连接文件资源时,jvm会为本资源分配内存。
3、equals()和hashCode()方法使用不当
定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | public class User{ public String name; public int age; public User(String name, int age) { this .name = name; this .age = age; } } public class EqualTest { public static void main(String[] args) { Map<User, Integer> map = new HashMap<>(); for ( int i= 0 ; i< 100 ; i++) { map.put( new User( "" , 1 ), 1 ); } System.out.println(map.size() == 1 ); //输出为false } } |
使用jvisualvm运行一遍,查看内存:

内存从150M一下子飙升到225M,可见飙升得厉害。输出为false,说明user对象被重复添加了。像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。
重写这俩方法再运行一遍
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class User{ public static String name; public User(String name) { this .name = name; } @Override public boolean equals(Object o) { if (o == this ) return true ; if (!(o instanceof User)) { return false ; } User user = (User) o; return User.name.equals(name); } @Override public int hashCode() { return name.hashCode(); } } |
在EqualTest内再测试一遍,查看内存变化:

上图可以看到上升幅度没那么大。而且输出为true,由于重写了hashcode和equal,所以HashMap添加的肯定是同一个对象。
4、内部类持有外部类
这个场景和上面类似。
5、finalize方法

这就是整个过程。不过在这里主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。
6、ThreadLocal的错误使用
ThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。

上面这张图详细地揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。
- Thread中有一个map,就是ThreadLocalMap
- ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
- ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
- 重点来了,突然ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
四、内存泄漏的检测
检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:
1、工具分析
这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。它可以帮助分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。
2、垃圾回收分析
这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。
如果不用工具的话,可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。

3、基准测试
也就是使用科学的方式进行分析java代码的性能。进而判断分析。
结论
内存泄漏是个很严重的问题,也比较常见。最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。因此良好的代码规范,可以有效地避免这些错误。