From ccb2311a0915dbc284e54ea069138d9d3fbb081a Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Wed, 6 Jul 2016 15:05:13 +0300 Subject: [PATCH] data race on adding/consuming ourInstances; deregister and stop LowMemoryWatcher before AppThreadPool shutdown to avoid rejected exceptions --- .../openapi/util/LowMemoryWatcher.java | 104 ++++-------------- .../openapi/util/LowMemoryWatcherManager.java | 100 +++++++++++++++++ .../AppScheduledExecutorService.java | 5 + 3 files changed, 126 insertions(+), 83 deletions(-) create mode 100644 platform/util/src/com/intellij/openapi/util/LowMemoryWatcherManager.java diff --git a/platform/util/src/com/intellij/openapi/util/LowMemoryWatcher.java b/platform/util/src/com/intellij/openapi/util/LowMemoryWatcher.java index 60f10dc3ae80..4edf039b7545 100644 --- a/platform/util/src/com/intellij/openapi/util/LowMemoryWatcher.java +++ b/platform/util/src/com/intellij/openapi/util/LowMemoryWatcher.java @@ -17,85 +17,31 @@ package com.intellij.openapi.util; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.util.containers.WeakList; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import javax.management.ListenerNotFoundException; -import javax.management.Notification; -import javax.management.NotificationEmitter; -import javax.management.NotificationListener; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryNotificationInfo; -import java.lang.management.MemoryPoolMXBean; -import java.lang.management.MemoryType; import java.util.List; -import java.util.concurrent.Future; /** * @author Eugene Zhuravlev * Date: Aug 24, 2010 */ public class LowMemoryWatcher { - private static final long MEM_THRESHOLD = 5 /*MB*/ * 1024 * 1024; - private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.LowMemoryWatcher"); - private static final List ourInstances = new WeakList(); - private static Future ourSubmitted; - private static final Runnable ourJanitor = new Runnable() { - @Override - public void run() { - LOG.info("Low memory signal received."); - try { - for (LowMemoryWatcher watcher : ourInstances) { - try { - watcher.myRunnable.run(); - } - catch (Throwable e) { - LOG.info(e); - } - } - } - finally { - synchronized (ourJanitor) { - ourSubmitted = null; - } - } - } - }; - private static final NotificationListener ourLowMemoryListener = new NotificationListener() { - @Override - public void handleNotification(Notification n, Object hb) { - if (MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(n.getType()) || - MemoryNotificationInfo.MEMORY_COLLECTION_THRESHOLD_EXCEEDED.equals(n.getType())) { - synchronized (ourJanitor) { - if (ourSubmitted == null) { - ourSubmitted = AppExecutorUtil.getAppExecutorService().submit(ourJanitor); - } - } - } - } - }; - + private static final List ourListeners = new WeakList(); private final Runnable myRunnable; - static { - try { - for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) { - if (bean.getType() == MemoryType.HEAP && bean.isUsageThresholdSupported()) { - long threshold = bean.getUsage().getMax() - MEM_THRESHOLD; - if (threshold > 0) { - bean.setUsageThreshold(threshold); - bean.setCollectionUsageThreshold(threshold); - } - } + static void onLowMemorySignalReceived() { + LOG.info("Low memory signal received."); + for (Runnable watcher : ourListeners) { + try { + watcher.run(); + } + catch (Throwable e) { + LOG.info(e); } - ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(ourLowMemoryListener, null, null); - } - catch (Throwable e) { - // should not happen normally - LOG.info("Errors initializing LowMemoryWatcher: ", e); } } @@ -103,7 +49,12 @@ public class LowMemoryWatcher { * Registers a runnable to run on low memory events * @return a LowMemoryWatcher instance holding the runnable. This instance should be kept in memory while the * low memory notification functionality is needed. As soon as it's garbage-collected, the runnable won't receive any further notifications. + * @param runnable the action which executes on low-memory condition. Can be executed: + * - in arbitrary thread + * - in unpredictable time + * - multiple copies in parallel so please make it reentrant. */ + @Contract(pure = true) public static LowMemoryWatcher register(@NotNull Runnable runnable) { return new LowMemoryWatcher(runnable); } @@ -112,23 +63,22 @@ public class LowMemoryWatcher { * Registers a runnable to run on low memory events. The notifications will be issued until parentDisposable is disposed. */ public static void register(@NotNull Runnable runnable, @NotNull Disposable parentDisposable) { - final Ref watcher = Ref.create(new LowMemoryWatcher(runnable)); + final LowMemoryWatcher watcher = new LowMemoryWatcher(runnable); Disposer.register(parentDisposable, new Disposable() { @Override public void dispose() { - watcher.get().stop(); - watcher.set(null); + watcher.stop(); } }); } private LowMemoryWatcher(@NotNull Runnable runnable) { myRunnable = runnable; - ourInstances.add(this); + ourListeners.add(runnable); } public void stop() { - ourInstances.remove(this); + ourListeners.remove(myRunnable); } /** @@ -136,19 +86,7 @@ public class LowMemoryWatcher { * In server environments, this thread may run indefinitely and prevent the class loader from * being gc-ed. Thus it's necessary to invoke this method to stop that thread and let the classes be garbage-collected. */ - public static void stopAll() { - synchronized (ourJanitor) { - if (ourSubmitted != null) { - ourSubmitted.cancel(false); - ourSubmitted = null; - } - } - ourInstances.clear(); - try { - ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).removeNotificationListener(ourLowMemoryListener); - } - catch (ListenerNotFoundException e) { - LOG.error(e); - } + static void stopAll() { + ourListeners.clear(); } } diff --git a/platform/util/src/com/intellij/openapi/util/LowMemoryWatcherManager.java b/platform/util/src/com/intellij/openapi/util/LowMemoryWatcherManager.java new file mode 100644 index 000000000000..ca5375cc569b --- /dev/null +++ b/platform/util/src/com/intellij/openapi/util/LowMemoryWatcherManager.java @@ -0,0 +1,100 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.util; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.concurrency.AppExecutorUtil; + +import javax.management.ListenerNotFoundException; +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryNotificationInfo; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.util.concurrent.Future; + +public class LowMemoryWatcherManager implements Disposable { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.LowMemoryWatcherManager"); + + private static final long MEM_THRESHOLD = 5 /*MB*/ * 1024 * 1024; + + private Future mySubmitted; // guarded by ourJanitor + private final Runnable myJanitor = new Runnable() { + @Override + public void run() { + // null mySubmitted before all listeners called to avoid data race when listener added in the middle of the execution and is lost + // this may however cause listeners to execute more than once (potentially even in parallel) + synchronized (myJanitor) { + mySubmitted = null; + } + LowMemoryWatcher.onLowMemorySignalReceived(); + } + }; + + public LowMemoryWatcherManager() { + try { + for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) { + if (bean.getType() == MemoryType.HEAP && bean.isUsageThresholdSupported()) { + long threshold = bean.getUsage().getMax() - MEM_THRESHOLD; + if (threshold > 0) { + bean.setUsageThreshold(threshold); + bean.setCollectionUsageThreshold(threshold); + } + } + } + ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(myLowMemoryListener, null, null); + } + catch (Throwable e) { + // should not happen normally + LOG.info("Errors initializing LowMemoryWatcher: ", e); + } + } + + private final NotificationListener myLowMemoryListener = new NotificationListener() { + @Override + public void handleNotification(Notification notification, Object __) { + if (MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(notification.getType()) || + MemoryNotificationInfo.MEMORY_COLLECTION_THRESHOLD_EXCEEDED.equals(notification.getType())) { + synchronized (myJanitor) { + if (mySubmitted == null) { + mySubmitted = AppExecutorUtil.getAppExecutorService().submit(myJanitor); + } + } + } + } + }; + + @Override + public void dispose() { + try { + ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).removeNotificationListener(myLowMemoryListener); + } + catch (ListenerNotFoundException e) { + LOG.error(e); + } + synchronized (myJanitor) { + if (mySubmitted != null) { + mySubmitted.cancel(false); + mySubmitted = null; + } + } + + LowMemoryWatcher.stopAll(); + } +} diff --git a/platform/util/src/com/intellij/util/concurrency/AppScheduledExecutorService.java b/platform/util/src/com/intellij/util/concurrency/AppScheduledExecutorService.java index be08014f1e38..fed13f11e2f6 100644 --- a/platform/util/src/com/intellij/util/concurrency/AppScheduledExecutorService.java +++ b/platform/util/src/com/intellij/util/concurrency/AppScheduledExecutorService.java @@ -16,6 +16,8 @@ package com.intellij.util.concurrency; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.LowMemoryWatcherManager; import com.intellij.util.Consumer; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; @@ -35,6 +37,7 @@ public class AppScheduledExecutorService extends SchedulingWrapper { private static final Logger LOG = Logger.getInstance("#org.jetbrains.ide.PooledThreadExecutor"); static final String POOLED_THREAD_PREFIX = "ApplicationImpl pooled thread "; @NotNull private final String myName; + private final LowMemoryWatcherManager myLowMemoryWatcherManager = new LowMemoryWatcherManager(); private Consumer newThreadListener; private final AtomicInteger counter = new AtomicInteger(); @@ -100,6 +103,8 @@ public class AppScheduledExecutorService extends SchedulingWrapper { } public void shutdownAppScheduledExecutorService() { + // LowMemoryWatcher starts background threads so stop it now to avoid RejectedExecutionException + Disposer.dispose(myLowMemoryWatcherManager); delayQueue.shutdown(); // shutdown delay queue first to avoid rejected execution exceptions in Alarm doShutdown(); }