美文网首页邦仁聊技术程序员首页投稿(暂停使用,暂停投稿)
Tomcat启动时检测到循环继承而栈溢出的问题

Tomcat启动时检测到循环继承而栈溢出的问题

作者: 七寸知架构 | 来源:发表于2016-12-17 11:33 被阅读193次

    一个用户在使用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 和子类 AncestorOrSelfAxisIterator:

    % 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() 是否需要进行扫描注释信息。

    相关文章

      网友评论

        本文标题:Tomcat启动时检测到循环继承而栈溢出的问题

        本文链接:https://www.haomeiwen.com/subject/kkvomttx.html