clearReferences系のエラーログの対応方法

調査を進めた。clearReferencesThreadsとclearThreadLocalMapのエラーを黙らせるには以下のようなコードをサーブレットのdestroyに書けば良い(サーブレットの定義順には注意)。

@Override
public void destroy() {
stopAllThreads();
}
private void stopAllThreads() {
Thread[] threads = getThreads();
ClassLoader cl = this.getClass().getClassLoader();
List<String> jvmThreadGroupList = new ArrayList<String>();
jvmThreadGroupList.add("system");
jvmThreadGroupList.add("RMI Runtime");
// Iterate over the set of threads
for (Thread thread : threads) {
if (thread != null) {
ClassLoader ccl = thread.getContextClassLoader();
if (ccl != null && ccl == cl) {
// Don't warn about this thread
if (thread == Thread.currentThread()) {
continue;
}
// Don't warn about JVM controlled threads
ThreadGroup tg = thread.getThreadGroup();
if (tg != null && jvmThreadGroupList.contains(tg.getName())) {
continue;
}
waitThread(thread);
// Skip threads that have already died
if (!thread.isAlive()) {
continue;
}
if (logger.isInfoEnabled()) {
logger.info("Interrupting a thread ["
+ thread.getName() + "]...");
}
thread.interrupt();
waitThread(thread);
// Skip threads that have already died
if (!thread.isAlive()) {
continue;
}
if (logger.isInfoEnabled()) {
logger.info("Stopping a thread [" + thread.getName()
+ "]...");
}
thread.stop();
}
}
}
Field threadLocalsField = null;
Field inheritableThreadLocalsField = null;
Field tableField = null;
try {
threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
inheritableThreadLocalsField = Thread.class
.getDeclaredField("inheritableThreadLocals");
inheritableThreadLocalsField.setAccessible(true);
// Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects
// accessible
Class<?> tlmClass = Class
.forName("java.lang.ThreadLocal$ThreadLocalMap");
tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
} catch (Exception e) {
// ignore
}
for (Thread thread : threads) {
if (thread != null) {
Object threadLocalMap;
try {
// Clear the first map
threadLocalMap = threadLocalsField.get(thread);
clearThreadLocalMap(cl, threadLocalMap, tableField);
} catch (Exception e) {
// ignore
}
try { // Clear the second map
threadLocalMap = inheritableThreadLocalsField.get(thread);
clearThreadLocalMap(cl, threadLocalMap, tableField);
} catch (Exception e) {
// ignore
}
}
}
}
private void waitThread(Thread thread) {
int count = 0;
while (thread.isAlive() && count < 5) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
count++;
}
}
/*
* Get the set of current threads as an array.
*/
private Thread[] getThreads() {
// Get the current thread group
ThreadGroup tg = Thread.currentThread().getThreadGroup();
// Find the root thread group
while (tg.getParent() != null) {
tg = tg.getParent();
}
int threadCountGuess = tg.activeCount() + 50;
Thread[] threads = new Thread[threadCountGuess];
int threadCountActual = tg.enumerate(threads);
// Make sure we don't miss any threads
while (threadCountActual == threadCountGuess) {
threadCountGuess *= 2;
threads = new Thread[threadCountGuess];
// Note tg.enumerate(Thread[]) silently ignores any threads that
// can't fit into the array
threadCountActual = tg.enumerate(threads);
}
return threads;
}
private void clearThreadLocalMap(ClassLoader cl, Object map,
Field internalTableField) throws NoSuchMethodException,
IllegalAccessException, NoSuchFieldException,
InvocationTargetException {
if (map != null) {
Method mapRemove = map.getClass().getDeclaredMethod("remove",
ThreadLocal.class);
mapRemove.setAccessible(true);
Object[] table = (Object[]) internalTableField.get(map);
if (table != null) {
for (int j = 0; j < table.length; j++) {
if (table[j] != null) {
boolean remove = false;
// Check the key
Field keyField = Reference.class
.getDeclaredField("referent");
keyField.setAccessible(true);
Object key = keyField.get(table[j]);
if (cl.equals(key)
|| (key != null && cl == key.getClass()
.getClassLoader())) {
remove = true;
}
// Check the value
Field valueField = table[j].getClass()
.getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(table[j]);
if (cl.equals(value)
|| (value != null && cl == value.getClass()
.getClassLoader())) {
remove = true;
}
if (remove) {
Object entry = ((Reference<?>) table[j]).get();
if (logger.isInfoEnabled()) {
logger.info("Removing " + key.toString()
+ " from a thread local...");
}
mapRemove.invoke(map, entry);
}
}
}
}
}
}

上記のコードで、スレッドが終了するのを待ち、Tomcatが処理する前にスレッド系のものたちをクリーンするのでエラーが出なくなる(TimerThreadを使っているものがあれば、上記のコードは未対応なので処理が必要)。Tomcatがやりたい、メモリリークをしないようにというのは分かるのだけど、これを 6.0.24 からエラーレベルでログは吐くのは間違っているんじゃないかね…。WARNくらいなレベルな気がするのだけど。しかも、シャットダウンする時なので、そもそも全部捨てるから吐かれてもな、という気がする(再配備するようなケースではこの処理があると有効かも)。
今回はアプリ側で直したけど、S2とかのライブラリ側で対応するとなると、ThreadLocalをextendedしないとか、ThreadLocalは使い終わりにremove()を呼んでおくとか、そういう対応になるような気が。たとえば、S2のHttpServletExternalContextとか、スレッドローカルがあるけど、removeな状態にしておくのは問題がある気がするし。しかも、スレッドローカルだから消すのがめんどい…。そんな感じで、そもそもの解決策は、どうせ値を消しているのだから、Tomcatがエラーレベルでログを吐かなければ良いだけじゃないかね。
そのうち、みんなが 6.0.24 を使い始めて文句が多発で変更が入りそうな気がする動きだな…。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です