記一次生產(chǎn)頻繁發(fā)生FullGC問題
問題發(fā)現(xiàn)
早上過來,飯都沒來的及吃,運維就給我發(fā)來信息,說是某個接口調(diào)用大量超時。因為最近這個接口調(diào)用量是翻倍了,所以我就去檢查了下慢SQL,發(fā)現(xiàn)確實是有較多的慢SQL,所以我就縮減了查詢的時間范圍,但是效果并不好。
過了一會發(fā)現(xiàn),這個服務fullGC是有問題的,太頻繁了,這個應該是導致接口超時的根本問題,因為時間也是對的上的。
這個是最近三個小時fullGC的監(jiān)控圖:

這個是最近三天fullGC的監(jiān)控圖:

對比一下,就不難發(fā)現(xiàn),fullGC數(shù)量是從3月15號晚上9點開始增加的,也是這個接口對外開放的時間。
?
解決思路
1、首先去服務器上面下載dump文件,分析是哪里造成了內(nèi)存泄漏,頻繁觸發(fā)fullGC。首先找出服務器內(nèi)java文件的PID,然后保存dump文件,我們公司java服務是固定端口號:1
使用top命令:

然后執(zhí)行命令:jmap -dump:file=202303160924.dump,format=b 1 ,保存dump文件

2、根據(jù)dump文件,分析出堆內(nèi)對象的分布情況
下載一個可以分析dump文件的工具,這里我下載是Jprofiler
查看大對象的分析,發(fā)現(xiàn)是java.lang.ApplicationShutdownHooks的hooks占用太大內(nèi)存,并且得知改熟悉是一個Map

分析這個Map里面的元素引用關系,可以看到這個map里面存的都是線程對象,并且大部分都是一個名為java.util.concurrent.ScheduledThreadPoolExecutor@59648a61的線程池對象,到了這里就定位到問題代碼了,是這次新加的接口里面有一個異步操作,用的guava并發(fā)包里面的一個超時等待功能的接口,具體思路就是啟用一個定時任務線程池去定時去檢查在規(guī)定時間內(nèi),是否返回結(jié)果。
??

3、看看我的業(yè)務代碼是哪里出現(xiàn)了問題
//異步執(zhí)行某個查詢方法,并且在規(guī)定時間內(nèi)返回查詢結(jié)果public <T> T asyncWithTimeout(ScheduledThreadPoolExecutor executor, Callable<T> callable, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?long time, TimeUnit unit) { ? ?try { ? ? ? ?ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(threadPoolExecutor);
? ? ? ?ListenableFuture<T> future = listeningExecutorService.submit(callable); ? ? ? ?//這里是創(chuàng)建一個定時任務線程,去定時檢查是否在規(guī)定時間內(nèi)查詢完畢,應該就是這個去添加了鉤子函數(shù),進去看看
? ? ? ?ScheduledExecutorService scheduledExecutorService = MoreExecutors.getExitingScheduledExecutorService(executor); ? ? ? ?return Futures.withTimeout(future, time, unit, scheduledExecutorService).get(time, unit);
? ?} catch (InterruptedException | ExecutionException | TimeoutException e) {
? ? ? ?log.warn("異步方法執(zhí)行失敗,error:{}", e.getMessage());
? ?} ? ?return null;
}//=======================guava并發(fā)包代碼======================= // TODOpublic static ScheduledExecutorService getExitingScheduledExecutorService(
? ?ScheduledThreadPoolExecutor executor) { ?//每次都去創(chuàng)建一個新的對象
?return new Application().getExitingScheduledExecutorService(executor);
}final ScheduledExecutorService getExitingScheduledExecutorService(
? ?ScheduledThreadPoolExecutor executor) { ?return getExitingScheduledExecutorService(executor, 120, TimeUnit.SECONDS);
}final ScheduledExecutorService getExitingScheduledExecutorService(
? ?ScheduledThreadPoolExecutor executor, long terminationTimeout, TimeUnit timeUnit) {
?useDaemonThreadFactory(executor); ?ScheduledExecutorService service = Executors.unconfigurableScheduledExecutorService(executor); ?//添加構(gòu)造函數(shù)的地方,進去看看
?addDelayedShutdownHook(executor, terminationTimeout, timeUnit); ?return service;
}final void addDelayedShutdownHook( ? ?final ExecutorService service, final long terminationTimeout, final TimeUnit timeUnit) {
?checkNotNull(service);
?checkNotNull(timeUnit); ?//繼續(xù)點進去
?addShutdownHook(
? ? ?MoreExecutors.newThread( ? ? ? ? ?//線程名字對上了,就在對象引用的截圖里面出現(xiàn)過
? ? ? ? ?"DelayedShutdownHook-for-" + service, ? ? ? ? ?new Runnable() { ? ? ? ? ? ?
? ? ? ? ? ?public void run() { ? ? ? ? ? ? ?try { ? ? ? ? ? ? ? ?// We'd like to log progress and failures that may arise in the
? ? ? ? ? ? ? ?// following code, but unfortunately the behavior of logging
? ? ? ? ? ? ? ?// is undefined in shutdown hooks.
? ? ? ? ? ? ? ?// This is because the logging code installs a shutdown hook of its
? ? ? ? ? ? ? ?// own. See Cleaner class inside {@link LogManager}.
? ? ? ? ? ? ? ?service.shutdown();
? ? ? ? ? ? ? ?service.awaitTermination(terminationTimeout, timeUnit);
? ? ? ? ? ? ?} catch (InterruptedException ignored) { ? ? ? ? ? ? ? ?// We're shutting down anyway, so just ignore.
? ? ? ? ? ? ?}
? ? ? ? ? ?}
? ? ? ? ?}));
}void addShutdownHook(Thread hook) {
?Runtime.getRuntime().addShutdownHook(hook);
}//=======================guava并發(fā)包代碼=======================public void addShutdownHook(Thread hook) { ? ?SecurityManager sm = System.getSecurityManager(); ? ?if (sm != null) {
? ? ? ?sm.checkPermission(new RuntimePermission("shutdownHooks"));
? ?} ? ?//定位到問題了,就是這里添加的鉤子函數(shù)
? ?ApplicationShutdownHooks.add(hook);
}static synchronized void add(Thread hook) { ? ?if(hooks == null) ? ? ? ?throw new IllegalStateException("Shutdown in progress"); ? ?if (hook.isAlive()) ? ? ? ?throw new IllegalArgumentException("Hook already running"); ? ?if (hooks.containsKey(hook)) ? ? ? ?throw new IllegalArgumentException("Hook previously registered"); ? ?//存在到 hooks 這個map對象里面,就是這個大對象
? ?hooks.put(hook, hook);
}
問題解決
經(jīng)過上面問題的排查,造成hooks大對象的原因找到了,就是每次調(diào)用接口的時候,都會往hooks里面put一個對象。
所以,解決辦法很簡單,就是不用每次都去生成一個ScheduledExecutorService對象,類初始化的時候創(chuàng)建一次就行了
改造后的代碼如下:
private ListeningExecutorService listeningExecutorService;private ScheduledExecutorService scheduledExecutorService;public static AsyncUtils getInstance() { ? ?return ThreadHolder.INSTANCE.getAsyncWithCallback();
}private AsyncUtils() {
? ?listeningExecutorService = MoreExecutors.listeningDecorator(ThreadPoolConstant.THREAD_POOL_EXECUTOR);
? ?scheduledExecutorService = MoreExecutors.getExitingScheduledExecutorService(ThreadPoolConstant.SCHEDULED_THREAD_POOL_EXECUTOR);
}public <T> T asyncWithTimeout(Callable<T> callable, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?long time, TimeUnit unit) { ? ?try {
? ? ? ?ListenableFuture<T> future = listeningExecutorService.submit(callable); ? ? ? ?return Futures.withTimeout(future, time, unit, scheduledExecutorService).get(time, unit);
? ?} catch (InterruptedException | ExecutionException | TimeoutException e) {
? ? ? ?log.warn("異步方法執(zhí)行失敗,error:{}", e.getMessage());
? ?} ? ?return null;
}private enum ThreadHolder { ? ?/**
? ? * 線程持有類 INSTANCE
? ? */
? ?INSTANCE; ? ?private final AsyncUtils asyncUtils;
? ?ThreadHolder() {
? ? ? ?asyncUtils = new AsyncUtils();
? ?} ? ?public AsyncUtils getAsyncWithCallback() { ? ? ? ?return asyncUtils;
? ?}
}