3.2 对象是否存活
已死对象:不可能再被任何途径使用的对象。
3.2.1 引用计数算法
- 基本思路:在对象中添加一个引用计数器,每当又一个地方引用它时,计数器值加一;当引用失效时,计数器值减一。任何时刻计数器为0的对象就是不可能再被使用的。
- 优点:引用计数算法虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高。
- 缺点:这个算法有很多例外情况需要考虑,必须配合大量的额外处理才能保证正确地工作。比如单纯的引用计算很难解决两个不可能再被访问的对象之间相互循环引用的问题。
3.2.2 可达性分析算法
- 基本思路:通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,即从GC Roots到这个对象不可达时,证明此对象是不可能再被使用的。
固定可作为GC Roots的对象包括以下几种:
- 在JVM栈(栈帧中的本地变量表)中引用的对象,如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,比如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,比如字符串常量池里的引用。
- 在本地方法栈中JNI(Native方法)引用的对象
- JVM内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象等。
- 所有被同步锁(Synchronized 关键字)持有的对象。
- 反映JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
3.2.3 引用分类
- 强引用:指在程序代码中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:用来描述一些还有用但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 弱引用:也用来描述非必须对象,但强度比软引用更弱一些,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用:最弱的一种引用关系。一个对象是否虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
3.2.4 方法区的回收
方法区的垃圾收集主要回收两部分的内容:废弃的常量和不再使用的类型。
- 回收废弃的常量 :对于曾进入常量池中的常量,如果已经没有任何对象,JVM中也没有其他地方引用该常量,如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个常量就会被系统清理出常量池。常量池中字面量、其他类(接口)、方法、字段的符号引用与此类似。
- 回收不再被使用的类:需要同时满足以下三个条件:
1. 该类(包括派生子类)所有的实例都已被回收。
2. 加载该类的加载器已被回收。
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.3 垃圾收集算法
从如何判定对象存活的角度出发,垃圾收集算法可分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,也被称为“直接垃圾收集”和“间接垃圾收集”。本节介绍的所有算法均属于追踪式垃圾收集的范畴。
3.3.1 分代收集理论
分代假说:
- 弱分代假说:绝大多数对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
常用垃圾收集器的一致设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。垃圾收集器得以每次只回收其中某一个或者某些部分的区域。
依据假说3,只需在新生代上建立一个全局的数据结构(记忆集Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入GC Roots进行扫描。这个方法需要在对象改变引用关系时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
3.3.2 标记-清除算法
两个过程:
- 标记出所有需要回收的对象。
- 在标记完成后,统一回收掉所有被标记的对象。
也可以反过来
- 标记出存活对象。
- 在标记完成后,统一回收掉所有未标记的对象。
缺点:
- 执行效率不稳定。如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作。导致标记和清除两个过程的执行效率都随对象数量增长而降低。
- 内存空间的碎片化问题。标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.3.3 标记-复制算法
将可用内存按容量划分成大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。被用于回收新生代。
优点:
- 解决了标记-清除算法面对大量可回收对象时执行效率低的问题。
- 每次都是针对整个半区进行内存回收,分配内存时不用考虑有空间碎片的复杂情况,只用移动堆顶指针,按顺序分配即可。
缺点:
- 将可用内存缩小为原来的一半,空间浪费未免太多了一点。
- 如果内存中多数对象都是存活的,将会产生大量的内存间复制的开销。
改进:
将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor空间。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpotJVM默认Eden和Survivor的大小比例是8:1。
3.3.4 标记-整理算法
该算法针对老年代对象的存亡特征。
- 标记所有需要清理的对象。
- 让所有存活的对象都向内存空间的一端移动。
- 直接清理掉边界以外的内存。
优点:解决了内存空间碎片化问题。
缺点:在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作。而且这种对象移动操作必须全程暂停用户应用进程才能进行。
Stop The World
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。
3.5 经典垃圾收集器
HotSpotJVM的七种垃圾收集器
| 序号 | 收集器 | 收集范围 | 算法 | 执行类型 |
|---|---|---|---|---|
| 1 | Serial | 新生代 | 标记复制 | 单线程 |
| 2 | ParNew | 新生代 | 标记复制 | 多线程并行 |
| 3 | Parallel | 新生代 | 标记复制 | 多线程并行 |
| 4 | Serial Old | 老年代 | 标记整理 | 单线程 |
| 5 | CMS | 老年代 | 标记清除 | 多线程并发 |
| 6 | Parallel Old | 老年代 | 标记整理 | 多线程 |
| 7 | G1 | 全部 | 标记复制、标记整理 | 多线程 |
并行(Parallel):多条垃圾收集线程并行工作,而默认用户线程仍处于等待状态。
并发(Concurrent):垃圾收集线程与用户线程一段时间内同时工作(交替执行)。

3.5.1 Serial收集器

Serial收集器和Serial Old收集器在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。迄今为止,它仍是HotSpotJVM运行在客户端模式下的默认新生代收集器。
优点:
- 简单而高效(与其他收集器的单线程相比)。
- 对于内存资源受限的环境,它是所有处理器里额外内存消耗(Memory Footprint)最小的。
- 对于单核处理器或处理器核心较少的环境来说,它由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
3.5.2 ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集外,其余行为与Serial收集器完全一致。
ParNew收集器运行在大多服务端模式下的HotSpotJVM。因为,除了Serial收集器外,目前只有它能和CMS收集器配合工作。
ParNew收集器在单核心处理器的环境中,由于存在线程交互开销,该收集器在通过超线程技术实现的伪双核处理器环境中都不能百分之百保证超越Serial处理器。
在多核心处理器环境中,ParNew收集器默认开启的收集线程数与处理器核心数量相同。
3.5.3 Parallel Scavenge收集器

Parallel Scavenge和ParNew一样都是多线程、新生代收集器,都使用标记-复制算法进行垃圾回收。但它们有个巨大的不同点:ParNew收集器和CMS收集器追求降低用户线程的停顿时间,因此适合交互式应用;而Parallel Scavenge追求CPU吞吐量,能够在较短的时间内完成指定任务,因此适合没有交互的后台计算。
吞吐量:处理器用于运行用户代码的时间与处理器总消耗时间的比值。
降低停顿时间的两种方式
- ParNew收集器(并行):在多CPU环境中使用多条GC线程,从而垃圾回收的时间减少,从而用户线程停顿的时间也减少;
- CMS收集器(并发):实现GC线程与用户线程并发执行。所谓并发,就是用户线程与GC线程交替执行,从而每次停顿的时间会减少,用户感受到的停顿感降低,但线程之间不断切换意味着需要额外的开销,从而垃圾回收和用户线程的总时间将会延长。
3.5.4 Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,使用标记-整理算法。Serial Old收集器的主要意义是供客户端模式下的HotSpot虚拟机使用。在服务端模式下,它可能与Parallel Scavenge收集器搭配使用;或作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
3.5.5 Parallel Old收集器

Parallel Old时Parallel Scavenge收集器的老年代版本,支持多线程并行收集(《深入理解Java虚拟机》第3版95页写的是支持多线程并发收集,个人觉得应该还是并行收集),基于标记-整理算法实现。
由于老年代Serial Old收集器在服务端模式下,无法充分利用服务器多处理器的并行处理能力,其他表现良好的CMS收集器无法与Parallel Scavenge配合工作,所以Parallel Old与Parallel Scavenge收集器搭配为“吞吐量优先”收集器。可用于注重吞吐量或处理器资源较为稀缺的场合。
3.5.6 CMS收集器
CMS(Concurrent Mask Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适用于互联网网站或者基于浏览器的B/S系统的服务端这类通常都会较为关注服务的响应速度的应用。
B/S架构是browser/server指浏览器和服务器端,在客户机端不用装专门的软件,只要一个浏览器即可.
C/S架构是client/server指客户机和服务器,在客户机端必须装客户端软件后,才能访问服务器如sql server 2000

CMS垃圾收集过程:
- 初始标记
停止一切用户线程,仅使用一条初始标记线程对所有与GC ROOTS直接关联的对象进行标记。速度很快。 - 并发标记
使用多条并发标记线程并行执行,并与用户线程并发执行。此过程进行可达性分析,标记出所有废弃的对象。速度很慢。 - 重新标记
停止一切用户线程,并使用多条重新标记线程并行执行,将刚才并发标记过程中新出现的废弃对象标记出来。这个过程的运行时间介于初始标记和并发标记之间。 - 并发清除
只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象。这个过程非常耗时。
缺点:
- 吞吐量低
由于CMS在垃圾收集过程使用用户线程和GC线程并发执行,从而线程切换会有额外开销,因此CPU吞吐量就不如在垃圾收集过程中停止一切用户线程的方式来的高。 - 无法处理浮动垃圾,导致频繁Full GC
由于垃圾清除过程中,用户线程和GC线程并发执行,也就是用户线程仍在执行,那么在执行过程中会产生垃圾,这些垃圾称为“浮动垃圾”。
如果CMS在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足时,就需要再次发起Full GC,而此时CMS正在进行清除工作,则会出现“Concurrent Mode Failure”失败,此时只能由Serial Old临时对老年代进行一次Full GC。
- 使用“标记-清除”算法产生碎片空间
由于CMS使用了“标记-清除”算法, 因此清除之后会产生大量的碎片空间,不利于空间利用率。
- 开启-XX:+UseCMSCompactAtFullCollection
开启该参数后,每次FullGC完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块儿。但每次都整理效率不高,因此提供了以下参数。- 设置参数-XX:CMSFullGCsBeforeCompaction
本参数告诉CMS,经过了N次不整理空间的Full GC过后再进行一次内存整理。
3.5.7 Garbage First(G1)收集器
G1收集器可以面向堆内存的任何部分来组成回收集(Collection Set, CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
基于Region的堆内存布局:
- G1仍然遵循分代收集理论,但G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分成多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间。
收集器能够对扮演不同角色的Region采用不同的策略去处理。 - Region中的Humongous区域:专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来看。
G1收集器Region分区示意图
停顿时间模型:能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
G1收集器能建立可预测的停顿时间模型:
- 它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
- 具体的处理思路:让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,然后在后台维护一个优先级列表,每次根据用户设定运行的收集停顿时间,优先处理回收价值收益最大的那些Region。这样保证了G1收集器在有限时间内获取尽可能高的收集效率。
价值:即回收所获得的空间以及回收所需时间的经验值。

G1收集器的Mixed GC运作过程:
-
初识标记:
标记与GC Roots直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程很快。 -
并发标记:
进行全面的可达性分析,开启一条并发标记线程与用户线程并发执行。这个过程比较长。 -
最终标记:
标记出并发标记过程中用户线程新产生的垃圾。停止所有用户线程,并使用多条最终标记线程并行执行。 -
筛选回收:
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成CSet,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个Region的全部空间。此时也需要停止一切用户线程,并使用多条筛选回收线程并行执行。
与CMS相比:
- G1可以指定最大停顿时间。
- G1运作期间不会产生内存空间碎片。
- 就内存占用来说,CMS只在新生代中维护一份全局的记忆集,而且只需要处理老年代到新生代的引用,反过来则不需要(代价则是当CMS发生Old GC时,要把整个新生代作为GC Roots来进行扫描)。而G1需要在堆中每个Region中,无论扮演的是新生代还是老年代角色,都维护一份记忆集。这导致G1的记忆集(和其他内存消耗)肯会占整个堆容量的非常大的空间。
