zoukankan      html  css  js  c++  java
  • Tomcat 加载包导致OOM

    Tomcat 加载包导致OOM

    遇到一个问题,启动时加载facade包后,导致出现tomcat启动时出现大量OOM,创建了超过2G的UNKNOW的对象

    环境介绍

    当前使用的是java8+ tomcat 7.0.54

    结果分析

    首先获取下内存 dump,进行分析,这里我推荐使用JProfile工具可视化程度较高,方便快捷,加载完之后可以显然看到2个异常的地方

    • 启动的时候创建了超过2G的Unknow对象

    1

    • tomcat localhost-startstop线程,启动的时候出现了OOM的异常

      2

    这里的OOM堆栈的上一个行为就是创建Unknow的对象,正好印证了OOM的罪魁祸首在这个代码中

    过程分析

    从第二点的调用堆栈可以明显看出,在调用 org.apache.catalina.startup.ContextConfig#webConfig的时候出现的OOM,而我们提供的Facade的包的主要功能就是提供dubbo接口,而我启动的项目的dubbo加载是通过配置dubbo- reference xml进行加载,整个流程如下

    • 项目为tomcat项目,需要在web.xml写入需要加载的spring-application.xml,启动的时候才会去初始化对应的spring bean.
    • 我们在初始化dubbo接口的时候,dubbo的接口是写在xml中, 所以也会由上面描述的#webConfig对我们写入的xml进行处理
    • 当我们Facade的接口去初始化的时候,会通过tomcat的classFile.parse(),这个时候解析异常,最终出现大量UNKNOW实例 (这里为啥会出现大量对象的原因没有找到,按照加载逻辑是不会同时出现图中1百万个对象)

    知道了产生这个UNKNOW的过程,现在最主要的问题是什么情况下产生这个问题,实践出真知,接下来分析下产生的条件,

    首先我们需要准备一个tomcat-7.0.54包

    wget https://archive.apache.org/dist/tomcat/tomcat-7/v7.0.54/src/apache-tomcat-7.0.54-src.tar.gz

    首先是org.apache.catalina.startup.ContextConfig#processAnnotationsStream这里就是加载jar包中的类并转成对应JavaClass

    protected void processAnnotationsStream(InputStream is, WebXml fragment,
                boolean handlesTypesOnly)
                throws ClassFormatException, IOException {
    			
            ClassParser parser = new ClassParser(is, null);
      			
            JavaClass clazz = parser.parse();
            checkHandlesTypes(clazz);
     		    ...
    }
    

    后面的主要问题在parse() 中的

    public static final Attribute readAttribute(DataInputStream file,
                ConstantPool constant_pool) throws IOException,
                ClassFormatException
        {
            ConstantUtf8 c;
            String name;
            int name_index;
            int length;
            byte tag = Constants.ATTR_UNKNOWN; // Unknown attribute
            // Get class name from constant pool via `name_index' indirection
    	      // 通过“name_index”间接从常量池中获取类名
            name_index = file.readUnsignedShort();
            // 找到映射关系
            c = (ConstantUtf8) constant_pool.getConstant(name_index,
                    Constants.CONSTANT_Utf8);
            name = c.getBytes();
            // Length of data in bytes
            length = file.readInt();
            // Compare strings to find known attribute
            // System.out.println(name);
            for (byte i = 0; i < Constants.KNOWN_ATTRIBUTES; i++)
            {
                if (name.equals(Constants.ATTRIBUTE_NAMES[i]))
                {
                    tag = i; // found!
                    break;
                }
            }
            // Call proper constructor, depending on `tag'
            switch (tag)
            {
            case Constants.ATTR_UNKNOWN:
                AttributeReader r = readers.get(name);
                if (r != null)
                {
                    return r.createAttribute(name_index, length, file,
                            constant_pool);
                }
                return new Unknown(name_index, length, file, constant_pool);
             ...
               //后面忽略
        }
    

    可见其执行逻辑是:

    • 读取class文件,获取当前属性
    • 循环判断该属性是不是其tomcat写死的属性表中的类型
    • 根据其属性创建不同的Attribute

    这里涉及到属性表, 在class文件,字段表,方法表都可以携带自己的属性表集合,以描述某些场景特有信息,任何人实现的编译器都可以向属性表写入自己的属性.虚拟机在执行的时候会忽略不认识的属性.在最新的Java SE 12中,预定义属性已经增加到29项,下面是一些常用的属性

    而在当前版本Tomcat中定义的属性表为:

    public static final String[] ATTRIBUTE_NAMES = {
      "SourceFile", "ConstantValue", "Code", "Exceptions",
      "LineNumberTable", "LocalVariableTable",
      "InnerClasses", "Synthetic", "Deprecated",
      "PMGClass", "Signature", "StackMap", 
      "RuntimeVisibleAnnotations", "RuntimeInvisibleAnnotations",
      "RuntimeVisibleParameterAnnotations", "RuntimeInvisibleParameterAnnotations",
      "AnnotationDefault", "LocalVariableTypeTable", "EnclosingMethod", "StackMapTable",
      "BootstrapMethods", "MethodParameters"
    };
    

    可见有些属性在tomcat中是没有定义到的,而当其没有找到定义的时候,就会去创建UNKNOW对象,到这里UNKNOW对象产生的流程已经出来了,不过要去验证下我们提供的jar包是否真的会产生该问题

    源jar包分析

    由于我们提供的jar包实体是非常的简单的,只有接口与类,pom.xml中包含的jar包如下

    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>${version.validation-api}</version>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${version.fasterxml.jackson}</version>
        <optional>true</optional>
    </dependency>
    

    只有Lombok,validation,jackson的相关注解,我们在一系列排除后怀疑到了2个主要注解上

    • Lombok包提供的注解
    • Validation提供的注解

    然后我们找了一个类,对其class进行反汇编分析

    
    import lombok.Data;
    
    import javax.validation.Valid;
    import javax.validation.constraints.Max;
    import javax.validation.constraints.NotNull;
    import java.io.Serializable;
    
    @Data
    public class ReceiveLimitIdentityDTO implements Serializable {
        /**
         * 限制维度类型
         *
         */
        @NotNull(message = "限制维度类型不能为空.", groups = {ActActivityReq.GroupAdd.class, ActActivityReq.GroupModify.class})
        private Integer dimensionType;
        /**
         * 每日统计限制
         */
        @NotNull(message = "每日统计限制不能为空.", groups = {ActActivityReq.GroupAdd.class, ActActivityReq.GroupModify.class})
        @Valid
        private StatLimit dayStatLimit;
        /**
         * 累计统计限制
         */
        @NotNull(message = "累计统计限制不能为空.", groups = {ActActivityReq.GroupAdd.class, ActActivityReq.GroupModify.class})
        @Valid
        private StatLimit totalStatLimit;
    
        @Data
        public static class StatLimit implements Serializable {
            /**
             * 限制类型 type 0无限次 1限次      
             */
            @NotNull(message = "限制类型不能为空.", groups = {ActActivityReq.GroupAdd.class, ActActivityReq.GroupModify.class})
            private Integer type;
            /**
             * 限制次数 -1无限制
             */
            @Max(value = 999, message = "限制次数不能大于999.", groups = {ActActivityReq.GroupAdd.class, ActActivityReq.GroupModify.class})
            @NotNull(message = "限制次数不能为空.", groups = {ActActivityReq.GroupAdd.class, ActActivityReq.GroupModify.class})
            private Integer num;
        }
    }
    

    生成的反编译文件如下

    Classfile /Users/fulln/IdeaProjects/demo/target/classes/com/example/demo/tomcat/ReceiveLimitIdentityDTO.class
      Last modified 2021-12-13; size 3667 bytes
      MD5 checksum 9c50a675fb864b5ca41a2f596ad06ebb
      Compiled from "ReceiveLimitIdentityDTO.java"
    public class com.example.demo.tomcat.ReceiveLimitIdentityDTO implements java.io.Serializable
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
        #1 = Methodref          #21.#91       // java/lang/Object."<init>":()V
        #2 = Fieldref           #5.#92        // com/example/demo/tomcat/ReceiveLimitIdentityDTO.dimensionType:Ljava/lang/Integer;
        #3 = Fieldref           #5.#93        // com/example/demo/tomcat/ReceiveLimitIdentityDTO.dayStatLimit:Lcom/example/demo/tomcat/ReceiveLimitIdentityDTO$StatLimit;
        #4 = Fieldref           #5.#94        // com/example/demo/tomcat/ReceiveLimitIdentityDTO.totalStatLimit:Lcom/example/demo/tomcat/ReceiveLimitIdentityDTO$StatLimit;
        #5 = Class              #95           // com/example/demo/tomcat/ReceiveLimitIdentityDTO
        #6 = Methodref          #5.#96        // com/example/demo/tomcat/ReceiveLimitIdentityDTO.canEqual:(Ljava/lang/Object;)Z
        #7 = Methodref          #5.#97        // com/example/demo/tomcat/ReceiveLimitIdentityDTO.getDimensionType:()Ljava/lang/Integer;
        #8 = Methodref          #21.#98       // java/lang/Object.equals:(Ljava/lang/Object;)Z
        #9 = Methodref          #5.#99        // com/example/demo/tomcat/ReceiveLimitIdentityDTO.getDayStatLimit:()Lcom/example/demo/tomcat/ReceiveLimitIdentityDTO$StatLimit;
       #10 = Methodref          #5.#100       // com/example/demo/tomcat/ReceiveLimitIdentityDTO.getTotalStatLimit:()Lcom/example/demo/tomcat/ReceiveLimitIdentityDTO$StatLimit;
       #11 = Methodref          #21.#101      // java/lang/Object.hashCode:()I
       #12 = Class              #102          // java/lang/StringBuilder
       #13 = Methodref          #12.#91       // java/lang/StringBuilder."<init>":()V
       #14 = String             #103          // ReceiveLimitIdentityDTO(dimensionType=
       #15 = Methodref          #12.#104      // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       #16 = Methodref          #12.#105      // java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
       #17 = String             #106          // , dayStatLimit=
       #18 = String             #107          // , totalStatLimit=
       #19 = String             #108          // )
       #20 = Methodref          #12.#109      // java/lang/StringBuilder.toString:()Ljava/lang/String;
       #21 = Class              #110          // java/lang/Object
       #22 = Class              #111          // java/io/Serializable
       #23 = Class              #112          // com/example/demo/tomcat/ReceiveLimitIdentityDTO$StatLimit
       #24 = Utf8               StatLimit
       #25 = Utf8               InnerClasses
       #26 = Utf8               dimensionType
       #27 = Utf8               Ljava/lang/Integer;
       #28 = Utf8               RuntimeVisibleAnnotations
       #29 = Utf8               Ljavax/validation/constraints/NotNull;
       #30 = Utf8               message
       #31 = Utf8               限制维度类型不能为空.
       #32 = Utf8               groups
       #33 = Class              #114          // com/example/demo/tomcat/ActActivityReq$GroupAdd
       #34 = Utf8               GroupAdd
       #35 = Utf8               Lcom/example/demo/tomcat/ActActivityReq$GroupAdd;
       #36 = Class              #115          // com/example/demo/tomcat/ActActivityReq$GroupModify
       #37 = Utf8               GroupModify
       #38 = Utf8               Lcom/example/demo/tomcat/ActActivityReq$GroupModify;
       #39 = Utf8               RuntimeVisibleTypeAnnotations
         
         ...
         //后面省略
    
    

    发现了# 39 常量.也就是对应validate包中的 @NotNull(message = "限制维度类型不能为空.", groups = {ActActivityReq.GroupAdd.class, ActActivityReq.GroupModify.class})反汇编出来使用的属性是RuntimeVisibleTypeAnnotations,没有出现在Tomcat的属性表中,至此,产生的逻辑也出现了

    待解决

    现在仍然需要解决的问题是

    1. 即使是在字段上面,但是创建总一百万个Objects,也是不太能实现的,内存也没有出现一直增长,只是启动时的飙升

    欢迎讨论指教上面疑问

  • 相关阅读:
    【postman】api开发必备神器
    【Nginx】Nginx服务器配置调优
    转:【微信公众号】微信snsapi_base静默授权与snsapi_userinfo网页授权的实现(不建议使用静默,直接用主动比较方便)
    【vue2.X+iview2.x】iView在非 template/render 模式下标签的转化
    【MySQL】MySQL悲观锁 + 事物 + for update 解决普通流量并发的问题
    【Redis】Redis事务详解,Redis事务支持回滚(不支持悲观锁)
    java 为什么有时一个类有多个构造函数
    一个普通类如何不实现一个接口的所有方法
    Groovy 安全导航(safe-navigation)操作符(?.)
    java 中的 ?: 埃尔维斯操作符
  • 原文地址:https://www.cnblogs.com/wzqshb/p/15684005.html
Copyright © 2011-2022 走看看