垃圾收集器(GC)

发布时间 2023-09-28 23:40:45作者: 柯南。道尔

垃圾收集器(GC)

GC(Garbage Collection):垃圾收集器,在Java内存运行时的区域中,java虚拟机栈,本地方法栈、程序计数器这三个区域的生命周期跟随线程,内存分配与回收都具备确定性,当方法结束或线程结束时,内存会跟随着回收。而java堆和方法区这两个区域有显著的不确定性,一个方法执行不同条件所需的内存不一样,因此只有运行期间才能确定创建了多少个对象,内存分配和回收是动态的,垃圾收集器(GC)所关注的正是这部分内存的管理

判断对象"死去"的方法(Java堆的垃圾收集)

垃圾收集器在对Java堆进行回收前需要确定这些对象之中哪些还"存活",哪些已经"死去"(不可能再被任何途径使用的对象)

引用计数算法

基本思路:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加一;当引用失效时,计数器值减一;任何时刻计数器值为零的对象就是不可能再被使用的

在主流的java虚拟机中,没有选用引用计数算法来管理内存,主要原因时看似简单的算法有很多例外的情况要考虑,需要配合大量额外处理才能保证正确工作,比如引用计数很难处理对象之间相互循环引用的问题(两个对象相互引用,除此以为再无其他引用,计数器都不为零,无法回收)

可达性分析算法

Java语言的内存管理子系统,就是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的

基本思路:通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程中走过的路径称为"引用链"(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连或GC Roots不达到此对象,则证明此对象不可能再被使用

GC Roots对象类型

在Java技术体系中,可固定作为GC Roots对象:

  • 在java虚拟机栈中引用的对象:如各个线程被调用的方法堆栈中使用的参数,局部变量等
  • 在方法区中类静态属性引用的对象:如Java类中引用类型静态变量
  • 在方法区中常量引用的对象:如字符串常量池里的引用
  • 在本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用:如基本数据类型对应的Class对象,一些常驻的异常对象(NullPointException)

,还有系统类加载器

  • 所有被同步锁(synchronized 关键词)持有的对象
  • 反映Java虚拟机内部情况JMXBean、JVMTI中注册的回调、本地代码缓存等

除了这些固定的GC Roots集合外,根据用户所选的垃圾收集器以及当前回收的内存区别的不同,还可以有其他对象"临时性"地加入,共同构成完整GC Roots集合

引用的类型

  • 强引用(Strongly Reference):是指在程序代码之中普遍存在的引用赋值,如Object obj = new Object()的引用关系,只要强引用关系还在就永远不会回收被引用的对象
  • 软引用(Soft Reference):是指一些还有用但非必须的对象,被软引用管理的对象,在系统要发生内存溢出异常前,会把这些对象列入回收范围内进行第二次回收
  • 弱引用(Weak Reference):是指那些非必须的对象,强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生为止
  • 虚引用(Phantom Reference):是指最弱的引用关系,一个对象是否存在虚引用,完全不会影响器生成周期,设置虚引用的目的是为了在此对象被收集器回收时收到一个系统通知

方法区的垃圾收集

《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集。通常方法区垃圾收集的性价比比较低

方法区的垃圾收集主要回收两种内容:废弃的常量不再使用的类型

废弃的常量判断:与回收java堆中的对象类似。比如回收常量池中的字面值,一个"test"的字符串曾进入常量池,但当前系统又没有一个字符串对象的值是"test",如果此时发生内存回收,垃圾收集器会判断其有必要的话,"test"常量就会被系统清理出常量池

不再使用的类型判断:需要同时满足以下三个条件
1.该类所有实例都已经被回收,即Java堆中不存在该类及其任何派生子类的实例
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,通常很难达成
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
java虚拟机允许同时满足这三个条件的无用类进行回收,但不一定要回收

垃圾收集算法

垃圾收集算法可以分为引用计数式垃圾收集(直接垃圾收集)追踪式垃圾收集(间接垃圾收集)。主流Java虚拟机的垃圾收集算法均属于追踪式垃圾收集

分代收集理论

分代收集理论建立在两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕死的
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡

这两个分代假说共同奠定了常用的垃圾收集器的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(对象熬过垃圾收集过程的次数)分配到不同区域之中存储

Java虚拟机的设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。在新生代中每次垃圾收集都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代存放。

在Java堆划分出不同区域后,垃圾收集器才可以每次只回收某一个或某一部分的区域,因此有了如Minor GC 、Major GC 、Full GC这样的回收类型的划分,也能针对不同区域安排与里面存储对象的存亡特征相匹配的垃圾回收算法。

垃圾收集的类型:
部分收集(Partial GC):指目标不是完整整个Java堆的垃圾收集,又分为以下几种:
1.新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
2.老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集
3.混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集(G1收集器会有这种行为)
整堆收集(Full GC):指收集整个Java堆和方法区的垃圾收集

标记-复制算法

复制算法将内存划分为两块,每次只使用其中一块,当这一块内存用完,就将存活的对象复制到另外一块上面,之前已使用的内存空间一次清理掉

当多数对象都是存活的对象,将会产生大量内存间复制的开销,但当多数对象都是可回收的对象时,则只需要复制少量存活对象,并且每次回收是针对整个半区,不需要考虑有空间碎片的情况,实现简单,运行高效。但代价是将可用内存缩小为原来的一般,空间浪费比较多

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

标记清除算法分为标记清除两个阶段:

  • 标记:扫描内存块,对存活的对象进行标记(或者对需要回收的对象进行标记)
  • 清除:扫描内存块,对没有标记的对象进行回收(或者对标记的对象进行回收)

缺点:

  1. 执行效率不稳定,执行效率随对象数量增长而降低
  2. 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致当以后程序运行过程中需要分配较大对象时无法找到足够连续内存而不得不提前触发另一次垃圾收集

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

标记整理算法的标记过程与标记清除算法一样,但后续步骤是让所有存活的对象都向内存空间的一段移动,然后清理掉边界以外的内存

标记-清除算法与标记-整理算法的本质差异在于前者是非移动式回收算法,而后者是移动式。是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂

移动存活对象并更新所有引用这些对象的地方是一种极为负重的操作,这种对象移动的操作必须全程暂停用户应用程序才能进行,此种停顿被设计者称为"Stop The World"(STW)

分代回收算法

没有最好的算法,只有最合适的算法。分代回收算法就是这样的,根据回收对象的特点进行选择

  • 年轻代:对象存活率低适合复制算法
  • 老年代:内存区域大,存活率高适合标记清除(内存碎片不多的情况)+标记整理混合实现

算法对比

效率(时间复杂度):复制算法>标记-清除算法>标记-整理算法

内存整齐度:复制算法=标记压缩算法>标记清除算法

内存利用率:标记压缩算法=标记清除算法>复制算法

经典的垃圾收集器

HotSpot虚拟机的垃圾收集器

图中展示了七种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线则说明它们可以搭配使用,图中收集器所在的区域表示它是属于新生代收集器或老年代收集器

查看Java默认的垃圾收集器的命令

查看默认设置命令(查看默认的垃圾收集器)
java -XX:+PrintCommandLineFlags -version

查看默认垃圾收集器详细信息命令(通过查看新生代与老年代名称推断默认的垃圾收集器)
java -XX:+PrintGCDetails -version

查询XX所有参数(通过查看参数来判断使用的默认垃圾收集器)
java -XX:+PrintFlagsFinal

收集器语境中的并行与并发

并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态

并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集线程与用户线程都在运行。此时用户线程没有被冻结,因此程序仍能响应服务请求,但由于垃圾收集器占用了一部分系统资源,相应地应用程序处理的吞吐量将收到影响

Serial 收集器

Serial收集器是最基础,历史最悠久的收集器,JDK1.3.1版本之前是HotSpot虚拟机新生代收集器的唯一选择

Serial收集器,新生代收集器,是一个单线程工作的收集器,使用复制算法。只会使用一个处理器或一条收集线程去完成垃圾收集工作,并且垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(Stop The World)

Serial收集器是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,原因是其简单而高效,对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较小的环境下,由于没有线程交互的开销,可以获得最高的单线程收集效率

Serial Old 收集器

Serial Old收集器,是Serial收集器的老年代版本,老年代收集器,是一个单线程收集器,使用标记-整理算法

Serial Old收集器也是提供给客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,可以作为CMS收集器发生失败时的后备预案,在Concurrent Mode Failure时使用

ParNew 收集器

ParNew收集器,实质上是Serial收集器的多线程并行版本,新生代收集器,多线程并行收集器,使用复制算法。收集算法、对象分配规则、回收策略与Serial收集器完全一致

当前除了Serial收集器之外,只有ParNew收集器能与CMS收集器配合工作。但随着收集器技术的不断改进,G1收集器作为了CMS的继承与替代者,其不再需要与其他新生代收集器配合工作。可以说ParNew是HotSpot虚拟机第一款退出历史舞台的垃圾收集器

Parallel Scavenge 收集器

Parallel Scavenge收集器,新生代收集器,多线程并行收集器,使用复制算法

Parallel Scavenge收集器的特点是关注点与其他收集器不同,CMS收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)

吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)
停顿时间越短就越适合与用户交互或保证服务响应质量的程序,良好的响应速度能提升用户体验
高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,适合在后台运算而不需要太多交换的分析任务

Parallel Scavenge收集器提供了两次参数用于精准控制吞吐量
-XX:MaxGCPauseMillis
控制最大垃圾收集停顿时间,参数是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。但垃圾收集停顿时间缩短是牺牲吞吐量和新生代空间为代价换取的(系统把新生代空间调小,停顿时间降低,但垃圾收集频次增加,吞吐量降低)

-XX:GCTimeRatio
设置吞吐量大小,参数是一个大于0小于100的整数,表示运行代码时间是运行垃圾收集时间的xx倍。例如参数设置为19,那运行最大垃圾收集时间占总时间的5%(1/(1+19))

除此之外Parallel Scavenge收集器还有一个开关参数
-XX:+UseAdaptiveSizePolicy
开关参数,开启时,不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调节这些参数以提供合适的停顿时间或最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)

Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,老年代收集器,多线程并行收集器,使用标记-整理算法

Parallel Old收集器出现后,与Parallel Scavenge搭配组合,成为"吞吐量优先"收集器。在注重吞吐量或处理器资源稀缺的情景下,优先考虑此种组合

CMS 收集器

CMS收集器,一种以获取最短回收停顿时间为目标的收集器,老年代收集器,并发收集器,使用标记-清除算法。CMS收集器符合关注服务的响应速度应用的需求,更短的系统停顿时间来给用户带来更好的交互体验

CMS相对于之前的几种收集器来说更复杂,整个过程分为四个步骤:
1.初始标记(CMS initial mark)
初始标记阶段需要停止所有用户线程(Stop The World),标记过程仅标记一下GC Roots能直接关联的对象
2.并发标记(CMS concurrent mark)
并发标记阶段是从GC Roots的直接关联对象开始遍历整个对象图的过程,用户线程与垃圾收集线程并发运行
3.重新标记(CMS remark)
重新标记阶段需要停止所有用户线程(Stop The World),是为了修正并发标记阶段因用户线程运行而导致标记发生变动的那一部分对象的标记记录
4.并发清除(CMS concurrent sweep)
并发清除阶段是清理掉标记阶段后判断已经死亡的对象,由于不需要移动存活对象,这个阶段用户线程与垃圾收集线程并发运行

CMS收集器的缺点:
1.对处理器资源敏感
并发阶段,虽然不会导致用户线程停顿,但会占用一部分线程,导致应用程序变慢,降低总吞吐量
2.无法处理"浮动垃圾"
有可能出现"Concurrent Mode Failure"失败从而导致停止所有用户线程(Stop The World)的整堆收集(Full GC)的产生
浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程仍然继续运行,程序在运行过程中会有新的垃圾对象不断产生,但这一部分的垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉,只能保留并在下一次垃圾收集时清理掉,这一部分的垃圾称为"浮动垃圾"
3.收集结束后会产生大量空间碎片产生
CMS是一款基于标记-清除算法实现的收集器,垃圾收集结束会产生大量空间碎片,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但因为无法找到足够大的连续空间来分配当前对象,而提前触发一次整堆收集(Full GC)

Garbage First 收集器(G1)

Garbage First收集器(G1)是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。是一款面向服务端应用的垃圾收集器

作为CMS收集器的替代者,设计者希望做出一款能建立起"停顿时间模型"。停顿时间模型指的是能够支持指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒

G1收集器出现之前所有的收集器,垃圾收集的目标范围要么是整个新生代(Minor GC),要么是整个老年代(Marjor GC),再或者是整个Java堆(Full GC)。G1收集器则是面向堆内存任何部分来组成回收集(Collection Set)进行回收,衡量标准不在是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的混合收集模式(Mixed GC)

G1开创的基于Region的堆内存布局与其他收集器有着明显的差异,G1收集器不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够根据不同角色的Region采用不同策略去处理

Region中还有一类特殊的Humongous区域,专门用来存储大对象。只有认为大小超过了一个Region容量一半的对象就会判定为大对象。每一个Region大小可以通过参数(-XX:G1HeapRegionSize)设定,取值范围为1MB-32MB,且为2的N次幂。对于容量超过整个Region容量的超级大对象,竟会被存放在N个连续的Humongous Region之中

G1仍然保留新生代与老年代的概念,但新生代与老年代的空间不再固定,而是一系列区域(不需要连续)的动态集合。G1收集器之所以能够建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划的避免在整个Java堆中进行全区域的垃圾收集
具体处理思路是让G1收集器去跟踪各个Region里面的垃圾堆进行收集的"价值"(回收所得的空间大小以及回收所需时间的比值),之后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(参数-XX:MaxGCPauseMillis指定,默认200毫秒)内,优先回收价值收集最大的一批Region

G1收集器的垃圾收集过程分为四个步骤:
1.初始标记(Initial Marking)
仅标记GC Roots能直接关联到的对象,此阶段需要暂停所有用户线程(Stop The World)
2.并发标记(Concurrent Marking)
从GC Roots直接关联的对象进行可达性分析,递归扫描整个堆的对象,找出要回收的对象,此阶段可与用户线程并发进行
3.最终标记(Final Marking)
修正并发标记阶段因用户线程运行而导致标记发生变动的那一部分对象的标记记录,此阶段需要暂停所有用户线程(Stop The World)
4.筛选回收(Live Data Counting and Evacuation)
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来指定回收计划,可以选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理整个旧的Region的全部空间。由于需要操作存活对象的移动,必须暂停所有用户线程(Stop The World)

参考资料

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)

https://zhuanlan.zhihu.com/p/265963546