Java内存泄露和线程池

本文详细介绍了Java内存泄露的概念,解释了为何会出现内存泄露,并提供了避免内存泄露的实践策略。此外,深入探讨了线程池的工作原理,包括线程池的创建、配置参数的意义,以及线程池在任务过多时的饱和策略。最后,讨论了Executor框架,特别是ExecutorService和ThreadPoolExecutor的使用,以及Callable、Future和FutureTask在异步计算中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

详解java内存泄露和如何避免内存泄漏

一直以来java都占据着语言排行榜的头把交椅。这是与java的设计密不可分的,其中最令大家喜欢的不

是面向对象,而是垃圾回收机制。你只需要简单的创建对象而不需要负责释放空间,因为Java的垃圾回

收器会负责内存的回收。然而,情况并不是这样简单,内存泄露还是经常会在Java应用程序中出现。

下面我们将详细的学习什么是内存泄露,为什么会发生,以及怎样阻止内存泄露。

什么是内存泄露

内存泄露的定义:对于应用程序来说,当对象已经不再被使用,但是Java的垃圾回收器不能回收它们的

时候,就产生了内存泄露。

要理解这个定义,我们需要理解对象在内存中的状态。如下图所示,展示了哪些对象是无用对象,哪些

是未被引用的对象;上图中包含了未引用对象和引用对象。未引用对象将会被垃圾回收器回收,而引用对象却不会。未引用

对象很显然是无用的对象。然而,无用的对象并不都是未引用对象,有一些无用对象也有可能是引用对

象,这部分对象正是内存泄露的来源。

为什么内存泄露会发生

让我们用下面的例子来看看为什么会发生内存泄露。如下图所示,对象A引用对象BA的生命周期(t1-

t4)比B的生命周期(t2-t3)要长,当B在程序中不再被使用的时候,A仍然引用着B。在这种情况下,

垃圾回收器是不会回收B对象的,这就可能造成了内存不足问题,因为A可能不止引用着B对象,还可能

引用其它生命周期比A短的对象,这就造成了大量无用对象不能被回收,且占据了昂贵的内存资源。同样的,B对象也可能引用着一大堆对象,这些被B对象引用着的对象也不能被垃圾回收器回收,所有的

这些无用对象消耗了大量内存资源。

怎样阻止内存泄露

1.使用ListMap等集合时,在使用完成后赋值为null

2.使用大对象时,在用完后赋值为null

3.目前已知的jdk1.6substring()方法会导致内存泄露

4.避免一些死循环等重复创建或对集合添加元素,撑爆内存

5.简洁数据结构、少用静态集合等

6.及时的关闭打开的文件,socket句柄等

7.多关注事件监听(listeners)和回调(callbacks),比如注册了一个listener,当它不再被使用的时候,忘

了注销该listener,可能就会产生内存泄露

线程池的原理,为什么要创建线程池?创建线程池的方式;

原理:

线程池的优点

1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。

2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

线程池的创建

corePoolSize:线程池核心线程数量

maximumPoolSize:线程池最大线程数量

keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

unit:存活时间的单位

workQueue:存放任务的队列

handler:超出线程范围和队列容量的任务的处理程序

public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue<Runnable> workQueue,

RejectedExecutionHandler handler)线程池的实现原理

提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创

建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如

果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已

经满了,则交给饱和策略来处理这个任务。

线程池的源码解读

1ThreadPoolExecutorexecute()方法

2、创建线程的方法:addIfUnderCorePoolSize(command)

1 public void execute(Runnable command) {

2 if (command == null)

3 throw new NullPointerException(); //如果线程数大于等于基本线

程数或者线程创建失败,将任务加入队列

4 if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))

{ //线程池处于运行状态并且加入队列成功

5 if (runState == RUNNING && workQueue.offer(command)) {

6 if (runState != RUNNING || poolSize == 0)

7 ensureQueuedTaskHandled(command);

8 } //线程池不处于运行状态或者加入队列失败,则创建线程(创建的

是非核心线程)

9 else if (!addIfUnderMaximumPoolSize(command)) //

创建线程失败,则采取阻塞处理的方式

10 reject(command); // is shutdown or saturated

11 }

12 }

1 private boolean addIfUnderCorePoolSize(Runnable firstTask) {2 Thread t = null;

3 final ReentrantLock mainLock = this.mainLock;

4 mainLock.lock();

5 try {

6 if (poolSize < corePoolSize && runState == RUNNING)

7 t = addThread(firstTask);

8 } finally {

9 mainLock.unlock();

10 }

11 if (t == null)

12 return false;

13 t.start();

14 return true;

15 }

我们重点来看第7行:

1 private Thread addThread(Runnable firstTask) {

2 Worker w = new Worker(firstTask);

3 Thread t = threadFactory.newThread(w);

4 if (t != null) {

5 w.thread = t;

6 workers.add(w);

7 int nt = ++poolSize;

8 if (nt > largestPoolSize)

9 largestPoolSize = nt;

10 }

11 return t;

12 }

这里将线程封装成工作线程worker,并放入工作线程组里,worker类的方法run方法:

public void run() {

try {

Runnable task = firstTask;

firstTask = null;

while (task != null || (task = getTask()) != null) {

runTask(task);

task = null;

}

} finally {

workerDone(this);

}

}

worker在执行完任务后,还会通过getTask方法循环获取工作队里里的任务来执行。

我们通过一个程序来观察线程池的工作原理:

1、创建一个线程

1 public class ThreadPoolTest implements Runnable

2 {

3 @Override

4 public void run()

5 {6 try

7 {

8 Thread.sleep(300);

9 }

10 catch (InterruptedException e)

11 {

12 e.printStackTrace();

13 }

14 }

15 }

2、线程池循环运行16个线程:

1 public static void main(String[] args)

2 {

3 LinkedBlockingQueue<Runnable> queue =

4 new LinkedBlockingQueue<Runnable>(5);

5 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 60,

TimeUnit.SECONDS, queue);

6 for (int i = 0; i < 16 ; i++)

7 {

8 threadPool.execute(

9 new Thread(new ThreadPoolTest(), "Thread".concat(i + "")));

10 System.out.println("线程池中活跃的线程数: " +

threadPool.getPoolSize());

11 if (queue.size() > 0)

12 {

13 System.out.println("----------------队列中阻塞的线程数" +

queue.size());

14 }

15 }

16 threadPool.shutdown();

17 }

执行结果:

线程池中活跃的线程数: 1

线程池中活跃的线程数: 2

线程池中活跃的线程数: 3

线程池中活跃的线程数: 4

线程池中活跃的线程数: 5

线程池中活跃的线程数: 5

----------------队列中阻塞的线程数1

线程池中活跃的线程数: 5

----------------队列中阻塞的线程数2

线程池中活跃的线程数: 5

----------------队列中阻塞的线程数3

线程池中活跃的线程数: 5

----------------队列中阻塞的线程数4

线程池中活跃的线程数: 5

----------------队列中阻塞的线程数5

线程池中活跃的线程数: 6

----------------队列中阻塞的线程数5

线程池中活跃的线程数: 7

----------------队列中阻塞的线程数5从结果可以观察出:

1、创建的线程池具体配置为:核心线程数量为5个;全部线程数量为10个;工作队列的长度为5

2、我们通过queue.size()的方法来获取工作队列中的任务数。

3、运行原理:

刚开始都是在创建新的线程,达到核心线程数量5个后,新的任务进来后不再创建新的线程,而是将任

务加入工作队列,任务队列到达上线5个后,新的任务又会创建新的普通线程,直到达到线程池最大的线

程数量10个,后面的任务则根据配置的饱和策略来处理。我们这里没有具体配置,使用的是默认的配置

AbortPolicy:直接抛出异常。

当然,为了达到我需要的效果,上述线程处理的任务都是利用休眠导致线程没有释放!!!

RejetedExecutionHandler:饱和策略

当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进

行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:

1AbortPolicy:直接抛出异常

2CallerRunsPolicy:只用调用所在的线程运行任务

3DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

4DiscardPolicy:不处理,丢弃掉。

我们现在用第四种策略来处理上面的程序:

线程池中活跃的线程数: 8

----------------队列中阻塞的线程数5

线程池中活跃的线程数: 9

----------------队列中阻塞的线程数5

线程池中活跃的线程数: 10

----------------队列中阻塞的线程数5

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task

Thread[Thread15,5,main] rejected from

java.util.concurrent.ThreadPoolExecutor@232204a1[Running, pool size = 10, active

threads = 10, queued tasks = 5, completed tasks = 0]

at

java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPool

Executor.java:2047)

at

java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)

at

java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)

at test.ThreadTest.main(ThreadTest.java:17)

1 public static void main(String[] args)

2 {

3 LinkedBlockingQueue<Runnable> queue =

4 new LinkedBlockingQueue<Runnable>(3);

5 RejectedExecutionHandler handler = new

ThreadPoolExecutor.DiscardPolicy();

6

7 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60,

TimeUnit.SECONDS, queue,handler);

8 for (int i = 0; i < 9 ; i++)

9 {10 threadPool.execute(

11 new Thread(new ThreadPoolTest(), "Thread".concat(i + "")));

12 System.out.println("线程池中活跃的线程数: " +

threadPool.getPoolSize());

13 if (queue.size() > 0)

14 {

15 System.out.println("----------------队列中阻塞的线程数" +

queue.size());

16 }

17 }

18 threadPool.shutdown();

19 }

执行结果:

线程池中活跃的线程数: 1

线程池中活跃的线程数: 2

线程池中活跃的线程数: 2

----------------队列中阻塞的线程数1

线程池中活跃的线程数: 2

----------------队列中阻塞的线程数2

线程池中活跃的线程数: 2

----------------队列中阻塞的线程数3

线程池中活跃的线程数: 3

----------------队列中阻塞的线程数3

线程池中活跃的线程数: 4

----------------队列中阻塞的线程数3

线程池中活跃的线程数: 5

----------------队列中阻塞的线程数3

线程池中活跃的线程数: 5

----------------队列中阻塞的线程数3

这里采用了丢弃策略后,就没有再抛出异常,而是直接丢弃。在某些重要的场景下,可以采用记录日志

或者存储到数据库中,而不应该直接丢弃。

设置策略有两种方式:

1

RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60,

TimeUnit.SECONDS, queue,handler);

2

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60,

TimeUnit.SECONDS, queue);

threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

JAVA线程池原理详解二 

Executor框架的两级调度模型

HotSpot VM的模型中,JAVA线程被一对一映射为本地操作系统线程。JAVA线程启动时会创建一个本

地操作系统线程,当JAVA线程终止时,对应的操作系统线程也被销毁回收,而操作系统会调度所有线程

并将它们分配给可用的CPU

在上层,JAVA程序会将应用分解为多个任务,然后使用应用级的调度器(Executor)将这些任务映射成

固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

Executor框架类图

在前面介绍的JAVA线程既是工作单元,也是执行机制。而在Executor框架中,我们将工作单元与执行

机制分离开来。RunnableCallable是工作单元(也就是俗称的任务),而执行机制由Executor来提

供。这样一来Executor是基于生产者消费者模式的,提交任务的操作相当于生成者,执行任务的线程相

当于消费者。

1、从类图上看,Executor接口是异步任务执行框架的基础,该框架能够支持多种不同类型的任务执行

策略。

Executor接口就提供了一个执行方法,任务是Runnbale类型,不支持Callable类型。

2ExecutorService接口实现了Executor接口,主要提供了关闭线程池和submit方法:

public interface Executor {

void execute(Runnable command);

}public interface ExecutorService extends Executor {

List<Runnable> shutdownNow();

boolean isTerminated();

<T> Future<T> submit(Callable<T> task);

}

另外该接口有两个重要的实现类:ThreadPoolExecutorScheduledThreadPoolExecutor

其中ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务;而

ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行任务,或者定期执行命令。

在上一篇文章中,我是使用ThreadPoolExecutor来通过给定不同的参数从而创建自己所需的线程池,但

是在后面的工作中不建议这种方式,推荐使用Exectuors工厂方法来创建线程池

这里先来区别线程池和线程组(ThreadGroupThreadPoolExecutor)这两个概念:

a、线程组就表示一个线程的集合。

b、线程池是为线程的生命周期开销问题和资源不足问题提供解决方案,主要是用来管理线程。

Executors可以创建3种类型的ThreadPoolExecutorSingleThreadExecutorFixedThreadExecutor

CachedThreadPool

aSingleThreadExecutor:单线程线程池

ExecutorService threadPool = Executors.newSingleThreadExecutor();

public static ExecutorService newSingleThreadExecutor() {

return new FinalizableDelegatedExecutorService

(new ThreadPoolExecutor(1, 1,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>()));

}

我们从源码来看可以知道,单线程线程池的创建也是通过ThreadPoolExecutor,里面的核心线程数和线

程数都是1,并且工作队列使用的是无界队列。由于是单线程工作,每次只能处理一个任务,所以后面所

有的任务都被阻塞在工作队列中,只能一个个任务执行。

bFixedThreadExecutor:固定大小线程池

ExecutorService threadPool = Executors.newFixedThreadPool(5);

public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>());

}

这个与单线程类似,只是创建了固定大小的线程数量。

cCachedThreadPool:无界线程池无界线程池意味着没有工作队列,任务进来就执行,线程数量不够就创建,与前面两个的区别是:空闲

的线程会被回收掉,空闲的时间是60s。这个适用于执行很多短期异步的小程序或者负载较轻的服务

器。

CallableFutureFutureTash详解

CallableFuture是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable

口的类与实现Runnable的类都是可以被线程执行的任务。

三者之间的关系:

CallableRunnable封装的异步运算任务。

Future用来保存Callable异步运算的结果

FutureTask封装Future的实体类

1CallableRunnbale的区别

aCallable定义的方法是call,而Runnable定义的方法是run

bcall方法有返回值,而run方法是没有返回值的。

ccall方法可以抛出异常,而run方法不能抛出异常。

2Future

Future表示异步计算的结果,提供了以下方法,主要是判断任务是否完成、中断任务、获取任务执行结

3FutureTask

可取消的异步计算,此类提供了对Future的基本实现,仅在计算完成时才能获取结果,如果计算尚未完

成,则阻塞get方法。

ExecutorService threadPool = Executors.newCachedThreadPool();

public static ExecutorService newCachedThreadPool() {

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

60L, TimeUnit.SECONDS,

new SynchronousQueue<Runnable>());

}

1 public interface Future<V> {

2

3 boolean cancel(boolean mayInterruptIfRunning);

4

5 boolean isCancelled();

6

7 boolean isDone();

8

9 V get() throws InterruptedException, ExecutionException;

10

11 V get(long timeout, TimeUnit unit)

12 throws InterruptedException, ExecutionException, TimeoutException;

13 }

public class FutureTask<V> implements RunnableFuture<V>

public interface RunnableFuture<V> extends Runnable, Future<V>FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个

任务交给Executor来执行,还可以通过Thread来创建一个线程。

CallableFutureTask

定义一个callable的任务:

执行结果:

1 public class MyCallableTask implements Callable<Integer>

2 {

3 @Override

4 public Integer call()

5 throws Exception

6 {

7 System.out.println("callable do somothing");

8 Thread.sleep(5000);

9 return new Random().nextInt(100);

10 }

11 }

1 public class CallableTest

2 {

3 public static void main(String[] args) throws Exception

4 {

5 Callable<Integer> callable = new MyCallableTask();

6 FutureTask<Integer> future = new FutureTask<Integer>(callable);

7 Thread thread = new Thread(future);

8 thread.start();

9 Thread.sleep(100);

10 //尝试取消对此任务的执行

11 future.cancel(true);

12 //判断是否在任务正常完成前取消

13 System.out.println("future is cancel:" + future.isCancelled());

14 if(!future.isCancelled())

15 {

16 System.out.println("future is cancelled");

17 }

18 //判断任务是否已完成

19 System.out.println("future is done:" + future.isDone());

20 if(!future.isDone())

21 {

22 System.out.println("future get=" + future.get());

23 }

24 else

25 {

26 //任务已完成

27 System.out.println("task is done");

28 }

29 }

30 }

callable do somothing

future is cancel:true

future is done:true

task is done这个DEMO主要是通过调用FutureTask的状态设置的方法,演示了状态的变迁。

a、第11行,尝试取消对任务的执行,该方法如果由于任务已完成、已取消则返回false,如果能够取消

还未完成的任务,则返回true,该DEMO中由于任务还在休眠状态,所以可以取消成功。

b、第13行,判断任务取消是否成功:如果在任务正常完成前将其取消,则返回true

c、第19行,判断任务是否完成:如果任务完成,则返回true,以下几种情况都属于任务完成:正常终

止、异常或者取消而完成。

我们的DEMO中,任务是由于取消而导致完成。

d、在第22行,获取异步线程执行的结果,我这个DEMO中没有执行到这里,需要注意的是,

future.get方法会阻塞当前线程, 直到任务执行完成返回结果为止。

CallableFuture

执行结果:

future.cancel(true);

System.out.println("future is cancel:" + future.isCancelled());

System.out.println("future is done:" + future.isDone());

System.out.println("future get=" + future.get());

public class CallableThread implements Callable<String>

{

@Override

public String call()

throws Exception

{

System.out.println("进入Call方法,开始休眠,休眠时间为:" +

System.currentTimeMillis());

Thread.sleep(10000);

return "今天停电";

}

public static void main(String[] args) throws Exception

{

ExecutorService es = Executors.newSingleThreadExecutor();

Callable<String> call = new CallableThread();

Future<String> fu = es.submit(call);

es.shutdown();

Thread.sleep(5000);

System.out.println("主线程休眠5秒,当前时间" + System.currentTimeMillis());

String str = fu.get();

System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" +

System.currentTimeMillis());

}

}这里的future是直接扔到线程池里面去执行的。由于要打印任务的执行结果,所以从执行结果来看,主

线程虽然休眠了5s,但是从Call方法执行到拿到任务的结果,这中间的时间差正好是10s,说明get方法

会阻塞当前线程直到任务完成。

**通过FutureTask也可以达到同样的效果:

以上的组合可以给我们带来这样的一些变化:

如有一种场景中,方法A返回一个数据需要10s,A方法后面的代码运行需要20s,但是这20s的执行过程

中,只有后面10s依赖于方法A执行的结果。如果与以往一样采用同步的方式,势必会有10s的时间被浪

费,如果采用前面两种组合,则效率会提高:

1、先把A方法的内容放到Callable实现类的call()方法中

2、在主线程中通过线程池执行A任务

3、执行后面方法中10秒不依赖方法A运行结果的代码

4、获取方法A的运行结果,执行后面方法中10秒依赖方法A运行结果的代码

这样代码执行效率一下子就提高了,程序不必卡在A方法处。

创建线程池的几种方式:

ThreadPoolExecutorThreadScheduledExecutorForkJoinPool

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心是凉的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值