【Java面试高频-JVM】- Java内存泄露是怎么样的呢?
首先先来理解一下内存泄露和内存溢出。
内存溢出:申请内存时,没有足够的内存可以使用了;
内存泄露:申请了内存用完了不释放,比如一共有1024M的内存,分配了521M的内存一直不回收。
例子来通俗易懂的了解Java中内存泄露
如上图:
对象 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
命令来查看服务器中内存使用情况是怎么样的;
-
top
命令查看实时的进程数;top -Hp pid
查看该进程下所有线程数量的内存使用情况
问题分析:
要分析是否存在内存泄露的问题,需要分析该Java进程的堆内存的使用情况,GC情况,需要分析该Java进程的堆内存的老年代空间占比大对象的成因,继而避免这些对象无法被回收,从而解决内存溢出问题。
第一步:查看堆情况
使用下述命令来查看进程堆内存使用情况
jhsdb jmap --pid your-java-serverce-pi --heap
这个命令只能反映当前堆内存情况,用来查看堆大小、代分配是否合理还行,更多的信息则无法很直观的看出来。
第二步:查看GC情况
使用命令查看进程 GC 情况:
jstat -gcutil your-java-service-pid 1000 100
这个命令可以查看进程启动以来 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 的信息
可以观察到,经过多次 Full GC,箭头右边的反映 GC 后老年代堆内存大小的数值在增加。这说明有老年对象一直没有被释放,某个对象对这些对象的引用一直维持着。这种趋势下去,OutOfMemory 错误的出现不可避免,完全实锤了该服务的内存泄漏问题。
第四步:查看堆实例对象分别
查看进程堆中当前实例数前 20 类排名:
jmap -histo:live your-java-service-pid | head -n 20
查看进程堆中当前实例数前 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方法
具体的,当我们将重复的对象放入集合中的时候–这将导致集合增长,而不是忽略重复对象。这也将导致内存泄露。