Java应用线程泄漏原因分析与避免

起因-日志丢失

生产上出现过几次日志丢失的问题,我们日志每小时生成一个文件,然后每个小时刚到整点切换的时候会生成新文件然后正常输出日志,到了固定时点就空了,只有一个定时清理数据的线程打的几行日志。

通过分析,是因为我们的应用部署在weblogic上,每次重新发war包并不会重启weblogic,只是停止之前的应用,重新启动一个新的,而之前的应用有个别线程没能关闭,与新应用同时打日志,出现了问题。

泄漏的线程与新应用的线程各自持有一个log4j的appender,关键这两个appender的规则完全一致。

新应用的线程一直在打印日志,到了整点就切换,而泄漏的线程每半个小时才被唤醒一次,然后打印几句日志。

我们来看一下log4j切换日志的代码:

 /**
     Rollover the current file to a new file.
  */
  void rollOver() throws IOException {

    /* Compute filename, but only if datePattern is specified */
    if (datePattern == null) {
      errorHandler.error("Missing DatePattern option in rollOver().");
      return;
    }

    String datedFilename = fileName+sdf.format(now);
    // It is too early to roll over because we are still within the
    // bounds of the current interval. Rollover will occur once the
    // next interval is reached.
    if (scheduledFilename.equals(datedFilename)) {
      return;
    }

    // close current file, and rename it to datedFilename
    this.closeFile();

    //!!!!!!!!!!重点在这!!!!
    //如果存在已经重名的就给删掉。
    File target  = new File(scheduledFilename);
    if (target.exists()) {
      target.delete();
    }

    File file = new File(fileName);
    boolean result = file.renameTo(target);
    if(result) {
      LogLog.debug(fileName +" -> "+ scheduledFilename);
    } else {
      LogLog.error("Failed to rename ["+fileName+"] to ["+scheduledFilename+"].");
    }

    try {
      // This will also close the file. This is OK since multiple
      // close operations are safe.
      this.setFile(fileName, true, this.bufferedIO, this.bufferSize);
    }
    catch(IOException e) {
      errorHandler.error("setFile("+fileName+", true) call failed.");
    }
    scheduledFilename = datedFilename;

  }

假如现在刚到10点了,因为新应用一直在打印日志,10点时切换了一个新日志,然后不停的打日志,结果到了10:15,另一个appender也要打日志了,它发现过了10点了,自己原来持有的日志还是9点点,就切换一个,发现已经有重名点,就删掉重建了,这就是原因。可是有人会说前一个appender持有的文件句柄文件被删了,它不应该报异常吗?经过我的实验,没有任何异常反应。

public static void main(String[] args) throws IOException {
    File a = new File("test.txt");
    BufferedWriter bw1 = new BufferedWriter(
			new FileWriter(a));
    bw1.write("aaaaaaaa");
    bw1.flush();
    a.delete();
    bw1.write("aaaaaaaaa");
    bw1.flush();

    File b = new File("test.txt");
    BufferedWriter bw2 = new BufferedWriter(
			new FileWriter(b));
    bw2.write("bbbbbbbbb");
    bw2.flush();

    bw1.write("aaaaaaaaa");
    bw1.flush();

}

上面这段代码不会有任何异常,最终生成的文件内容是bbbbbbbbb。

这个问题只是线程泄漏带来的问题之一,还有与之对应的内存泄漏等其它问题。接下来就来分析一下线程泄漏等原因与如何避免此类问题。

应用服务器如何清理线程?


对于应用中自己起动的一些后台线程,应用服务器一般不会给你停掉。不了解weblogic怎么清理这些线程的,看了下tomcat的,tomcat默认并不会强制关闭这些线程。

先看tomcat中的一段警告日志:

七月 27, 2016 7:02:10 下午 org.apache.catalina.loader.WebappClassLoaderBase clearReferencesThreads

警告: The web application [firefly] appears to have started a thread named [memkv-gc-thread-0] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:

sun.misc.Unsafe.park(Native Method)

java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)

java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)

java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1090)

java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:807)

java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)

java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)

java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)

java.lang.Thread.run(Thread.java:745)

在tomcat中有关停止这些线程有一个配置默认是关的,如果开了,它是用stop方法,也是有风险的。

    /**
     * Should Tomcat attempt to terminate threads that have been started by the
     * web application? Stopping threads is performed via the deprecated (for
     * good reason) <code>Thread.stop()</code> method and is likely to result in
     * instability. As such, enabling this should be viewed as an option of last
     * resort in a development environment and is not recommended in a
     * production environment. If not specified, the default value of
     * <code>false</code> will be used.
     */
    private boolean clearReferencesStopThreads = false;

我猜weblogic也是类似的策略,所以不能指望应用服务器给你清理线程。

应该在什么地方清理线程?

正确的停止应用线程的方法是自己去停止,而不要依赖于应用服务器!

例如,使用spring的,可以利用bean的destroy方法,或者没有spring的,注册一个listener。

public class ContextDestroyListener 
		implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
	// TODO Auto-generated method stub
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
	// TODO Auto-generated method stub
	//在这个地方清理线程
    }
}

我们知道在什么地方去清理这些线程了,接下来就是如何去清理他们了。清理线程并不是加个shutdown方法或者调用一下interrupt那么简单的事情。

如何正确停止线程?

要自己停止线程,首先你得拿到线程的句柄,也就是thread对象或者线程池,如果你写了下面的代码,启动完后你就找不到这个线程了,所以线程一定要在清理线程的时候能拿得到句柄。

public static void main(String[] args) {
    new Thread(new Runnable() {
	@Override
	public void run() {
	    while(true) {
		try {
		    Thread.sleep(1000);    
		System.out.println("wake up");
		} catch (InterruptedException e) {
		e.printStackTrace();
		}
	    }
	}

    }).start();
}

正确的方法是把Thread放到一个变量里,例如t,然后通过t去停止这个线程。停止线程的方法一般有stop,destroy,interrupt等,但是stop,destroy都已经被废弃了,因为可能造成死锁,所以通常等做法是使用interrupt。使用interrupt其实类似于信号,就好比你在Linux进程中把SIGTERM信号忽略了,那就没法通过kill杀死进程了,interrupt也是如此。 下面等线程只会报一个异常,中断信号被忽略。

public static void main(String[] args) {
    Thread t = new Thread(new Runnable() {
	@Override
	public void run() {
	    while(true) {
		try {
		    Thread.sleep(1000);
		    System.out.println("wake up");
	        } catch(InterruptedException e) {
		    e.printStackTrace();
	        }
	    }
	}
    });
    t.start();
    t.interrupt();//并不能停止线程
}

这种阻塞的线程,一般调用的函数都会强制检查并抛出interruption异常,类似的还有wait(),阻塞队列的take等,为了程序能正常关闭,InterruptedException最好不好忽略。

public static void main(String[] args) {
	Thread t = new Thread(new Runnable() {
		@Override
		public void run() {
			while (true) {
				try {
					Thread.sleep(1000);
					System.out.println("wake up");
				} catch (InterruptedException e) {
					e.printStackTrace();
					System.out.println("stop...");
					break;
				}
			}
		}
	});
	t.start();
	t.interrupt();
}

如果run方法里没有抛出InterruptedException怎么办?例如下面这个

public static void main(String[] args) {
	Thread t = new Thread(new Runnable() {
		@Override
		public void run() {
			int i = 0;
			while (true) {
				i++;
			}
		}
	});
	t.start();
	t.interrupt();
}

这种情况就需要run方法里不断检查是否被中断了,否则永远停不下来。

public static void main(String[] args) {
	Thread t = new Thread(new Runnable() {
		@Override
		public void run() {
			int i = 0;
			while (true) {
				i++;
				if(Thread.interrupted()) {
					System.out.println("stop..");
					break;
				}
			}
		}
	});
	t.start();
	t.interrupt();
}

上面就是正确停止单个线程的方法,对于线程池,一般有两个方法,shutdown和shutdownNow,这两个方法差别是很大的,shutdown只是线程池不再接受新的任务,但是不会打断正在运行的线程,而shutdownNow会对逐个线程调用interrupt方法,如果你的线程是确定可以在一段时间跑完的,可以用shutdown,但是如果是一个死循环,或者在sleep需要很长时间才重新唤醒,那就用shutdownNow,然后对于Runnable的实现也需要遵循上面单个线程的原则。

时间: 2024-10-01 04:55:50

Java应用线程泄漏原因分析与避免的相关文章

Java创建线程的细节分析

转载:http://shmilyaw-hotmail-com.iteye.com/blog/1880902 前言 关于线程创建的问题,可以说是老生常谈了.在刚开始学习Thread的时候基本上都会接触到,用简单的一两句话就可以概括起来.一个是创建类实现Runnable接口,然后将该类的实例作为参数传入到Thread构造函数中.再调用Thread对象的start方法.还有一种是继承Thread类,覆写run方法.然后在该对象实例中调用start方法.那么,这两种方式在什么情况下适用呢?还有,既然我们

java threadPool 线程池简单分析

java 1.5 concurrent 工具包中提供了五类线程池的创建: ExecutorService executor=Executors.newCachedThreadPool(); ExecutorService cacheExecutor=Executors.newCachedThreadPool(new TestThreadFactory()); ExecutorService fixExecutor=Executors.newFixedThreadPool(10); Executo

电厂锅炉安全:泄漏原因与预防措施整理

电厂锅炉,火电厂三大主力设备之一.在电厂生产过程中发挥了极其重要的作用,是火电厂生产所必不可少的重要设备.但是,近些年,因为锅炉安全管控不到位而造成的事故屡有发生,给电厂的财产及人员的生命造成极大威胁.分析电厂锅炉的泄漏原因,制定有针对性的应对预案,并做好检修工作,对于降低事故发生的风险,提升电厂锅炉安全性具有重大意义. 电厂锅炉泄漏原因分析 电厂锅炉泄露的原因有很多,从大的方面来看导致锅炉泄露的因素主要有锅炉自身设计问题.工作人员操作问题.锅炉内部水循环问题以及各部位受热程度问题,然而从结构来

实战Java内存泄漏问题分析 -- hazelcast2.0.3使用时内存泄漏 -- 2

hazelcast 提供了3中方法调用startCleanup: 第一种是在ConcuurentMapManager的构造函数中,通过调用node的executorManager中的ScheduledExecutorService来创建每秒执行一次cleanup操作的线程(代码如下).由于这是ConcuurentMapManager构造函数的代码,所以这种调用startCleanup的操作是默认就会有的. node.executorManager.getScheduledExecutorServ

实战Java内存泄漏问题分析 -- hazelcast2.0.3使用时内存泄漏 -- 1

公司当年有一个自己缓存集群用户session的Java library,是基于hazlcast2.0.3实现的,最近在customer site集群环境中某个blade报了Out of Memory Exception, 其他blades都正常,马上用jrockit jrcmd命令dump了堆和线程进行分析. printf "##################### heap ##################\n" su -p occas -c "/opt/jrocki

Java线程泄露的分析与处理

1. 生产环境的异常现象及初步分析 最近发现系统程序内存消耗越来越大,开始并没特别注意,就简单调了一下jvm参数.但直到前些天内存爆满,持续Full GC,这肯定出现了内存泄露. 原以为哪里出现了比较低级的错误,所以很直接想到先去看看程序是在跑哪段代码.jstack -l <pid>以后,居然有上千个线程,而且都是属于RUNNING并WAIT的状态. I/O dispatcher 125" #739 prio=5 os_prio=0 tid=0x0000000002394800 ni

深度分析内存泄漏原因,使用MAT工具检测内存泄露和性能

造成内存泄漏原因: 场景一:静态变量导致的内存泄漏 例如:mainactivity中 private static context scontext: @override protected void oncreat(bundle savedinstancestate){ ............................................. scontext=this; } 泄漏点:静态变量scontext引用,activity无法正常销毁 场景二:单例模式导致的内存泄漏

JAVA线程池的分析和使用

http://www.infoq.com/cn/articles/java-threadPool/ 1. 引言 合理利用线程池能够带来三个好处.第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗.第二:提高响应速度.当任务到达时,任务可以不需要等到线程创建就能立即执行.第三:提高线程的可管理性.线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控.但是要做到合理的利用线程池,必须对其原理了如指掌. 2. 线程池

[转]JAVA线程池的分析和使用

1. 引言 合理利用线程池能够带来三个好处. 第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗. 第二:提高响应速度.当任务到达时,任务可以不需要等到线程创建就能立即执行. 第三:提高线程的可管理性.线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控. 但是要做到合理的利用线程池,必须对其原理了如指掌. 2. 线程池的使用 线程池的创建 我们可以通过ThreadPoolExecutor来创建一个线程池. n