JVM垃圾回收简介

166 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情

一、JVM内存模型

image.png

image.png

1、程序计数器(线程私有)

当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成, 改区域是JAVA虚拟机规范中唯一没有规定任何OOM情况的区域。

2、Java虚拟机栈(线程私有)

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息, 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。栈深度过大抛出StackOverflowError异常,栈内存申请失败和动态扩展失败抛出OOM异常。

3、本地方法栈(线程私有)

与虚拟机栈所发挥的作用是非常相似的,其区别本地方法栈为虚拟机使用到的本地(Native)方法服务。

4、Java堆(线程共享)

几乎所有的对象实例以及数组都是在堆上分配的,因此Java堆是垃圾收集器管理的内存区域。

5、方法区/元空间(线程共享)

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

二、如何定位垃圾?

1、引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。该算法原理简单,判定效率较高,但无法解决循环引用造成内存泄露的问题。

2、可达性分析算法

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

image.png 固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

当存在非收集区域对象引用收集区域对象时,收集器普遍使用了名为“记忆集”(Remembered Set)的数据结构,以避免将整个老年代或者Region加入GC Roots扫描范围,RSet有三种精度:1、字粒度;2、对象粒度;3、卡粒度。其中卡粒度是CMS和G1采用的方案,其实现称为Card Table。RSet只能保证应该活着的对象都会活着,无法保证已经死去的对象都被会收集。

image.png

并发情况下的可达性分析解决方案:三色标记

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

image.png 当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

因此,要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新和原始快照(SATB)。增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。原始快照要破坏的是第二个条件,S抽象的说就是在一次GC开始的时候是活的对象就被认为是活的,此时的对象图形成一个逻辑“快照”;然后在GC过程中新分配的对象都当作是活的。其它不可到达的对象就是死的了

三、垃圾收集算法

1、标记-清除算法

image.png

2、标记-复制算法

image.png

3、标记-整理算法

image.png

四、垃圾收集器

image.png

1、Serial收集器、Serial Old收集器

采用复制算法,单线程处理,会引发STW问题,但简单高效,额外内存消耗较少,单核处理器环境下,垃圾收集的效率较高。Serial Old是Serial的老年代版本,采用标记整理算法。

2、ParNew收集器

Serial收集器的多线程并行版本。CMS作为老年代收集器时,新生代只能采用ParNew收集器。

3、Parallel Scavenge收集器、Parallel Old收集器

基于标记-复制算法的新生代收集器,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput),吞吐量 = 用户代码运行时间 / (用户代码运行时间 + 垃圾收集时间)

4、CMS收集器

是一种以获取最短回收停顿时间为目标的标记-清除垃圾收集器。为什么没用采用标记-整理算法?因为如果需要移动对象,是需要STW(没有采用read barrier)的,同时老年代存活率高,总体算下来使用标记-整理算法并不划算。CMS整个回收过程分为4个步骤:

  • 初始标记,标记GC Roots直接关联的对象,需要STW,但是速度很快。
  • 并发标记:GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程。
  • 重新标记:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。需要STW。
  • 并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

CMS收集器采用增量更新算法何保证收集线程与用户线程互不干扰地运行,即用户线程改变对象引用关系时,其原本的对象图结构不会被打破。

5、Garbage First收集器

开创了收集器面向局部收集的设计思路和基于Region(单次回收的最小单元)的内存布局形式,为了实现“停顿时间模型”(即指定N秒内,垃圾收集时间不超过M秒),G1收集器可以面向堆内存任何部分来组成回收集(CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。

G1收集器收集步骤大概如下:

  • 初始标记(STW):标记一下GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值。
  • 并发标记:GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。本阶段与用户线程同时进行。
  • 最终标记(STW):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
  • 筛选回收(STW)::对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。

在并发标记的过程中,用户线程执行的时候不仅修改了对象引用关系,还新分配了新对象,G1 是如何找到并处理这些对象的呢?

image.png