1. 垃圾回收算法
1.1. 标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象
缺点:
- 标记和清除的效率都比较低
- 清除后会产生大量的不连续内存碎片,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World
1.2. 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
- 每次清理都是直接清理掉一整块内存,效率高
- 不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
缺点:
- 效率问题:在对象存活率较高时,复制操作次数多,效率降低;
- 空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)
1.3. 标记-整理算法
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
1.4. 分代收集
GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代收集”算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收
概念
- 并行 多条垃圾收集线并行工作,但此时用户线程仍然处理等待状态
- 并发 用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
2. 垃圾收集器
2.1. 垃圾收集方式
2.1.1. Minor GC 次要
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
-
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
-
内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
-
执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
-
质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
2.1.2. Major GC 重要
Major GC 是清理永久代。
2.1.3. Full GC 充分
Full GC 是清理整个堆空间—包括年轻代和永久代。
2.2. 新生代收集器
2.2.1. Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器。
这个收集器是一个单线程收集器。
它在进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。
它最大的特点是:简单而高效
2.2.2. ParNew收集器
ParNew收集器是Serail收集器的多线程版本,使用多条线程进行垃圾收集。
他的控制参数、收集算法、Stop The World、对象分配规则、回收策略都与Serial收集器一样。
2.2.3. Parallel Scavenge收集器
它是一个新生代收集器,使用复制算法
它的关注点与其他收集器不同,其他收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。
吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)
它可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算不需要太多交互的任务。
设置参数
-
MaxGCPauseMillis参数 最大垃圾收集停顿时间
该值必须一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。
但是值如果设置的过小会导致吞吐量变低、新生代空间也会被缩小。 -
GCTimeRatio参数 垃圾收集时间占总时间的比率
该值应该是一个大于0小于100的整数。
默认值99,当设置为99时,吞吐量=1/(1+99)=1% -
UseAdaptiveSizePolicy
这是一个开关参数,这个参数打开后就不需要手工指定新生代的大小 Eden Survivor区的比例,晋升老年代对象年龄等细节参数了,会自动调整。
2.3. 老年代收集器
2.3.1. Serial Old收集器
它是Serial收集器的老年代版本,它同样是一个单线程收集器。
2.3.2. Parallel Old集器
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。他的特点和Parallel Scavenge收集器一样。
在注重吞吐量和CPU资源敏感的场合,都可以优先考虑 Parallel Scavenge加 Parallel Old收集器的组合拳。
2.3.3. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
最适合的场景是互联网网站或者B/S系统的服务端上,这类应用尤其重视服务响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS回收期是基于“标记-清除”算法实现的,分为4个步骤:
- 初始标记(CMS initial mark) Stop The World
标记GC Roots能直接关联到的对象,速度极快 - 并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程 - 重新标记(CMS remark) Stop The World
修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
这个阶段的停顿时间比初始标记阶段稍长一些,但远比并发标记时间短 - 并发清除(CMS concurrent sweep)
缺点:
- CMS收集器对CPU资源非常敏感
回收线程数=(CPU数量+3)/4,也就是当CPU是4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并随着CPU数量的增加而下降
虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。 - CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
浮动垃圾-由于在并发清理阶段还有用户线程还在运行,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记之后
CMS无法在档次手机中处理掉他们,只好留到下次GC,这一部分垃圾就被称为"浮动垃圾"
- 导致大量的内存碎片,这是“标记-清除”算法的局限性。
2.4. 全能收集器 G1
G1是一款面向服务端应用的垃圾收集器。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
具体来有以下特点:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
2.4.1. 概念
2.4.1.1. 分区 Region
G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
2.4.1.2. 卡片 Card
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
2.4.1.3. 堆 Heap
G1同样可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。
2.4.1.4.本地分配缓冲 Local allocation buffer (Lab)**
在分配内存时,如果多个线程同时分配同一块内存可能会导致系统内存混乱,一般是使用CAS配上失败重试的方式保证更新操作的原子性。
但是每次都是用同步的方式会严重影响效率,所以我们会给每个线程预先分配一块内存等待使用,当内存不够时,就使用同步的方式再次给他分配一块内存。
每个内存分配自己的分配缓冲内存,这样就不用在每次分配的时候使用同步的方式,而减少同步时间。
TLAB
其中,应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,而大部分都会落入Eden区域(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间
GCLAB
而每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,每次回收会将对象复制到Suvivor空间或老年代空间
PLAB
对于从Eden/Survivor空间晋升(Promotion)到Survivor/老年代空间的对象,同样有GC独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)。
2.4.1.5.巨型对象 Humongous Region
一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(Starts Humongous),相邻连续分区被标记为连续巨型(Continues Humongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。
2.4.1.6. 已记忆集合 Remember Set (RSet)
在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。
2.4.1.7. Per Region Table (PRT)
RSet在内部使用Per Region Table(PRT)记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:
- 稀少:直接记录引用对象的卡片索引
- 细粒度:记录引用对象的分区索引
- 粗粒度:只记录引用情况,每个分区对应一个比特位
由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。

本文详细解析了Java垃圾回收的各种算法,包括标记-清除、复制、标记-整理及分代收集策略。介绍了多种垃圾收集器,如Serial、ParNew、ParallelScavenge、SerialOld、ParallelOld、CMS及G1收集器的特性和适用场景。
1379

被折叠的 条评论
为什么被折叠?



