调优实战

上面说完了调优的目的和调优的指标,那么我们就来实战调优,首先准备我的案例代码,如下:

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){
			 = 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的打印日志,然后通过Visual VM来看GC的显示如下:

image.png

可以看到一段时间后出现4次Minor GC,使用的时间是29.648ms,发生一次Full GC使用的时间是41.944ms。

Minor GC非常频繁,Full GC也是,在短时间内就发生了几次,观察输出的日志发现以及Visual VM的显示来看,都是因为内存没有设置,太小,导致Minor GC频繁。

因此,我们第二次适当的增大Java堆的大小,调优设置的参数为:

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

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

image.png

可以发现Minor GC次数明显下降,但是还是发生了Full GC,根据打印的日志来看,是因为元空间的内存不足,看了上面的Visual VM元空间的内存图,也是一样,基本都到顶了:

image.png

因此第三次对于元空间的区域设置大一些,并且将GC回收器换成是CMS的,设置的参数如下:

java -Xmx2048m -Xms2048m -Xmn1024m -Xss256k -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m -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

观察相同的时间后,Visual VM的显示图如下: image.png 同样的时间,一次Minor GC和Full GC都没有发生,所以这样我觉得也算是已经调优了。

但是调优并不是一味的调大内存,是要在各个区域之间取得平衡,可以适当的调大内存,以及更换GC种类,举个例子,当把上面的案例代码的Thread.sleep(1000)给去掉。

然后再来看Visual VM的图,如下:

image.png

可以看到Minor GC也是非常频繁的,因为这段代码本身就是不断的增大内存,直到OOM异常,真正的实际并不会这样,可能当内存增大到一定两级后,就会在一段范围平衡。

当我们将上面的情况,再适当的增大内存,JVM参数如下:

java -Xmx4048m -Xms4048m -Xmn2024m -XX:SurvivorRatio=7  -Xss256k -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=100m -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

可以看到相同时间内,确实Minor GC减少了,但是时间增大了,因为复制算法,基本都是存活的,复制需要耗费大量的性能和时间:

image.png

所以,调优要有取舍,取得一个平衡点,性能、状态达到佳就OK了,并没最佳的状态,这就是调优的基本法则,而且调优也是一个细活,所谓慢工出细活,需要耗费大量的时间,慢慢调,不断的做对比。

调优参数

  • -Xms1024m 设置堆的初始大小
  • -Xmx1024m 设置堆的最大大小
  • -XX:NewSize=1024m 设置年轻代的初始大小
  • -XX:MaxNewSize=1024m 设置年轻代的最大值
  • -XX:SurvivorRatio=8 Eden和S区的比例
  • -XX:NewRatio=4 设置老年代和新生代的比例
  • -XX:MaxTenuringThreshold=10 设置晋升老年代的年龄条件

  • -Xss128k

元空间

  • -XX:MetasapceSize=200m 设置初始元空间大小
  • -XX:MaxMatespaceSize=200m 设置最大元空间大小 默认无限制

直接内存

  • -XX:MaxDirectMemorySize 设置直接内存的容量,默认与堆最大值一样

日志

  • -Xloggc:/opt/app/ard-user/ard-user-gc-%t.log 设置日志目录和日志名称
  • -XX:+UseGCLogFileRotation 开启滚动生成日志
  • -XX:NumberOfGCLogFiles=5 滚动GC日志文件数,默认0,不滚动
  • -XX:GCLogFileSize=20M GC文件滚动大小,需开 UseGCLogFileRotation
  • -XX:+PrintGCDetails 开启记录GC日志详细信息(包括GC类型、各个操作使用的时间),并且在程序运行结束打印出JVM的内存占用情况
  • -XX:+ PrintGCDateStamps 记录系统的GC时间
  • -XX:+PrintGCCause 产生GC的原因(默认开启)

GC

Serial垃圾收集器(新生代)

开启

  • -XX:+UseSerialGC

关闭:

  • -XX:-UseSerialGC //新生代使用Serial 老年代则使用SerialOld

Parallel Scavenge收集器(新生代)开启

  • -XX:+UseParallelOldGC 关闭
  • -XX:-UseParallelOldGC 新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器

ParallelOl垃圾收集器(老年代)开启

  • -XX:+UseParallelGC 关闭
  • -XX:-UseParallelGC 新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器

ParNew垃圾收集器(新生代)开启

  • -XX:+UseParNewGC 关闭
  • -XX:-UseParNewGC //新生代使用功能ParNew 老年代则使用功能CMS

CMS垃圾收集器(老年代)开启

  • -XX:+UseConcMarkSweepGC 关闭
  • -XX:-UseConcMarkSweepGC
  • -XX:MaxGCPauseMillis GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间,比如减小年轻代
  • -XX:+UseCMSCompactAtFullCollection 用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的
  • -XX:CMSFullGCsBefore-Compaction 多少次FullGC之后压缩一次,默认值为0,表示每次进入FullGC时都进行碎片整理)
  • -XX:CMSInitiatingOccupancyFraction 当老年代使用达到该比例时会触发FullGC,默认是92
  • -XX:+UseCMSInitiatingOccupancyOnly 这个参数搭配上面那个用,表示是不是要一直使用上面的比例触发FullGC,如果设置则只会在第一次FullGC的时候使用-XX:CMSInitiatingOccupancyFraction的值,之后会进行自动调整
  • -XX:+CMSScavengeBeforeRemark 在FullGC前启动一次MinorGC,目的在于减少老年代对年轻代的引用,降低CMSGC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段
  • -XX:+CMSParallellnitialMarkEnabled 默认情况下初始标记是单线程的,这个参数可以让他多线程执行,可以减少STW
  • -XX:+CMSParallelRemarkEnabled 使用多线程进行重新标记,目的也是为了减少STW

G1垃圾收集器开启

  • -XX:+UseG1GC 关闭
  • -XX:-UseG1GC
  • -XX:G1HeapRegionSize 设置每个Region的大小,取值范围为1MB~32MB
  • -XX:MaxGCPauseMillis 设置垃圾收集器的停顿时间,默认值是200毫秒,通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的