深入理解 Java 垃圾回收机制

时间:2022-12-20 00:02:53

首先需要明白的是:Java 垃圾回收器回收的是什么?

内存。而比如文件句柄、数据库连接这些资源则需要我们手工释放。可以使用 JDK 7 中的 try-with-resources 完成资源的释放。
一般的回收的大部分是我们在堆中创建的类实例和数组。这里的类实例也包括Class类产生的类实例,在JDK7以后类实例也保存在堆中。以前HotSpot实现这个类实例保存在在永久代。而GC主要回收的内存也是这块堆内存。

堆内存模型

堆内存由垃圾回收器的自动内存管理系统回收。
堆内存分为两大部分:新生代和老年代。比例为1:2。
老年代主要存放应用程序中生命周期长的存活对象。
新生代又分为三个部分:一个Eden区和两个Survivor区,比例为8:1:1。
Eden区存放新生的对象。
Survivor存放每次垃圾回收后存活的对象。

垃圾回收算法

用途:

  • 发现无用的对象
  • 回收被无用对象占用的内存空间,使该空间可被程序再次使用

垃圾对象的判定

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1,当引用失效时,计数器值就减1,任何时刻计数器都为 0 的对象就是不可能再被使用的。
引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择,当 Java 语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。

class MyObject {                  
    private Object object;        
}                                 
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();

object1.object = object2; 
object2.object = object1; 

object1 = null;// 如果使用引用计数算法,此时并不会导致原来
object2 = null;// object1 和 object2 指向的对象被回收 

可达性分析法

这种算法的基本思想是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。
在 Java 语言里,可作为 GC Roots 的包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中 JNI(Native 方法)的引用对象。

实际上,在可达性分析算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法,或 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行 finalize()方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后由一条由虚拟机自动建立的、低优先级的 Finalizer 线程去执行 finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize()方法最多只会被系统自动调用一次),稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果要在 finalize()方法中成功拯救自己,只要在 finalize()方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

深入理解 Java 垃圾回收机制

垃圾收集算法

判定出垃圾对象之后,便可以进行垃圾回收了。

标记—清除算法

标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。

优点:简单,容易实现。
缺点:容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

标记—清除算法的执行情况如下图所示:

深入理解 Java 垃圾回收机制

标记—整理算法

该算法标记阶段和标记—清除算法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
所以,特别适用于存活对象多,回收对象少的情况下。

深入理解 Java 垃圾回收机制

复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
优缺点就是,实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
从算法原理我们可以看出,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

深入理解 Java 垃圾回收机制

分代收集算法

分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的的特点综合而成。这种综合是考虑到java的语言特性的。
这里重复一下两种老算法的适用场景:

复制算法:适用于存活对象很少。回收对象多
标记整理算法: 适用用于存活对象多,回收对象少

刚好互补!不同类型的对象生命周期决定了更适合采用哪种算法。
于是,我们根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
这就是分代回收算法。

内存的分配策略

  • 对象优先在 Eden 分配。
  • 大对象直接进入老年代。
  • 长期存活的对象将进入老年代。

垃圾回收策略

  • 新生代 GC(Minor GC):发生在新生代的垃圾收集动作,因为 Java 对象大多都具有朝生夕灭的特性,因此Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC(Major GC/Full GC):发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次 Minor GC。由于老年代中的对象生命周期比较长,因此 Major GC 并不频繁,一般都是等待老年代满了后才进行 Full GC,而且其速度一般会比 Minor GC 慢 10 倍以上。另外,如果分配了 Direct Memory,在老年代中进行 Full GC时,会顺便清理掉 Direct Memory 中的废弃对象。

垃圾收集器


参考:
http://www.cnblogs.com/sunniest/p/4575144.html
https://blog.csdn.net/justloveyou_/article/details/71216049
http://www.infoq.com/cn/news/2017/03/garbage-collection-algorithm
http://jayfeng.com/2016/03/11/理解Java垃圾回收机制/

深入理解Java虚拟机