JVM总结(二):垃圾回收器
垃圾回收器
对象已经死了吗?
对象生存判断算法
引用判断过程
垃圾收集算法简介
垃圾收集器
新生代垃圾收集器
老年代垃圾收集器
新生代和老年代垃圾收集器
垃圾回收器
对象已经死了吗?
对象生存判断算法
- 引用计数法
给对象中添加一个引用计数器,每当一个地方引用到这个对象的时候,计数器值就加1,当引用失效时,计数器的值就减1,当计数器值变为0时,便说明该对象不可能再被使用了。
优点:实现简单,判定效率较高。
缺点:当出现对象之间的相互循环引用时,即两个类中都存在引用字段分别引用着对方的时候,在回收过程中这时该算法无效。 - 可达性分析算法
为了克服引用计数法的弊端,现在比较主流的实现算法是可达性分析算法。该算法的基本思想是通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。如果在一次的搜索中,一个对象到GC Root没有任何的引用链相连,则说明此对象是不可用的。具体如图所示
GC Roots的判定:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象
2、方法区中静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中JNI(即一般说的Native方法)引用的对象
引用判断过程
判断引用是否无效的过程分为三个阶段
1、当JVM进行垃圾收集时,JVM使用可达性分析算法进行分析,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,此时该对象将被第一次标记,并进行一次筛选,筛选的条件是此对象有没有必要执行finalize()方法,如果对象没有覆盖该方法,或者该方法已经被虚拟机调用过了,虚拟机将这两种情况都视为“没有必要执行”。
2、如果该对象被判定为有必要执行finalize()方法,那么对象将会被放置到一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。因为一个对象可能在finalize()方法中执行缓慢,或者发生了死循环,这将导致该队列中的其他对象长期处于等待阶段,甚至导致整个内存系统的奔溃。
3、F-Queue中的标记筛选。
finalize()方法是对象逃脱死亡命运的最后一次机会,然后GC将对F-Queue中的对象进行第二次小规模的标记。如果对象在finalize()方法中成功拯救了自己,比如与引用链上的任何一个对象建立关联,那么在第二次标记的时候,该对象将被移出F-Queue的集合,如果对象这个时候还没有逃脱,那基本上它就真的被回收了。
垃圾收集算法简介
目前比较主流的垃圾收集算法有四种:标记-清除算法、复制算法、标记-整理算法、分代收集算法。具体分析对比如下:
垃圾收集器
常见的JVM垃圾收集器有七种,具体如下图所示:
分类 | 标记-清除算法(Mark-Sweep) | 复制算法(Coping) | 标志-整理算法(Mark-Compact) | 分代收集算法(Generational Collection) |
进行整理 | 否 | 是 | 是 | 是 |
算法实现过程 | 该算法分为两个过程:标记和清除。先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的要回收的对象。 | 将内存按容量划分为大小相等的两块区域,每次使用其中的一块,当一块的内存用完了,执行GC算法时将还存活的对象整理复制到另外一块上,然后清理所有的内存块。 | 该算法分为两个过程:标记和整理。首先标记出所有需要回收的对象,然后让存活的对象都向内存的一端移动,然后直接清除掉端边界以外的内存。 | 根据对象存活周期的不同将内存划分为几块,一般把堆划分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。 |
优点 | 简单,易于实现 | 内存分配时算法不产生内存碎片 | 内存分配时算法不产生内存碎片,也比较易于实现 | 分代收集,效率较高 |
缺点 | 1、效率低 2、会产生大量不连续的内存碎片 | 空间消耗太大,内存被压缩为原来的一半 | 算法复杂度大,执行步骤较多 | 算法复杂度大,执行步骤较多 |
新生代垃圾收集器
- Serial
Serial收集器是单线程的一个收集器,但它的单线程的意义是它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集的时候,必须暂停其他所有的工作线程,直到它收集结束。
分代收集算法:新生代单线程采用复制算法,并暂停所有用户线程;老年代单线程采用标记-整理算法,并暂停所有用户线程。 - ParNew
ParNew收集器是Serial收集器的多线程版。其基本操作和Serial算法基本一致。该收集器一般搭配CMS收集器进行工作。‘
分代收集算法:新生代采用复制算法,并暂停所有用户线程;老年代采用标记-整理算法,并暂停所有用户线程。 - Parallel Scavenge
Parallel Scavenge收集器是也与ParNew算法十分相似,但是与其他收集器的关注点大多是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器目的是达到一个可控制的吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗的时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),举个例子,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
GC自适应调节策略:JVM会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。Parallel Scavenge收集器可以搭配自适应调节策略。
分代收集算法:新生代采用复制算法,并暂停所有用户线程;老年代采用标记-整理算法,并暂停所有用户线程。
老年代垃圾收集器
- Serial Old
Serial Old是Serial算法的老年代版本,同样是一个单线程收集器。该收集器主要是给Client模式下的虚拟机使用的。
分代收集算法:新生代单线程采用复制算法,并暂停所有用户线程;老年代单线程采用标记-整理算法,并暂停所有用户线程。 - Parallnel Old
Parallnel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。 - CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。该收集器是基于”标记-清除“算法实现的。
CMS收集器的收集过程分为以下4个步骤:
1、初始标记(Stop the World,标记GC Roots能直接关联到的对象)
2、并发标记(进行GC Roots Tracing的过程)
3、重新标记(Stop the World,休整并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录)
4、并发清除(并发清除无用的对象)
缺点:
a、CMS收集器对CPU资源非常敏感,并发阶段占用的线程资源较多。
b、CMS收集器无法处理浮动垃圾。因为CMS并发清理阶段用户线程还在运行着,所以也会有相应的垃圾产生,这部分垃圾CMS无法在此次的收集中处理掉它们。
c、CMS收集器由于是基于“标记-清除”算法,故会产生较多的内存空间碎片。
G1
- G1 (适合堆内存大,多CPU的情况)
G1(Garbage-First)收集器所具备的特点:
1、并行和并发:使用多个CPU来缩短Stop-The-World的时间,部分垃圾收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
2、分代收集
3、空间整合:从整体看是标记-整理算法,从局部看是复制算法 。
4、可预测的停顿。追求低停顿,并建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,达到了实时Java的垃圾收集器。
G1收集器分代策略:
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region)。G1收集器之所以可以有计划地避免在整个Java堆中进行全区域的垃圾收据,是因为G1收集器跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。即Grabage-First。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是通过 Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set。在新建对象时,JVM会将相关的引用信息记录到被引用对象所属的Region的Remembered Set中。当进行回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对堆进行扫描也不会有遗漏。
G1收集器的收集阶段也分以下几个步骤:
1、初始标记(只是标记一下GC Roots能直接关联到的对象,并修改可以得Region中创建新对象,这阶段需要停顿线程,但耗时很短)
2、并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象)
3、最终标记(修正在并发标记期间因月洪湖程序继续运行而导致标记产生变动的那一部分标记记录)
4、筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)
最后,我们总结一下JVM中的垃圾收集器:
分类 | 所属分代 | 使用线程 | 使用算法 |
Serial | 新生代 | 单线程 | 复制(新)、标记-整理(老) |
ParNew | 新生代 | 多线程 | 复制(新)、标记-整理(老) |
Parallel Scavenge | 新生代 | 多线程 | 吞吐量优先算法 |
Serial Old | 老年代 | 单线程 | 复制(新)、标记-整理(老) |
Parallel Old | 老年代 | 多线程 | 复制(新)、标记-整理(老) |
CMS | 老年代 | 多线程 | 标记-清除算法(初始标记、并发标记、重新标记、并发清除) |
G1 | 新生代&&老年代 | 多线程 | 标记-整理算法(初始标记、并发标记、最终标记、筛选回收) |
最后再补充几个概念:
强引用:在程序代码中普遍存在, 类似" Object obj = new Object(); ", 只要强引用还存在, 垃圾收集器就永远不会回收被引用对象
软引用:用来描述有用但非必需的对象, 在系统将要发生内存溢出之前, 将会把这些对象列进回收范围之中进行第二次回收
弱引用:非必需的对象. 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象.
虚引用:一个对象是否有虚引用完全不会影响其生存时间.为一个对象设置虚引用只是为了让对象被回收之时收到系统通知
G1补充:
分区 Region
G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
卡片 Card
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
堆 Heap
G1同样可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。
G1将新生代,老年代的物理空间划分取消了,逻辑上划分为新生代和老年代。
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200 如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
对象的分配策略。它分为3个阶段:
- TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
- Eden区中分配
- Humongous区分配
TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。
对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。
Young GC 阶段:
阶段1:根扫描
静态和本地对象被扫描
阶段2:更新RS
处理dirty card队列更新RS
阶段3:处理RS
检测从年轻代指向年老代的对象
阶段4:对象拷贝
拷贝存活的对象到survivor/old区域
阶段5:处理引用队列
软引用,弱引用,虚引用处理
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。当老年代占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动一次混合垃圾收集周期。
1、全局并发标记(global concurrent marking)
2、拷贝存活对象(evacuation)
global concurrent marking的执行过程分为五个步骤:
初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。