`
kwenge
  • 浏览: 2614 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

一个构造函数抛出异常引发的“餐具”

阅读更多

                                 一个构造函数抛出异常引发的“餐具”
                                                                                                                                                       --20141009
        公司基于IBM MQ开发了企业级综合服务管理平台(ESB),同时将用户、客户、机构等等基础信息管理功能分离出来提供基本的数据服务,而其他系统则通过综合服务管理平台进行服务访问。我方恰好设计、并开发了其中的用户信息管理系统。该系统需要管理企业范围内的所有用户信息、机构信息、系统信息、功能信息、权限信息,并提供各种用户认证服务。其中一个较为关键的功能是,我们需要将各业务系统相关的用户、机构以及功能权信息实时同步到各业务系统,并且保证同步功能的高效、稳定与可靠。
        数据信息实时同步功能,依托企业级综合服务管理平台,并且采用异步消息通信的方式,从系统Server端将变化的信息同步到各业务系统的Client端,Server端在我方部署,Client端在各业务系统部署。我方将Client端开发完成,然后通知所有需要使用我方信息的系统下载并安装Client端。Client端一旦启动,通常不会停止。除非:(1)人为的将Client端关闭;(2)Client端进程宕掉。
        近期,项目组收到业务系统发来的问题求助:该业务系统在部署Client端之后,过了一段时间之后,Client端的进程宕了。在Client端的启动目录下,产生了一批javacore和heapdump文件。希望项目组能够协助分析、并解决该问题。
        由于整个企业范围内大约有上百个Client端安装,绝大多数Client端都是没有出现这样的问题的。由此初步判定,此案例只是个例,如果是个例,那么问题通常该业务系统的环境存在些许关系。

第一步,对javacore文件进行分析
        由于javacore分析工具jca有些异样,我们只好使用Editplus将javacore打开,看看其中的内容究竟是怎么回事。
        文件一开始便告诉我们:java.lang.OutOfMemoryError “java heap space received”。即,本次导致进程宕掉的主要原因,是JVM的堆空间出现了内存溢出。在Javacore后面的线程堆栈输出方面,可以看到大量的线程都处于等待状态。
        既然是堆空间溢出,那么我们有必要分析下heapdump文件,看看是什么对象占用了大量的数据空间。

第二步,对heapdump文件进行分析
        由于heapdump文件是二进制文件,并且文件大小比较庞大,不可能像javacore那样使用编辑工具软件便可以进行分析,而是需要使用专门的分析软件,例如IBM Heap Analyzer分析工具。
        将heapdump文件导入到IBM Heap Analyzer工具后,可以看到:在发生内存溢出的时候,Client端JVM进程中存在大量的java对象。通过分析工具左下方Subponena Leak Suspects可以看到:
(1) WMQFFSTInfo对象竟然占用了97%的堆内存空间。
(2) WMQFFSTInfo中存在一个Map对象,包含7900个HashMap$Entry对象,每个Entry对象的大小为113K左右。
(3) 每个HashMap$Entry对象都涉及到WMQMessageConsumer对象。
于是,我们基本上可以判定,是WMQFFSTInfo类中的Map对象占用内存过多,而导致内存溢出。
        疑点1:是不是JVM分配的堆内存空间太小呢?因为Client端的java启动命令参数是由项目组设定,但是各业务系统依然可以打开startup.sh启动脚本,更改JVM分配空间大小。经过该业务系统人员提供的启动命令,可以看到java的启动参数:
java –Xms256 –Xmx1024 ………
此启动参数与其他能够正常运行的JVM启动参数是一致的。可以排除因为指定的JVM堆内存空间太小,而导致内存溢出的情况。
        疑点2:是不是因为同步到达的数据过多而导致的呢?从Server端将数据同步到Client端,其底层数据通信中间件使用的是IBM MQ,由于Client端从未成功连接到MQ队列上,不可能有数据从MQ取出来放入Client端的Java进程中。
        上面的疑点都排除了。那会是什么原因呢?
        当然,有读者或许会问,Client端在宕机前输出的日志信息,有没有?日志信息中,有没有什么异常输出?日志信息里边有没有什么值得分析的线索?的确,异常日志输出对于问题诊断是非常关键的,然而Client端的开发人员并没有正确地将异常堆栈跟踪信息输出到日志文件中,而是简单地将某个环节的操作结果打印到日志文件中,导致问题诊断难度加大。我们先来分析下问题所涉及的两个关键类的相关代码。

第三步,分析WMQFFSTInfo和WMQMessageConsumer
        由于MQ使用的是IBM的WebSphere MQ,属于商业版本,相关的接口jar包的代码并未开源,我们只能通过反编译工具对IBM MQ的jar包进行反编译,然后分析其中的java代码实现。
        对于WMQFFSTInfo类,我们发现其存在4个属性,其中,每个属性都有对应的add和remove方法,值得注意的是,这4个属性是静态属性,如果不调用相关属性对应的remove方法,则4个Map属性中引用的对象不会被JVM GC回收。

public class WMQFFSTInfo{
……….
static Map connection;
static Map session;
static Map consumer;
static Map producer;
………..

Public static void addConsumer(WMQMessageConsumer consumer){
……….
key=func(consumer);
consumer.put(key,consumer);
………
}

Public static removeConsumer(WMQMessageConsumer consumer){
……………
key=func(consumer);
consumer.remove(key);
……….
}
……….
}

 

       下面我们来看看WMQMessageConsumer类:
       (1) 有两个构造函数,这两个构造函数中,首先对对象属性进行this.prop=prop式的赋值,然后调用WMQFFSTInfo的addConsumer方法:WMQFFSTInfo. addConsumer(this);

public WMQMessageConsumer(WMQDestination destination, WMQSession session, String selector, boolean nolocal, JmsPropertyContext jmsProps)
    throws JMSException
  {
    super(jmsProps);
    this.destination = destination;
    this.session = session;
    this.selector = selector;
    this.nolocal = nolocal;

    WMQFFSTInfo.addConsumer(this);

    checkDestinationValidForNPHigh();
    this.syncShadow = new WMQSyncConsumerShadow(this, session, destination, selector, nolocal, this.subscriptionName);
    this.currentShadow = this.syncShadow;
    this.currentShadow.initialize();
  }

 
       (2) 存在close方法,在close方法中会调用WMQFFSTInfo. removeConsumer(this);即只有consumer对象成功调用了close方法,才会从WMQFFSTInfo中移除该consumer对象。
单纯从上面的类和方法中,我们并不能得出任何结论。

第四步,分析Client端输出的日志
       从Client端输出的日志中,发现在出现JVM宕掉之前,日志中大量输出JMSException,其主要内容是说:XXXX MQ队列没有找到。
       后来与Client端开发人员交流,得知,如果Client端在建立MQ连接后,若创建MessageConsumer失败,则会每个5秒钟,再次重复创建MessageConsumer。并且,MessageConsumer的创建次数并没有上限限制。
       结合前面分析的结果,在加上Client软件运行良好的系统并没有出现此问题,我们初步估计问题原因如下:Client端每隔5秒钟创建了一个MessageConsumer对象,并且将此对象放入了WMQFFSTInfo中,等到真正需要去连接MQ队列时,却由于MQ队列信息不正确而抛出了异常,导致系统根本没有时机去调用MessageConsumer的close方法释放已经放入WMQFFSTInfo的对象,在创建7900多个MessageConsumer之后,WMQFFSTInfo成功地消耗掉了JVM进程中所有的堆空间,而且还无法进行垃圾回收,最终导致JVM堆内存溢出,进而引发JVM进程宕机。

       下面我们来上述分析中存在的两个疑点:
       (1)MessageConsumer对象何时去连接MQ队列?
       (2)为什么连接MQ队列抛异常时,会导致MessageConsumer对象没有调用close方法?

 


第五步,问题的真正原因
       根据上面的分析,我们基本上对问题的发生原因有一个方向判定,但是所有的问题的真正原因还需要通过软件代码来证实,比如,本案例的Bug产生原因是,MQ队列(queue)无法正常连接,系统抛出JMSException之后,并没有将WMQFFSTInfo中的废弃WMQMessageConsumer对象移除,导致内存泄漏。
按照正常情况,如果调用了WMQMessageConsumer的close方法,则系统必然会从WMQFFSTInfo中将consumer移除。我们进而需要分析Spring框架下,DefaultMessageListenerContainer的代码中,是否支持在异常发生的情况下,会调用consumer的close方法。
       通过反编译工具,反编译spring子项目jms的jar包,找到DefaultMessageListenerContainer的内部运行线程,可以看到线程方法中代码片段如下:

public void run() { 
synchronized (DefaultMessageListenerContainer.this.lifecycleMonitor) {
       ………………..
      }
      catch (Throwable ex)
      {
        clearResources();
        ………………
      }
      finally
      {
        ………………..
        synchronized (DefaultMessageListenerContainer.this.lifecycleMonitor) {
          if ((!DefaultMessageListenerContainer.this.shouldRescheduleInvoker(this.idleTaskExecutionCount)) || (!DefaultMessageListenerContainer.this.rescheduleTaskIfNecessary(this)))
          {
            ………………..
            clearResources();
          }
………..
      }
    }


    private void clearResources() {
     …………….
      JmsUtils.closeMessageConsumer(this.consumer);
      JmsUtils.closeSession(this.session);
……………
      this.consumer = null;
      this.session = null;
    }

 
       由此判断,当consumer抛出异常时,Spring-JMS框架肯定会执行consumer的close()方法。
       然而,事实是,Client端进程抛出JMSException时,根本就没有调用close()方法。即,似乎是在告诉我们,是在创建consumer对象的时候抛出的异常,而不是在创建成功之后,抛出的异常,所以,Spring-JMS根本无法调用consumer的close方法【因为consumer是null】。
       我们将WMQMessageConsumer的构造函数再次拿出来详细分析:

public WMQMessageConsumer(WMQDestination destination, WMQSession session, String selector, boolean nolocal, JmsPropertyContext jmsProps)
    throws JMSException
  {
    super(jmsProps);
    this.destination = destination;
    this.session = session;
    this.selector = selector;
    this.nolocal = nolocal;

    WMQFFSTInfo.addConsumer(this);

    checkDestinationValidForNPHigh();
    this.syncShadow = new WMQSyncConsumerShadow(this, session, destination, selector, nolocal, this.subscriptionName);
    this.currentShadow = this.syncShadow;
    this.currentShadow.initialize();
  }

 
       WMQMessageConsumer的构造函数抛出了一个JMSException,并且是在WMQFFSTInfo.addConsumer(this);之后抛出。通过代码查证,new WMQSyncConsumerShadow()构造函数中,会通过JMSSession连接MQ的本地消息队列,而在没有成功连接的情况下,会抛出JMSException。由此,是WMQMessageConsumer构造函数中抛出了异常,导致了WMQFFSTInfo中的consumer对象没有释放,进而发生了内存泄漏。由此,问题不难从根本上解决之。
       正常情况下,在WMQMessageConsumer的构造函数中,需将抛出JMSException的Java代码用try-catch代码块封装,并且在出现异常时,在catch代码块中显示调用WMQFFSTInfo.removeConsumer(this)方法。至于为什么IBM的工程师们,不按常规的办法做,而是使用了会产生内存泄漏的解决办法,我们无法得知其中缘由。这一缺陷IBM MQ7.5版本中依然存在。【请IBM给解释】

第六步,模拟验证
       首先我们写一套类似的代码,用于重现问题。
WMQFFSTInfo.java

public class WMQFFSTInfo {
private final static Map<Integer, WMQMessageConsumer> consumer;
static {
consumer = new HashMap<Integer, WMQMessageConsumer>();
}

public static void addConsumer(WMQMessageConsumer consumer) {
WMQFFSTInfo.consumer.put(consumer.hashCode(), consumer);
}

public static void remove(WMQMessageConsumer consumer) {
WMQFFSTInfo.consumer.remove(consumer.hashCode());
}

public static Map<Integer, WMQMessageConsumer> getConsumer() {
return WMQFFSTInfo.consumer;
}
}

 

WMQMessageConsumer.java

public class WMQMessageConsumer {
	private String name;
	private String code;
	private JMSException e;

	public WMQMessageConsumer(String name, String code) {
		super();
		this.name = name;
		this.code = code;
		WMQFFSTInfo.addConsumer(this);
	}

	public WMQMessageConsumer(String name, String code,JMSException e) throws JMSException {
		super();
		this.name = name;
		this.code = code;
		this.e = e;
		WMQFFSTInfo.addConsumer(this);
		throw e;
	}

	public WMQMessageConsumer(String name, String code,JMSException e,boolean flag) throws JMSException {
		super();
		this.name = name;
		this.code = code;
		this.e = e;
		WMQFFSTInfo.addConsumer(this);
		try {
			throw e;
		} catch (Exception e1) {
			WMQFFSTInfo.remove(this);
			throw e;
		}
	}
	
	public void close(){
		WMQFFSTInfo.remove(this);
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getCode() {
		return code;
	}

	public void setCode(String code) {
		this.code = code;
	}

}

 

DefaultMessageListenerContainer.java

public class DefaultMessageListenerContainer {
public static void main(String[] args) {
main1();
main3();
main2();
}

public static void main1(){
WMQMessageConsumer consumer = null;
try {
consumer = new WMQMessageConsumer("对象成功构造不会发生异常的测试","TEST_01");
} catch (Throwable e) {
e.printStackTrace(); 
} finally{
if(consumer != null)consumer.close();
}

System.out.println("对象成功构造时,WMQFFSTInfo中consumer个数为:"+WMQFFSTInfo.getConsumer().size());
}

public static void main2(){
WMQMessageConsumer consumer = null;
try {
consumer = new WMQMessageConsumer("对象构造发生异常并抛出时的测试","TEST_02",new JMSException("模拟consumer构造函数发生了异常"));
} catch (Throwable e) {
e.printStackTrace(); 
} finally{
if(consumer != null)consumer.close();
}

System.out.println("对象构造发生异常并抛出时,WMQFFSTInfo中consumer个数为:"+WMQFFSTInfo.getConsumer().size());
}

public static void main3(){
WMQMessageConsumer consumer = null;
try {
consumer = new WMQMessageConsumer("对象构造发生异常并抛出,并且在捕获异常后去除Consumer的测试","TEST_03",new JMSException("模拟consumer构造函数发生了异常"),true);
} catch (Throwable e) {
e.printStackTrace(); 
} finally{
if(consumer != null)consumer.close();
}

System.out.println("对象构造发生异常时去除Consumer,WMQFFSTInfo中consumer个数为:"+WMQFFSTInfo.getConsumer().size());
}
}

 

测试结果显示如下:

TEST_01的输出结果:
对象成功构造时,WMQFFSTInfo中consumer个数为:0

TEST_02的输出结果:
javax.jms.JMSException: 模拟consumer构造函数发生了异常
at com.klie.emic.base.constructor.DefaultMessageListenerContainer.main2(DefaultMessageListenerContainer.java:28)
at com.klie.emic.base.constructor.DefaultMessageListenerContainer.main(DefaultMessageListenerContainer.java:7)
对象构造发生异常并抛出时,WMQFFSTInfo中consumer个数为:1

TEST_03 的输出结果:    
javax.jms.JMSException: 模拟consumer构造函数发生了异常
at com.klie.emic.base.constructor.DefaultMessageListenerContainer.main3(DefaultMessageListenerContainer.java:15)
at com.klie.emic.base.constructor.DefaultMessageListenerContainer.main(DefaultMessageListenerContainer.java:9)
对象构造发生异常时去除Consumer,WMQFFSTInfo中consumer个数为:0

 

为什么DefaultMessageListenerContainer中存在consumer的close方法调用,但是对象没有从WMQFFSTInfo中被移除?
       TEST_02的输出结果是特别有意思的,该结果说明虽然构造函数中抛出了JMSException,但是WMQMessageConsumer对象是已经生成了。由于构造函数抛出了异常,所以构造函数这个方法并没有正确结束,没有正确地返回对象引用。故而,在使用consumer = new WMQMessageConsumer(……)时,引用对象consumer并没有被正确赋值,或者更准确地说,是consumer引用的赋值操作根本就没有被执行。所以,虽然示例中,DefaultMessageListenerContainer 的main方法将consumer.close方法写入了finally代码块,但是由于consumer没有赋值,还是null,所以close方法没有执行,或者就算是执行,也会抛出NullPointerException。

后记
        事后,Client端开发人员通过更改异常日志信息输出,并且根据本文的分析结论,对问题进行重现并输出异常堆栈跟踪信息,结果如下:
[img]

 [/img]
       从异常堆栈信息,可以明显地看到是何处抛出了异常。同时,本文的分析结论,也得到了客观的证实。

问题如何解决?
       最好的办法是,将问题类反编译出来,对问题代码进行完善,并将编译后的结果放入jar包中,重启下Client端,并且保证Client端所连MQ是无误的、是经过其他工具测试,能够收发消息的。

       疑问1:是否可以通过升级JAR包解决?
       ANS:不可以,因为最新V7.5版本的IBM MQ.jar所提供的JMS实现,依然存在此问题。

       疑问2:是否JMSConnection连接时,IP或端口配置出错,也会内存泄漏?
       ANS:不会,因为IBM的JMSMessageConnection中,构造函数对IP和端口的连接抛异常的情况进行了try-catch处理。

       疑问3:是否队列管理器配置出错,会出现内存泄漏?
       ANS:估计会,因为此处的情况与连接QUEUE的处理代码是一样的,出现异常时直接抛出,而并没有catch异常,并remove掉Connection对象。

       疑问4:为什么IBM的JMS实现,有时候会对异常try-catch避免内存泄漏,有时候又不会呢?
       ANS:无可奉告,呵呵。

       本文到此结束,谢谢观看。

 

 

分享到:
评论

相关推荐

    dotnet C# 如果在构造函数抛出异常 是否可以拿到对象赋值的变量.rar

    标题中的“dotnet C# 如果在构造函数抛出异常 是否可以拿到对象赋值的变量”是一个关于C#编程语言中的构造函数和异常处理的问题。在C#中,构造函数是用于初始化新创建的对象的特殊方法。当在构造函数中抛出异常时,...

    dotnet C# 如果在构造函数抛出异常 析构函数是否会执行.rar

    本话题聚焦于一个特定的编程概念:当在构造函数中抛出异常时,析构函数是否会执行。这个问题涉及到类对象的生命周期、异常处理以及垃圾回收机制。 首先,我们需要了解构造函数和析构函数的基本概念。构造函数是类的...

    C++构造函数抛出异常需要注意的地方

    虽然从语法上讲,构造函数确实可以抛出异常,但在实际编程实践中,这通常被视为一种不良做法,因为构造函数抛出异常可能会引发一系列问题,尤其是与内存管理和对象生命周期相关的风险。 首先,我们需要了解构造函数...

    构造函数中抛出的异常

    标准C++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证对象的...

    C++构造函数中抛出的异常

     1、标准C++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证...

    析构函数不能抛出异常的原因

    当一个对象的析构函数抛出异常时,通常情况下这个异常并不会被捕获处理,而是会被传递给上层的析构函数或程序的其他部分。这可能导致一系列连锁反应,使得多个析构函数依次抛出异常。最终,这种异常链的传播可能会...

    构造函数和复制构造函数

    在上面的代码中,我们定义了一个名为classA的类,其中包括一个默认构造函数、一个带参数的构造函数、一个复制构造函数、一个赋值操作符和一个析构函数。在main函数中,我们使用了多种方式来调用这些函数,例如语句1...

    构建一个类Point,它提供两个公有的构造函数,一个没有参数的Point构造函数和一个有两个double参数的构造函数。

    构建一个类Point,它提供两个公有的构造函数,一个没有参数的Point构造函数和一个有两个double参数的构造函数。另外在该类中提供一个静态方法计算两个点的直线距离,传入参数为两个Point类实例。然后设计一个测试类...

    详解C++中构造函数,拷贝构造函数和赋值函数的区别和实现

    构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同) 首先说一下一个C++的空类,编译器会加入哪些默认的成员函数 默认...

    包含构造函数和析构函数的C++程序

    构造函数可以被重载,这意味着一个类可以有多个构造函数,但每个构造函数的参数列表必须不同。 在提供的代码示例中,定义了一个名为`Student`的类,其中包含了一个构造函数: ```cpp Student(int n, string nam, ...

    没有可用的复制构造函数或复制构造函数声明

    在C++编程中,"没有可用的复制构造函数或复制构造函数声明"是一个常见的错误,通常出现在尝试复制一个对象,而该对象的类没有定义复制构造函数时。在这个特定的情境中,问题出在一个名为`CArray, int&gt;`的自定义数组...

    构造函数的继承问题 笔记

    2. **显式调用父类的构造函数**:如果父类中定义了显式构造函数,那么子类的构造函数必须通过`super()`来显式调用父类的一个构造函数。如果不这样做,编译器将报错。 例如: ```java class Base { public Base...

    构造函数和析构函数PPT课件.pptx

    缺省构造函数是指在定义类时没有定义构造函数的情况下,编译器自动产生的一个构造函数,该函数什么事也不做。其形式为:&lt;类名&gt;::&lt;类名&gt;(){}。 拷贝构造函数是一种特殊的构造函数,它的功能是用一个已知的对象来初始...

    构造函数与析构函数

    析构函数也是以类名作为函数名,与构造函数不同的是在函数名前添加一个“~”符号,标识该函数是析构函数。析构函数没有返回值,甚至void类型也不可以,析构函数也没有参数,因此析构函数是不能够重载的。这是析构...

    Java 自定义异常和抛出异常

    在这个例子中,我们创建了一个名为`CustomException`的新异常类,它继承了`Exception`类并提供了构造函数来传递错误消息。 接下来,我们讨论如何在代码中抛出异常。在Java中,`throw`关键字用于抛出一个异常对象。...

    C++\测试 对象成员构造函数、基类构造函数、派生类本身的构造函数 的先后顺序.rar

    当一个类是另一个类的基类时,基类的构造函数会在派生类构造函数之前被调用。这是为了确保基类的部分首先被正确初始化。基类的构造函数可以通过派生类的成员初始化列表来指定: ```cpp class Base { public: ...

    在派生类的构造函数中调用基类的构造函数

    这个实例可能涉及到一个基类和一个或多个派生类,其中基类有一个或多个构造函数,而派生类需要根据不同的情况调用相应的基类构造函数。通过这样的实例,读者可以深入理解构造函数的调用顺序,以及如何在多级继承和...

    重写重载构造函数

    结构类型的构造函数与类的构造函数类似,但是structs不能包含显式默认构造函数,因为编译器将自动提供一个构造函数。此构造函数将结构中的每个字段初始化为默认值表中显示的默认值。 类和structs都可以定义具有参数...

Global site tag (gtag.js) - Google Analytics