我眼中的JVM(三)

继续分析类的销毁过程,垃圾回收机制。垃圾收集不是Java的伴生产物,GC的历史比Java更久远。而使用GC时我们需要关注哪些问题:

 1、哪些对象需要回收?

     在堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,首先要做的就是确认这些对象中哪些还在使用中,那些已经不再使用。GC思想中提出了2种算法:

  1)  引用计数算法:  为每一个对象增加一个引用计数器,每当引用该对象时,计数器就加1;当引用失效时,计数器就减1;而如果计数器为0的就表示对象不再被使用,可以进行回收。但是这种算法会出现一个问题,如果当2个对象 objA和objB直接出现互相引用时, 那么他们的计数器永远都不会为0,这样无法通过引用计数器算法来回收。

 2)可达性分析算法:目前主流的程序语言(Java、C#等)都是使用这种算法的,它的基本思路是通过一系列"GC Roots"的对象作为起始点,从这些点开始向下搜索,这个路径称为引用链,而当一个对象没有任何引用链相连,证明此对象已经不再使用,GC将进行回收。

Java中作为GC Roots的对象包括

  • 虚拟机栈中局部变量表中的引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本机方法栈中JNI引用的对象

无论哪种算法都与"引用"有关,而关于引用在JDK 1.2之后,Java对引用的概念进行了扩充,分为:

  • 强引用   new实例出的,只要强引用存在,GC永远不会回收
  • 软引用  当要发生内存溢出异常时,才会进行回收的引用类型。
  • 弱引用  只能生存到下一次垃圾回收发生之前的引用类型
  • 虚引用  当被回收时会收到一个系统通知。

如何回收?

   在研究这个问题之前,我们来先研究一下JMM,Java内存模型。

我眼中的JVM(三)

  在JDK1.8之前,分为新生代、老年代、永久代,其中新生代与老年代在堆内存中,永久代在方法区中。在JDK1.8之后为了解决永久代内存溢出问题变为:

我眼中的JVM(三)

其中移除了永久代,而在直接内存中增加了Meta Space来替代永久代。那么具体每个代中是如何进行垃圾回收的?

在新生代中有Serial收集器、Parnew收集器、Parallel scavenge收集器。其中新生代有一个特点,在新生代中的绝大部分对象都是朝生夕死的,所以按这种特点在新生代中又分为伊甸园(eden)、from Survivor、to Survivor 三个部分,当实例一个新对象时,将在eden中分配内存,当执行垃圾回收后将存活下的对象放到from Survivor区,当eden内存空间再次变满触发GC后,会对eden与from Survivor执行垃圾回收,将存活的对象放入到to Survivor,这样就形成一个特点就是新生代的垃圾收集器都是采用的复制算法。其中Serial收集器是单线程收集器,Parnew收集器是多线程收集器,Parallel scavenge收集器也是多线程,但是它的关注点是追求高吞吐量,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

在老年代中有Serial old收集器,Parallel  old收集器,CMS收集器。其中Serial old收集器,Parallel  old收集器与新生代的一样,不过使用的垃圾回收算法为 标记-整理算法。而CMS收集器是一款追求最短回收停顿时间为目标的收集器,它执行的是标记-清除算法。

CMS回收整个过程分为四个步骤:

1)初始标记  这个阶段会进行stop the world。会启动一个GC线程,标记一下GC Roots能直接关联的对象。

2)并发标记   这个阶段用户线程会正常执行,GC线程会按上一步中标记的对象继续按引用关系查找关联的对象。

3)重新标记  这个阶段还会stop the world。 这个阶段会开启多个GC线程来处理在第二步时,用户线程工作时产生的对象进行标记记录。

4)并发清楚  这个阶段用户线程又可以正常执行,而GC线程开始执行清除过程。


至此,针对JVM的基本内容学习到此结束,当然其中还有很多的东西没有介绍,因为其中的内容和知识点都比较的多,而进行知识分享的篇幅又过大,所以在这里只是按照一个类的生命周期来做为一个链路进行学习。