jvm调优到底需要做什么

354 阅读5分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

上篇文章介绍了jvm基础知识,这篇文章带你详细解决jvm出现的问题及优化方案。

内存泄漏及解决方法

 1.系统崩溃前的一些现象:

  • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
  • FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
  • 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

 之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。

2.生成堆的dump文件  通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

 3.分析dump文件

 下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。当然我们可以借助X-Window把Linux上的图形导入到Window。我们考虑用下面几种工具打开该文件:

  1. Visual VM
  2. IBM HeapAnalyzer
  3. JDK 自带的Hprof工具

 使用这些工具时为了确保加载速度,建议设置最大内存为6G。使用后发现,这些工具都无法直观地观察到内存泄漏,Visual VM虽能观察到对象大小,但看不到调用堆栈;HeapAnalyzer虽然能看到调用堆栈,却无法正确打开一个3G的文件。因此,我们又选用了Eclipse专门的静态内存分析工具:Mat。

 4.分析内存泄漏

 通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。针对本案,在ThreadLocal中有很多的JbpmContext实例,经过调查是JBPM的Context没有关闭所致。

 另,通过Mat或JMX我们还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。

5.回归问题

   Q:为什么崩溃前垃圾回收的时间越来越长?

   A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据

   Q:为什么Full GC的次数越来越多?

   A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

   Q:为什么年老代占用的内存越来越大?

   A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代

调优目的

再说GC堆的调优之前,我们先来聊一聊调优的目的,可能人云亦云,每个人的看法都不一样,我就单纯的说说我对调优的看法。

我认为调优的目的主要有两个,而且调优还必须要有前提,前提是你的系统必须要调优了,什么意思呢?比如你的服务运行速度慢,响应慢,吞吐量小,甚至出现OOM异常了,那么这就需要调优了。

假如,你的服务运行状态佳,响应快,吞吐量高,那就没必要了,此时你再去调优,就有可能适得其反。

然后,调优的目的就是第一个是是你的服务运行状态佳,响应速度快(响应时间能够在接受范围),或者说吞吐量高,要达到这种目的就是使GC时间(STW)合理,次数合理,Minor GC次数合理(几小时一次),Full GC合理(一天一次或者几天一次)。

第二个目的就是为了解决问题,比如出现了OOM,那么调优的目的就是为了防止再次出现OOM异常。

import java.util.ArrayList;
import java.util.List;

class OOM {

 static class User{
  private String name;
  private int age;

  public User(String name, int age){
   this.name = name;
   this.age = age;
  }

 }

 public static void main(String[] args) throws InterruptedException {
  List<User> list = new ArrayList<>();
  for (int i = 0; i < Integer.MAX_VALUE; i++) {
       Tread.sleep(1000);
   System.err.println(Thread.currentThread().getName());
   User user = new User("zhangsan"+i,i);
   list.add(user);
  }
 }
}

案例代码很简单,就是不断的往一个集合里里面添加对象,首先初次我们启动的命令为:

java   -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

就是纯粹的设置了一些GC的打印日志,我们可以看到不断的设置 调优参数为:

java -Xmx2048m -Xms2048m -Xmn1024m -Xss256k  -XX:+UseConcMarkSweepGC  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

观察一段时间后,结果如下图所示:

图片.png 可以看到相同时间内,确实Minor GC减少了,但是时间增大了,因为复制算法,基本都是存活的,复制需要耗费大量的性能和时间。所以,调优要有取舍,取得一个平衡点,性能、状态达到佳就OK了,并没最佳的状态,这就是调优的基本法则,而且调优也是一个细活,所谓慢工出细活,需要耗费大量的时间,慢慢调,不断的做对比。