首先明确什么时候用多线程? 多线程是提高cpu的利用率,只有当cpu空闲时间比较多情况下,才能体现出多线程的优势。 线程:线程是进程的组成单位。 系统启动一个新线程的成本比较高,因为涉及到与操作系统交互。这个时候用线程池可以很好的提高性能, 尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。 原理:(流程)线程池和数据库连接池有点类似的是,线程池在系统启动时创建大量空闲线程,程序将一个Runnable对象传给线程池, 线程池就会启动一条线程来执行该线程对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲线程, 等待执行下一个Runnable对象的run方法。 优点:使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量的并发线程时,会导致系统性能剧烈下降, 甚至导致JVM的崩溃,而线程池的最大线程参数可以控制系统中并发线程数目不超过此数目。 在JDK1.5开始,提供Java内建的线程池,JDK提供一个Executors工厂类来产生线程池,该工程类包含如下几个静态工厂方法来创建连接池: A、newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。 B、newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池 C、newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法传递参数1 D、newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务, corePoolSize 指池中所保持的线程数,即使线程是空闲的也被保存在线程池内。 E、newSingleThreadScheduiedExecutor():创建只有一条线程的线程池,它可以在指定延时后执行线程任务。 以上5个方法的前三个方法返回的是一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程。 而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延时后执行线程任务。 ExecutorService代表尽快执行线程的线程池(只要线程中有空闲的线程就立即执行线程任务)。 程序只要将一个Runnable对象或Callable对象(代表线程任务)提及给该线程池即可,该线程池就会尽快的执行任务。 ExecutorService里提供如下3个方法: A、Future<?> submit(Runnable task):将一个Runnable对象提交给指定的线程池。线程池将在有空闲线程时执行Runnable对象的代表的任务。 其中Future对象代表Runnable任务的返回值—run方法蛮腰返回值,所以Future对象将在run方法执行结束后返回null, 但可以调用Future的isDone,isCancelled方法来获得Runnable对象的执行状态 B、 Future submit(Runnable task, T reslut):将一个Runnable对象提及给指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务, result显示执行线程执行结束后的返回值,所以Future对象将在run方法执行结束后返回result C、 Future submit(Callable task):将一个Callable对象提交给指定线程池。线程池将在有空闲线程时执行Callable对象代表的任务, Future代表Callable对象里的call方法的返回值
ScheduledExecutorService代表可在指定延迟或周期性执行线程任务的线程池,它提供了如下方法:
A、ScheduledFuture<V> schedule(Callable<V> callable, long delay, Timeout unit):指定Callable任务将在delay延迟后执行
B、ScheduledFuture<V> schedule(Runnable command, long delay, Timeout unit):指定command任务将在delay延迟后执行
C、ScheduleFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
指定command任务将在delay延长后执行,而且以设定频率重复执行,也就是说,在initialDelay后开始执行,
异常在initialDelay+2* period 处重复运行,依次类推
D、ScheduleFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, Timeout unit)
创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。
如果任务的任一次运行遇到异常,就会取消后续运行。否则,只能通过程序来显示取消或终止任务
当用完一个线程池后,应该调用shutdown方法,该方法将启动线程池的关闭序列,调用了shutdown方法后线程池不再接受新的任务,
但会将以前所有一提交的任务执行完成。当线程池所有线程任务执行完毕后,池中所有线程都会死亡。另外也可以执行线程池的shutdownNow方法来关闭线程池,
该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行任务列表。
使用线程池来执行线程任务步骤:
A、调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池
B、创建Runnable实现类或是Callable实现类的实例,作为线程的执行任务
C、调用ExecutorService对象的submit方法来提交Runnable和Callable实例
D、当不想提交任务时调用ExecutorService对象的shutdown方法来关闭线程池
线程相关类 一、ThreadLocal类 在JDK5后ThreadLocal引入了泛型的支持,通过使用ThreadLocal可以简化多多线程的编程时是并发访问,使用这个工具类可以帮我们更好的实现多线程。 ThreadLocal,是Thread Local Variable(线程的局部变量)的意思。线程局部变量功能非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本, 是每一个线程都可以独立的改变自己的副本,而不会和其他线程的副本冲突。 ThreadLocal提供常用方法: A、T get():返回此线程局部变量中当前线程副本中的值 B、void remove():删除此线程局部变量中当前线程副本中的值 C、void set(T value):设置此线程局部变量中当前线程副本中的值 ThreadLocal和其他所有同步机制都是为了解决多线程中对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。 在这种情形下,该变量是多个线程共享的,所以要使用这种同步机制需要很细致的分析在什么时候对变量进行读写, 上面时候需要锁定某个对象,什么时候释放对象锁等。 在这种情况下系统并没有将这份资源复制多份,只是采用了案情机制来控制队这份资源的访问而已。 ThreadLocal就从另一个角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源复制出多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本, 从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal, 或者把该对象与现场相关的状态使用ThreadLocal保存。 ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式; 而ThreadLocal是隔离多个线程的数据共享,从根本上避免了多个线程之间共享资源(变量),也就不需要对多个线程进行同步了。 通常认为:如果需要进行多个线程之间的共享资源,已到达线程之间的通信功能,就使用步机制,如果仅仅需要隔离多个线程之间的共享冲突,就用ThreadLocal。
二、包装线程不安全集合
当用多线程操作集合时,对线程不安全的集合进行操作容易破坏集合数据的完整性。
A、<T> Collection<T> synchronizedCollection(Collection<T> c):返回指定collection对应的线程安全的collection
B、static <T> List<T> synchronizedList(List<T> list):返回指定List对应的现场安全的List对象
C、static <K, V> Map<K, V> synchronizedMap(Map<K, V> m):返回指定Map对象对应的现场安全的Map对象
D、static <T> Set<T> synchronizedSet(Set<T> s):返回指定Set对应的线程安全的Set
E、static <K, V> SortedMap<K, V> synchronizedSortedMap(SortedMap<K, V> m):返回指定SortedMap对象所对应线程安全的SortedMap对象
F、static <K, V> SortedSet<K, V> synchronizedSortedSet(SortedSet<K, V> m):返回指定SortedSet对象所对应线程安全的SortedSet 对象
使用线程安全的HashMap对象:
HashMap map = Collections.synchronizedMap(new HashMap());
注意:如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装,包装后就是线程安全的HashMap对象了。
三、线程安全的集合 在JDK5后提供了java.util.concurrent包的ConcurrentHashMap、ConcurrentLinkedQueue两个支持并发访问的集合, 它们分别代表了支持并发访问的HashMap和支持并发访问的Queue。默认都支持多线程并发写入,写入操作是线程安全的,读取不必锁定。 这两个集合采用了复杂的算法,他们是永远都锁不住的集合。 当多线程共享访问一个集合时,ConcurrentLinkedQueue最合适不过,Queue不允许为null元素。 Quee实现了多线程的高效访问,多条线程访问ConcurrentLinkedQueue集合是无需等待。 ConcurrentHashMap默认支持16条线程并发访问,当有超过16条线程并发访问就需要等待。 但可以设置concurrentLevel构造方法参数(默认16)来支持更多的线程并发数量。 与HashMap和普通集合不同的是,当我们用迭代器变量ConcurrentHashMap、ConcurrentLinkedQueue时, 如果在迭代器创建后对集合元素的修改是不会在迭代器中做出修改,也不会出现异常。 如果用Collection作为集合对象时,如果对象在创建迭代器后发生变化修改,就会引发ConcurrentModificationException
JVM优化,内存溢出,堆内存,什么时候那种内存溢出。 Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等,而Hotspot jvm的实现中,将堆内存分为了三部分,新生代,老年代,持久带,其中持久带实现了规范中规定的方法区,而内存模型中不同的部分都会出现相应的OOM错误,接下来我们就分开来讨论一下。 栈溢出(StackOverflowError) 栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。 出现这种情况,一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。 下面我们通过一段代码来模拟一下此种情况的内存溢出。 import java.util.; import java.lang.; public class OOMTest{ public void stackOverFlowMethod(){ stackOverFlowMethod(); } public static void main(String... args){ OOMTest oom = new OOMTest(); oom.stackOverFlowMethod(); } } 运行上面的代码,会抛出如下的异常: 1 2 Exception in thread "main" java.lang.StackOverflowError at OOMTest.stackOverFlowMethod(OOMTest.java:6) 堆溢出(OutOfMemoryError:java heap space) 堆内存溢出的时候,虚拟机会抛出java.lang.OutOfMemoryError:java heap space,出现此种情况的时候,我们需要根据内存溢出的时候产生的dump文件来具体分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm启动参数)。出现此种问题的时候有可能是内存泄露,也有可能是内存溢出了。 如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。 如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。 下面我们通过如下的代码来演示一下此种情况的溢出:
import java.util.*;
import java.lang.; public class OOMTest{ public static void main(String... args){ List<byte[]> buffer = new ArrayList<byte[]>(); buffer.add(new byte[101024*1024]); } } 我们通过如下的命令运行上面的代码: java -verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC OOMTest 程序输入如下的信息: 1 2 3 4 5 [GC 1180K->366K(19456K), 0.0037311 secs] [Full GC 366K->330K(19456K), 0.0098740 secs] [Full GC 330K->292K(19456K), 0.0090244 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at OOMTest.main(OOMTest.java:7) 从运行结果可以看出,JVM进行了一次Minor gc和两次的Major gc,从Major gc的输出可以看出,gc以后old区使用率为134K,而字节数组为10M,加起来大于了old generation的空间,所以抛出了异常,如果调整-Xms21M,-Xmx21M,那么就不会触发gc操作也不会出现异常了。 通过上面的实验其实也从侧面验证了一个结论:当对象大于新生代剩余内存的时候,将直接放入老年代,当老年代剩余内存还是无法放下的时候,出发垃圾收集,收集后还是不能放下就会抛出内存溢出异常了 持久带溢出(OutOfMemoryError: PermGen space) 我们知道Hotspot jvm通过持久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。当持久带溢出的时候抛出java.lang.OutOfMemoryError: PermGen space。 我在工作可能在如下几种场景下出现此问题。 使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。 如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。 一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。 我们知道Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么我们就可以通过String.intern方法来模拟一下运行时常量区的溢出.下面我们通过如下的代码来模拟此种情况:
import java.util.*;
import java.lang.*; public class OOMTest{ public static void main(String... args){ List list = new ArrayList(); while(true){ list.add(UUID.randomUUID().toString().intern()); } } } 我们通过如下的命令运行上面代码: java -verbose:gc -Xmn5M -Xms10M -Xmx10M -XX:MaxPermSize=1M -XX:+PrintGC OOMTest 运行后的输入如下图所示: 1 2 3 Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at OOMTest.main(OOMTest.java:8) 通过上面的代码,我们成功模拟了运行时常量池溢出的情况,从输出中的PermGen space可以看出确实是持久带发生了溢出,这也验证了,我们前面说的Hotspot jvm通过持久带来实现方法区的说法。 OutOfMemoryError:unable to create native thread 最后我们在来看看java.lang.OutOfMemoryError:unable to create natvie thread这种错误。 出现这种情况的时候,一般是下面两种情况导致的: 程序创建的线程数超过了操作系统的限制。对于Linux系统,我们可以通过ulimit -u来查看此限制。 给虚拟机分配的内存过大,导致创建线程的时候需要的native内存太少。我们都知道操作系统对每个进程的内存是有限制的,我们启动Jvm,相当于启动了一个进程,假如我们一个进程占用了4G的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。 线程栈总可用内存=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存 通过上面的公式我们可以看出,-Xmx 和 MaxPermSize的值越大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的unable to create native thread,那么要么我们增大进程所占用的总内存,或者减少-Xmx或者-Xss来达到创建更多线程的目的。
GC回收 一、垃圾回收机制的意义 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。 ps:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。 二、垃圾回收机制中的算法 Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。 1.引用计数法(Reference Counting Collector) 1.1算法分析 引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。 1.2优缺点 优点: 引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。 缺点: 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0. 1.3引用计数算法无法解决循环引用问题,例如: public class Main { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.object = object2; object2.object = object1; object1 = null; object2 = null; } } 最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。 2.tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep) 2.1根搜索算法
根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。 java中可作为GC Root的对象有 1.虚拟机栈中引用的对象(本地变量表) 2.方法区中静态属性引用的对象 3. 方法区中常量引用的对象 4.本地方法栈中引用的对象(Native对象) 2.2tracing算法的示意图
2.3标记-清除算法分析 标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。 3.compacting算法 或 标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。 4.copying算法(Compacting Collector)
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。 5.generation算法(Generational Collector)
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。 年轻代(Young Generation) 1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。 2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。 3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收 4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发) 年老点(Old Generation) 1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。 2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。 持久代(Permanent Generation) 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。 三.GC(垃圾收集器) 新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge 老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(复制算法) 新生代单线程收集器,标记和清理都是单线程,优点是简单高效。 Serial Old收集器(标记-整理算法) 老年代单线程收集器,Serial收集器的老年代版本。 ParNew收集器(停止-复制算法) 新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。 Parallel Scavenge收集器(停止-复制算法) 并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。 Parallel Old收集器(停止-复制算法) Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先 CMS(Concurrent Mark Sweep)收集器(标记-清理算法) 高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择 四、GC的执行机制 由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。 Scavenge GC 一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。 Full GC 对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC: 1.年老代(Tenured)被写满 2.持久代(Perm)被写满 3.System.gc()被显示调用 4.上一次GC之后Heap的各域分配策略动态变化 五、Java有了GC同样会出现内存泄露问题 1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。 Static Vector v = new Vector(); for (int i = 1; i<100; i++) { Object o = new Object(); v.add(o); o = null; } 在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。 2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。 3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。