1. 理论篇
系统真正最大的问题,就是因为内存分配、参数设置不合理,导致对象频繁的进入老年代,然后频繁触发老年代gc,导致系统频繁的每隔几分钟就要卡死几秒钟。
Young GC指年轻代GC,Old GC指老年代GC,Full GC指年轻代、老年代、永久代共同GC。
针对大内存机器通常建议采用G1垃圾回收器。
在Young GC以后,有部分对象会留在Survivor中,有部分对象会进入老年代。
尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。
2. 工具篇
jstat -gc PID
jstat -gccapacity PID 堆内存分析
jstat -gcnew PID 年轻代GC分析,TT跟MTT是年轻代存活时间跟最大存活时间
jstat -gcnewcapacity PID 年轻代内存分析
jstat -gcold PID 老年代GC分析
jstat -gcoldcapacity PID 老年代内存分析
jstat -gcmetacapacity PID 元数据区内存分析
jstat -gc PID 1000 10
每1000ms打印一次,一共打印10条。
jmap -heap PID 这个命令跟jstat -gc PID类型
jmap -histo PID
按内存大小打印出占用内存的对象。
jmap -dump:live,format=b,file=dump.hprof PID
打印内存快照。
jhat dump.hprof -port 7000
浏览器通过7000端口,查看内存快照。
3. 压测
经过压测,观察新生代对象的增长速率、Young GC的触发频率、Young GC的耗时、每次Young GC存活下的对象,每次Young GC多少对象进入老年代、老年代增长速率、Full GC的触发频率、Full GC的耗时。
对线上运行的系统,要不然用命令工具手动监控,发现问题就优化,要不然就是依托公司的监控系统进行自动监控,可视化查看日常系统的运行状态。
4. 实战
public static void main(String[] args) throws Exception {
Thread.sleep(30000);
while (true) {
loadData();
}
}
private static void loadData() throws Exception {
byte[] data = null;
for (int i = 0; i < 4; i++) {
data = new byte[10 * 1024 * 1024];
}
data = null;
byte[] data1 = new byte[10 * 1024 * 1024];
byte[] data2 = new byte[10 * 1024 * 1024];
byte[] data3 = new byte[10 * 1024 * 1024];
data3 = new byte[10 * 1024 * 1024];
Thread.sleep(1000);
}
启动参数如下:
-XX:NewSize=100m -XX:MaxNewSize=100m -XX:InitialHeapSize=200m -XX:MaxHeapSize=200m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=20m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:gc.log
当前Survivor区只有100*0.2/2=10M
每次Full GC后大概有20-30M的数据进入老年代。调整如下:堆大小变为300M,200M给新生代,比例调整为2:1:1,则Survivor有50M,每次Young GC的数据就可以存入Survivor,即减少Full GC的频率。具体参数如下:
-XX:NewSize=200m -XX:MaxNewSize=200m -XX:InitialHeapSize=300m -XX:MaxHeapSize=300m -XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=20m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:gc.log
可以看到这就是一次通过观察线上的JVM的内存变化,进行合理的参数调优的实践。
5. CMS参数调优
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
即每次Full GC后都进行碎片整理,默认配置。
-XX:-CMSParallelInitialMarkEnabled
CMS"初始标记"启用多线程。
-XX:+CMSScavengeBeforeRemark
重新标记之前,先执行下 Young GC,这样可以回收掉一些没用的对象,降低标记数量。
6. 生产频繁Full GC案例分析
案例1:每次Young GC也很少的对象进入老年代,但是会周期性的出现大对象, 直接进入老年代,触发Full GC,该案例是定时任务从数据库加载大量数据进JVM导致的。
案例2:每秒都会Full GC,但是jstat查看年轻的、老年代对象都很少,代码中显示调用了System.gc(),在大流量下频繁调用System.gc,导致的频繁Full GC,建议不需要进行显示释放。 启动的时候加 -XX:DisableExplicitGC,禁止显示执行GC。
案例3:CPU负载高。一般两种情况:业务处理开启大量线程,每个线程负荷都高;第二种情况是频繁的Full GC,Full GC也是很消耗CPU资源的,这个案例是同MAT分析,发现老年代存在很多无法回收的对象(内存泄露)了,导致稍微有年轻代进入老年代就触发Full GC。
7. 一个新开发的系统如何设置JVM参数
通过压测得出一些数据:
Eden区的对象增长速率多块?
Young GC频率多高?
一次Young GC多长耗时?
Young GC过后多少对象存活?
老年代的对象增长速率多高?
Full GC频率多高?
一次Full GC耗时多少?
有了这些数据,就可以针对性的进行JVM参数调优了。
频繁Full GC的几种常见原因
每次Young GC之后存活的对象太多,内存分配不合理,Survivor区域过小,导致对象频繁进入老年代,频发触发Full GC
系统一次性加载过多数据进入内存,搞出来很多大对象,导致频繁有大对象进入老年代
发生了内存泄露,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里
Metaspace因为加载过多类触发Full GC
误调用System.gc()触发Full GC