《深入理解Java虚拟机》学习笔记(2)--垃圾收集器

一、Jvm垃圾回收区域
程序计数器

虚拟机栈

本地方法栈
三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。每一个栈帧中分配多少内存基本是在类结构确定下来时就已知的,因此这三个区域的内存分配和回收都具有确定性,在这三个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而
java堆

方法区
不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,我们只有在程序运行时才能知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集器所关注的是这部分内存。
二、垃圾回收的判定方法
怎么判断哪些对象是已经“死去”的对象呢?也就是说怎么判定一个对象是否需要回收呢?
1.引用计数数
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。存在循环引用的问题。Java语言并没有选用引用计数器算法来管理内存。
2.可达性分析算法(引用链算法)
通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可引用的,所以它们将会被判定为可回收对象。
在Java语言中可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象;
  • 方法区中的类静态属性引用的对象;
  • 运行时常量池中的对象引用;
  • 方法区中的常量引用的对象;
  • 本地方法栈中JNI(即一般所说的native方法)引用的对象。

三、Java中的四种引用类型

  • 强引用:程序中普遍存在的,类似“String s=”hello wold””这类的引用,强引用的对象不会被回收。
  • 软件用:有用但是非必需的对象,在系统将要发生内存溢出之前会对软引用的对象进行垃圾回收,用SoftReference类实现软引用。
  • 弱引用:非必需的对象,被弱引用关联的对象只能存活到下一次垃圾收集发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。用WeakReference类来实现弱引用。
  • 虚引用:最弱的引用关系,不能通过虚引用取得对象的实例,为对象设置虚引用的唯一目的就是在这个对象被收集器回收时收到一个系统通知。用PhantomReference类来实现虚引用。

四种引用强度依次逐渐减弱。
四、方法区的回收
永久代(方法区)的垃圾收集主要回收两部分内容:
废弃常量

无用的类
。回收废弃常量与回收Java堆中的对象非常相似。以常量池中的字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象叫做“abc”的,也没有其他地方引用了这个字面量,如果这个时候发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判断一个类是否是“无用的类”需要同时满足下面三个条件:

  • 该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何对方被引用,无法在任何地方通过反射访问该类的方法。

五、垃圾对象回收的过程 1.对象是否与GC Roots有引用链?有的话则存活,不需要回收;没有的话说明是个需要回收的垃圾对象,进入步骤2; 2.是否需要执行对象的finalize()方法。当对象覆盖了finalize()方法、且finalize()方法还没有被虚拟机调用过,则需要执行,转步骤3处理;否则说明没有必要执行finalize()方法,回收之; 3.将对象放入一个F-Queue队列中,虚拟机会有一个低优先级的线程去执行对象的finalize()方法; 4.若对象在finalize()中拯救了自己—-重新与GC Roots建立引用链,则存活;否则回收之。 finalize()方法只会被系统自动调用一次,是对象避免自己被回收的最后一次机会。
六、垃圾收集算法
1.标记-清除算法(Mark-Sweep)
首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
优点:简单,是后面其他算法的基础
缺点:标记和清除的效率很低;标记清除之后会产生大量不连续的内存碎片,空间碎片太多会导致以后遇到大对象的时候无法找到足够的内存而不得不提前触发另一次垃圾收集动作。
2.复制算法
将可用内存分为两个区域,每次只使用其中一块,当使用的那一块内存用完时,将还存活的对象复制到另外一块内存中,然后把已使用过的内存空间一次清理掉。
优点:
不会产生内存碎片,实现简单,运行高效。
缺点
:将内存缩小为两块,内存使用率不高,
而且可能会需要额外的担保空间
。如果对象存活率较高时就要进行较多的复制,导致效率变低。
3.标记-整理算法(Mark-Compact)
首先标记出所有需要回收的对象,然后将所有存活的对象向一端移动,最后清理掉边界以外的内存。
优点:不会产生空间碎片,比复制算法提高了内存空间利用率。
4.分代收集算法

  • 当前商用虚拟机都采用了这种算法,根据对象的存活周期将内存划分为几块,一般是把Java堆分为新生代和老生代,根据各个年代采用适当的收集算法;
  • 新生代一般采用复制算法,老年代一搬采用标记-清理或者标记-整理进行回收;
  • 新生代中由于每次存活的对象少(新生代中约98%的对象是“朝生夕死”的),每次垃圾收集时都会发现只有少量对象存活,因此适用复制算法,只需付出少量存活对象的复制成本就可以完成收集;
  • 老年代中因为对象存活率高、没有额外空间提供分配担保,因此适用标记-清除算法或标记-整理算法。

七、HotSpot的算法实现
1.枚举根节点

  • 可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),现在很多应用仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗大量时间。
  • 枚举根节点时必须停顿所有java执行线程(即Stop The World)。
  • HotSpot使用一组称为OopMap的数据结构来记录哪些地方存着对象的引用,因此枚举根节点时并不需要遍历执行上下文和全局的引用位置,可以快速且准确地完成GC Roots枚举。

2.安全点

  • 程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停;
  • 让Java线程运行到安全点时停顿下来有两种方案:抢先式中断和主动式中断;
  • 主动式中断的思想是当GC需要中断线程时不直接对线程进行操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起

3.安全区域

  • 为了处理“不执行”的程序的安全点问题,提出了安全区域来解决问题;
  • 安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域内的任何地方进行GC都是安全的。

4.垃圾收集器
(1)Serial收集器/Serial Old收集器

  • 两者都是单线程的,他在进行垃圾收集时必须暂停其他的所有线程,直到收集结束;
  • Serial收集器用于新生代,采用复制算法;Serial Old收集器用于老年代,采用标记-整理算法;
  • Serial/Serial Old收集器是虚拟机运行在Client模式下默认的新生代/老年代收集器
  • 对于单个CPU坏境来说,由于没有线程交互的开销,专心做垃圾收集,可以获得很高的单线程收集效率
  • Serial Old收集器作为年老代中使用CMS收集器的后备垃圾收集方案。

新生生采用Serial、老年代采用Serial Old收集器示意图:
《《深入理解Java虚拟机》学习笔记(2)--垃圾收集器》
(2)ParNew收集器
ParNew收集器用于新生代,是Serial收集器的多线程版本,使用多条线程进行垃圾收集,是运行在Server模式下虚拟机中首选的新生代收集器。
新生代ParNew与年老代Serial Old搭配垃圾收集过程图:
《《深入理解Java虚拟机》学习笔记(2)--垃圾收集器》
(3)Parallel Scavenge收集器

  • Parallel Scavenge收集器是一个新生代收集器,采用复制算法。
  • Parallel Scavenge收集器的特点是他的关注点与其他收集器不同。其他收集器的目标是尽可能的缩短用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控的吞吐量,因此也经常被称为“吞吐量优先”收集器。
  • 高吞吐量可以高效的利用CPU时间,尽快得完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
  • GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的;

(5)Parallel Old收集器

  • Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法;
  • 在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器

新生代采用
Parallel Scavenge收集器
与老年代采用
Parallel Old收集器
搭配垃圾收集过程图:
《《深入理解Java虚拟机》学习笔记(2)--垃圾收集器》
(6)CMS收集器

  • CMS(Concurrent Mark Sweep)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验;
  • 和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法,分为四个步骤:初始标记、并发标记、重新标记、并发清除。

a.
初始标记
:只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
b.
并发标记
:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
c.
重新标记
:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
d.
并发清除
:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
CMS收集器工作过程:
《《深入理解Java虚拟机》学习笔记(2)--垃圾收集器》
CMS的缺点:

  • CMS收集器对CPU资源非常敏感。在用户程序本来CPU负荷已经比较高的情况下,如果还要分出CPU资源用来运行垃圾收集器线程,会使得CPU负载加重。
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full FC的产生。由于CMS收集器和用户线程并发运行,因此在收集过程中不断有新的垃圾产生,这些垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好等待下一次GC时再将其清理掉,这些垃圾就称为浮动垃圾。
  • 由于CMS收集器采用了标记-清除算法,所以在回收结束时会有大量空间碎片产生,碎片过多时,在给大对象分配内存时会有很大麻烦,有时不得不提前触发一次Full GC。因此,CMS收集器提供-XX:+UseCMSCompactAtFullColloction开关参数(默认开启),用于在顶不住要进行Full GC时开启内存碎片的合并整理过程。还可以设置每执行多少次不压缩的Full GC后,跟着来一次带压缩的。

(7)G1收集器

  • G1收集器是一款面向服务端应用的垃圾收集器
  • G1收集器具备以下特点:


并行与并发

分代收集

空间整合:
从整体上来看是基于“标记-整理”算法实现的,在局部上是基于复制算法实现的,因此不会
产生内存碎片。

可预测的停顿:
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

  • G1收集器将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老生代的概念,但新生代和老生代不再是物理隔的了,他们是一部分Region的集合
  • G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。
  • 在G1收集器中,使用Remembered Set来避免全堆扫描
    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/halfclear/article/details/78116764
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞