Java回收机制总结

Java回收机制

如何确定“垃圾”

既然是垃圾回收机制,第一步肯定是要确定垃圾,知道了垃圾便可以进行回收。但是如何确定垃圾呢?什么是垃圾呢?

什么是“垃圾”

首先要明白什么是“垃圾”,垃圾回收机制是回收堆内存中的对象(具体的内存划分可以看:),对于栈中的对象是不需要回收机制去考虑的。在Java中堆内存中的对象是通过和栈内存中的引用相互关联,才被利用的。既然是对堆内存的回收,并且堆内存中存储的都是引用对象的实体,所以回收的就是没有被任何一个引用所关联的实体对象。

因此,“垃圾”实质上指的是java虚拟机中堆内存里没有被引用到的,永远也不会访问到的实体对象。

1.  引用计数法

既然明白了什么是“垃圾”,那么该如何找垃圾呢?很显然,通过定义可知,没有引用的对象就是垃圾,所以可以通过查看对象的当前被引用的数量来判断是否应该回收。

比如当一个引用关联了实体对象后,就在对象的引用数加1,若取消引用则减1.当引用数为0时即被系统回收。看似简单的方法,却十分高效,但是却存在一个弊端——循环引用。假如两个对象互相引用时,各自的引用数都为1,可是对象是永远都访问不到的,如下代码:

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;

}

}

 

class MyObject{

public Object object = null;

}

 

最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于计数不为0(因为object1中的object和object2中的object指向对方),导致对象始终不会被回收。

 

 

所以在JAVA中并没有采用这种方法。(Python采用的是引用计数法)

2.  可达性分析算法

可以作为GC Root 引用点的是:

  JavaStack中的引用的对象。

  方法区中静态引用指向的对象。

  方法区中常量引用指向的对象。

  Native方法中JNI引用的对象。

GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。

目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

 

 

.GC判断为垃圾的对象一定会回收吗?

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,放入回收集合中,并且会对集合中的对象进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”(没有必要执行finalize方法)。(即意味着直接回收)

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了(放入回收集合,等待被回收)。

代码示例:

  1. public class FinalizeEscapeGC {
  2.     public static FinalizeEscapeGC SAVE_HOOK = null;
  3.  
  4.     public void isAlive() {
  5.         System.out.println("yes,i am still alive:)");
  6.     }
  7.  
  8.     @Override
  9.     protected void finalize() throws Throwable {
  10.     super.finalize();
  11. System.out.println("finalize mehtod executed!");
  12. FinalizeEscapeGC.SAVE_HOOK = this;
  13. }

14.

  1. public static void main(String[] args) throws Throwable {
  2. SAVE_HOOK = new FinalizeEscapeGC();
  3. // 对象第一次成功拯救自己
  4. SAVE_HOOK = null;
  5. System.gc();
  6. // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
  7. Thread.sleep(500);
  8. if (SAVE_HOOK != null) {
  9. SAVE_HOOK.isAlive();
  10. } else {
  11. System.out.println("no,i am dead:(");
  12. }
  13. // 下面这段代码与上面的完全相同,但是这次自救却失败了
  14. SAVE_HOOK = null;
  15. System.gc();
  16. // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
  17. Thread.sleep(500);
  18. if (SAVE_HOOK != null) {
  19. SAVE_HOOK.isAlive();
  20. } else {
  21.           System.out.println("no,i am dead:(");
  22. }
  23. }

38.}

39.

40.运行结果:

41.

42.finalize mehtod executed!

43.yes,i am still alive:

44.no,i am dead:

SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。因为finalize()方法已经被虚拟机调用过,虚拟机都视为“没有必要执行”。(即意味着直接回收)

finalize()方法

大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖(重写)了finalize方法,若未覆盖,则直接将其回收。否则(即覆盖了的话),并且对象的finalize方法未被执行过(如果已经被执行过了就直接被GC回收了,就像上面的例子),则将其放入F-Queue队列,由一个低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

垃圾回收器准备释放内存的时候,会先调用finalize()。

      (1).对象不一定会被回收。

       (2).垃圾回收不是析构函数。

       (3).垃圾回收只与内存有关。

       (4).垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收

          之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候Java允许在类中定义一个finalize()方法。

      特殊的区域例如:1)由于在分配内存的时候可能采用了类似C语言的做法,而非JAVA的通常new做法。这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free()方法是在C/C++中的函数,所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。

 

      换言之,finalize()的主要用途是释放一些其他做法开辟的内存空间,以及做一些清理工作。因为在JAVA中并没有提够像“析构”函数或者类似概念的函数,要做一些类似清理工作的时候,必须自己动手创建一个执行清理工作的普通方法,也就是override Object这个类中的finalize()方法。例如,假设某一个对象在创建过程中会将自己绘制到屏幕上,如果不是明确地从屏幕上将其擦出,它可能永远都不会被清理。如果在finalize()加入某一种擦除功能,当GC工作时,finalize()得到了调用,图像就会被擦除。要是GC没有发生,那么这个图像就会被一直保存下来。

        一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。

       在普通的清除工作中,为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这与C++"析构函数"的概念稍有抵触。在C++中,所有对象都会破坏(清除)。或者换句话说,所有对象都"应该"破坏。若将C++对象创建成一个本地对象,比如在堆栈中创建(在Java中是不可能的,Java都在堆中),那么清除或破坏工作就会在"结束花括号"所代表的、创建这个对象的作用域的末尾进行。若对象是用new创建的(类似于Java),那么当程序员调用C++的 delete命令时(Java没有这个命令),就会调用相应的析构函数。若程序员忘记了,那么永远不会调用析构函数,我们最终得到的将是一个内存"漏洞",另外还包括对象的其他部分永远不会得到清除。
  相反,Java不允许我们创建本地(局部)对象--无论如何都要使用new。但在Java中,没有"delete"命令来释放对象,因为垃圾回收器会帮助我们自动释放存储空间。所以如果站在比较简化的立场,我们可以说正是由于存在垃圾回收机制,所以Java没有析构函数。然而,随着以后学习的深入,就会知道垃圾收集器的存在并不能完全消除对析构函数的需要,或者说不能消除对析构函数代表的那种机制的需要(原因见下一段。另外finalize()函数是在垃圾回收器准备释放对象占用的存储空间的时候被调用的,绝对不能直接调用finalize()所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java中的一个方法。它等价于C++的析构函数,只是没后者方便。

      在C++中所有的对象运用delete()一定会被销毁,而JAVA里的对象并非总会被垃圾回收器回收。In another word, 1 对象可能不被垃圾回收,2 垃圾回收并不等于“析构”,3 垃圾回收只与内存有关。也就是说,并不是如果一个对象不再被使用,是不是要在finalize()中释放这个对象中含有的其它对象呢?不是的。因为无论对象是如何创建的,垃圾回收器都会负责释放那些对象占有的内存。

典型的垃圾回收算法

1. 标记-清除算法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图:

 

 

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

2. 复制算法(Copying)

为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:

 

 

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。

3. 标记-整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

 

 

2.4. 分代收集算法(Generational Collection)

分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。

 

而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。

另外,不要忘记在Java基础:Java虚拟机(JVM)中提到过的处于方法区的永生代(Permanet Generation)。它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了(因为此时From Space被清空了,所以它和To Space 角色互换了),如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。

下面是对分代算法的详细描述:

 

 

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

年轻代(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 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

 

典型的垃圾收集器

垃圾收集算法是垃圾收集器的理论基础,而垃圾收集器就是其具体实现。下面介绍HotSpot虚拟机提供的几种垃圾收集器。

3.1. Serial/Serial Old

最古老的收集器,是一个单线程收集器,用它进行垃圾回收时,必须暂停所有用户线程。Serial是针对新生代的收集器,采用Copying算法;而Serial Old是针对老生代的收集器,采用Mark-Compact算法。优点是简单高效,缺点是需要暂停用户线程。

3.2. ParNew

Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。

3.3. Parallel Scavenge

新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。

3.4. Parallel Old

Parallel Scavenge的老生代版本,采用Mark-Compact算法和多线程。

3.5. CMS

Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。

3.6. G1

G1(Garbage First)收集器技术的前沿成果,是面向服务端的收集器,能充分利用CPU和多核环境。是一款并行与并发收集器,它能够建立可预测的停顿时间模型。

 

本文是一篇总结性的文章,借鉴了下面网站的内容

https://blog.csdn.net/mine_song/article/details/63251367

https://www.cnblogs.com/andy-zcx/p/5522836.html

https://www.cnblogs.com/cielosun/p/6674431.html

https://www.i3geek.com/archives/1220

posted on 2018-10-09 18:54  左青龙,右白虎  阅读(265)  评论(0编辑  收藏  举报

导航