閱讀576 返回首頁    go 阿裏雲 go 技術社區[雲棲]


tomcat啟動時檢測到循環繼承而棧溢出的問題

一個用戶在使用tomcat7054版本啟動的時候遇到的錯誤:

Caused by: java.lang.IllegalStateException: 
Unable to complete the scan for annotations for web application [/test] 
due to a StackOverflowError. Possible root causes include a too low setting 
for  -Xss and illegal cyclic inheritance dependencies. 

The class hierarchy being processed was 

[org.jaxen.util.AncestorAxisIterator->
org.jaxen.util.AncestorOrSelfAxisIterator->
org.jaxen.util.AncestorAxisIterator]

at org.apache.catalina.startup.ContextConfig.checkHandlesTypes(ContextConfig.java:2112)
at org.apache.catalina.startup.ContextConfig.processAnnotationsStream(ContextConfig.java:2059)
at org.apache.catalina.startup.ContextConfig.processAnnotationsJar(ContextConfig.java:1934)
at org.apache.catalina.startup.ContextConfig.processAnnotationsUrl(ContextConfig.java:1900)
at org.apache.catalina.startup.ContextConfig.processAnnotations(ContextConfig.java:1885)
at org.apache.catalina.startup.ContextConfig.webConfig(ContextConfig.java:1317)
at org.apache.catalina.startup.ContextConfig.configureStart(ContextConfig.java:876)
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:374)
at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:117)
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5355)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)

這是在tomcat解析servlet3注釋時進行類掃描的過程,發現了兩個類的繼承關係存在循環繼承的情況而導致了棧溢出。

排查了一下,是因為應用所依賴的 dom4j-1.1.jar 裏存在 AncestorAxisIterator 和子類 AncestorOrSelfAxisIterato

% javap org.jaxen.util.AncestorAxisIterator

Compiled from "AncestorAxisIterator.java"
public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.StackedIterator {
    protected org.jaxen.util.AncestorAxisIterator();
    public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator);
    protected java.util.Iterator createIterator(java.lang.Object);
}

% javap org.jaxen.util.AncestorOrSelfAxisIterator

Compiled from "AncestorOrSelfAxisIterator.java"
public class org.jaxen.util.AncestorOrSelfAxisIterator extends org.jaxen.util.AncestorAxisIterator {
    public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator);
    protected java.util.Iterator createIterator(java.lang.Object);
} 

同時應用所依賴的 sourceforge.jaxen-1.1.jar 裏麵也存在這兩個同名類,但繼承關係正好相反:

% javap org.jaxen.util.AncestorAxisIterator

Compiled from "AncestorAxisIterator.java"
public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.AncestorOrSelfAxisIterator {
    public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator);
}

% javap org.jaxen.util.AncestorOrSelfAxisIterator

Compiled from "AncestorOrSelfAxisIterator.java"
public class org.jaxen.util.AncestorOrSelfAxisIterator implements java.util.Iterator {
    public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator);
    public boolean hasNext();
    public java.lang.Object next();
    public void remove();
}

簡單的說,在第1個jar裏存在 B繼承自A,在第2個jar裏存在同名的A和B,但卻是A繼承自B。其實也能運行的,隻是可能出現類加載時可能加載的不一定是你想要的那個,但tomcat做類型檢查的時候把這個當成了一個環。

ContextConfig.processAnnotationsStream方法裏,每次解析之後要對類型做一次檢測,然後才獲取注釋信息:

ClassParser parser = new ClassParser(is, null);
JavaClass clazz = parser.parse();
checkHandlesTypes(clazz);
...
AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
...

再看這個用來檢測類型的checkHandlesTypes方法裏麵:

populateJavaClassCache(className, javaClass);
JavaClassCacheEntry entry = javaClassCache.get(className);
if (entry.getSciSet() == null) {
    try {
        populateSCIsForCacheEntry(entry); // 這裏
    } catch (StackOverflowError soe) {
        throw new IllegalStateException(sm.getString(
            "contextConfig.annotationsStackOverflow",context.getName(),
            classHierarchyToString(className, entry)));
    }
}

每次新解析出來的類(tomcat裏定義了JavaClass來描述),會被populateJavaClassCache放入cache,這個cache內部是個Map,所以對於key相同的會存在把以前的值覆蓋了的情況,這個“環形繼承”的現象就比較好解釋了。

Map裏的key是String類型即類名,value是JavaClassCacheEntry類型封裝了JavaClass及其父類和接口信息。我們假設第一個jar裏B繼承自A,它們被放入cache的時候鍵值對是這樣的:

"A" -> [JavaClass-A, 父類Object,父接口]"
"B" -> [JavaClass-B, 父類A,父接口]

然後當解析到第2個jar裏的A的時候,覆蓋了之前A的鍵值對,變成了:

"A" -> [JavaClass-A, 父類B,父接口]
"B" -> [JavaClass-B, 父類A,父接口]

這2個的繼承關係在這個cache裏被描述成了環狀,然後在接下來的populateSCIsForCacheEntry方法裏找父類的時候就繞不出來了,最終導致了棧溢出。

這個算是cache設計不太合理,沒有考慮到不同jar下麵有相同的類的情況。問題確認之後,讓應用方去修正自己的依賴就可以了,但應用方說之前在7026的時候,是可以正常啟動的。這就有意思了,接著一番排查之後,發現在7026版本裏,ContextConfig.webConfig的時候先判斷了一下web.xml裏的版本信息,如果版本>=3才會去掃描類裏的servlet3注釋信息。

// Parse context level web.xml
InputSource contextWebXml = getContextWebXmlSource();
parseWebXml(contextWebXml, webXml, false);

if (webXml.getMajorVersion() >= 3) {
    // 掃描jar裏的web-fragment.xml 和 servlet3注釋信息
    ...
}

而在7054版本裏是沒有這個判斷的。搜了一下,發現是在7029這個版本裏去掉的這個判斷。在7029的changelog裏:

As per section 1.6.2 of the Servlet 3.0 specification and clarification from the Servlet Expert Group, the servlet specification version declared in web.xml no longer controls if Tomcat scans for annotations. Annotation scanning is now always performed – regardless of the version declared in web.xml – unless metadata complete is set to true.

之前對servlet3規範理解不夠清晰;之所以改,是因為在web.xml裏定義的servlet版本,不再控製tomcat是否去掃描每個類裏的注釋信息。也就是說不管web.xml裏聲明的servlet版本是什麼,都會進行注釋掃描,除非metadata-complete屬性設置為true(默認是false)。

所以在7029版本之後改為了判斷 webXml.isMetadataComplete() 是否需要進行掃描注釋信息。

最後更新:2017-05-23 19:31:51

  上一篇:go  JVM實用參數(四)內存調優
  下一篇:go  Http中get和post關係