java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaEE(j2ee), JavaME(j2me), JavaSE(j2se))的总称。
下面小编就为大家带来一篇老生常谈 java 垃圾回收算法 (必看篇)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
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.Mark-Sweep(标记 - 清除)Tracing Collector(tracing 算法)
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记 - 清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
从图中可以很容易看出标记 - 清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
3.Copying(复制)算法
为了解决 Mark-Sweep 算法的缺陷,Copying 算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
很显然,Copying 算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么 Copying 算法的效率将会大大降低。
4.Mark-Compact(标记 - 整理)算法
为了解决 Copying 算法的缺陷,充分利用内存空间,提出了 Mark-Compact 算法。该算法标记阶段和 Mark-Sweep 一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
5.Generational Collection(分代收集)算法
分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照 1:1 的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是 Mark-Compact 算法。
注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储 class 类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
垃圾收集器
新生代收集器使用的收集器: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 同样会出现内存泄露问题
静态集合类像 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. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。
来源: http://www.phperz.com/article/17/1227/356974.html