背景

在最近的一次线上部署项目中,发现有一个部署的tomcat内存一直在增长,第一感觉就是内存泄漏了,然后各种查看虚拟机的运行状态,终于将问题解决了,下面记录一下我解决的方法和步骤

java及tomcat版本

java虚拟机缓存移除 java虚拟机占用内存过高_java

解决步骤

  1. 使用jstat命令查看虚拟机内存的运行状况

java虚拟机缓存移除 java虚拟机占用内存过高_tomcat_02


从图上我们发现,eden区一直在稳定的增长,增长到100M的时候就会发生YGC,S0,S0中的对象就行复制拷贝,YGC之后M中的值也在变化,说明有对象保存在常量池中。

  1. 使用jmap查看哪个对象占用空间最多
jmap -histo [进程ID] > class.txt

java虚拟机缓存移除 java虚拟机占用内存过高_jvm_03


发现是char类型 和 byte类型占用内存最大

  1. 接着使用jmap将内存日志拷贝出来
jmap -dump:format=b,file=dump.bin [进程ID]

拷贝出来,使用MemoryAnalyzer工具进行分析

java虚拟机缓存移除 java虚拟机占用内存过高_tomcat_04


发现Finalizer对象占用快接近一半的内存了,这个Finalizer是什么对象,我们在开发中根本就没有创建过这个类型的对象啊

java finalizer机制
java为了确保对象在被GC回收之前做一些收尾的工作,比如Socket关闭等操作,引入了Finalizer机制,即,在GC将对象回收之前需要回调Object的finalize()方法,回调完成之后才允许GC将该对象回收。

这样就导致了一个问题,如果该对象的finalize方法一直得不到调用,这个对象就一直不会回收掉,内存占用就会变的越来越大

下面是Finalizable对象的生命周期

  1. JVM创建Finalizable对象
  2. JVM创建java.lang.ref.Finalizer对象,指向刚创建的Finalizable对象
  3. java.lang.ref.Finalizer类会持有下一个java.lang.ref.Finalizer实例,这样会使得下一次GC无法将该Finalizable对象进行回收
  4. 新生代中的Finalizable对象无法被回收,只能转移到S0和S1或者老年代
  5. 当该Finalizable对象生命周期结束之后,垃圾回收器GC会将该对象放入java.lang.ref.Finalizer.ReferenceQueue队列中
  6. 会有一个优先级比较低的Finalizer线程不停的扫描ReferenceQueue队列,将里面的对象逐个弹出,并调用他们的finalize()方法
  7. 调用完finalize()方法之后,Finalizer线程会将该Finalizer类从链表中去掉,这样下一轮的GC才会将该对象进行回收
  8. 但是,Finalizer守护线程(FinalizerDaemon)会与我们的主线程进行竞争,由于这个线程的优先级比较低,所有它获取的CPU的时间较少,因此它永远也赶不上主线程创建Finalizable对象的步伐
  9. 程序会消耗掉所有的可用资源,最后会抛出OOM异常

解决方法:将Finalizer对象指向的对象中的finalize()方法去掉,我们手动进行资源的扫尾操作

将finalize()方法去掉之后,内存不再泄漏了

java虚拟机缓存移除 java虚拟机占用内存过高_java_05


使用jstack查看Finalizer守护线程

jstack [进程ID]

java虚拟机缓存移除 java虚拟机占用内存过高_jvm_06