1、如何确定一个对象是垃圾?
想要进行垃圾回收,得先知道什么样的对象是垃圾。
1)引用计数法
通过记录对象被其他对象引用的个数,来判断对象是不是垃圾。
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任
何指针对其引用,它就是垃圾。
**缺点:**如果A和B相互持有引用,则会导致永远不能回收。
2)可达性分析
通过GC Root对象,开始向下寻找,看某个对象是否可达,如果可达,则不是垃圾。
什么样对象可作为 GC Root 的对象呢?
类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。
2、垃圾收集算法
在确定一个对象为垃圾之后,接下来就要考虑垃圾该怎么回收呢?
1)标记-清除(Mark-Sweep)
标记
找出内存中需要回收的对象,并把他们标记出来。
此时堆中所有的对象都会被扫描一遍,从而才能确定哪些对象需要回收,所以比较耗时。
清除
清除掉被标记为需要回收的对象,释放出对应的内存空间。
缺点
- 标记和清除两个过程都比较耗时,效率不高。
- 标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2)复制(Copying)
将内存划分为两块相等的区域,每次只使用其中一块。当其中一块内存使用完,就会将还存活的对象复制到另一快内存上,然后把已经使用过的内存空间一次清除掉。
缺点
- 空间利用率低。
3)标记-整理(Mark-Compact)
标记
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是将所有存活
的对象都移动到一端,然后直接清理掉端边界以外的内存。
整理
将所有存活的对象都移动到一端,清理掉边界以外的内存。
缺点
3、分代收集算法
堆内存到底使用的是哪一种垃圾收集算法呢?
Young区
采用复制算法。对象在被分配之后,可能生命周期比较短,Young区复制效率比较高,比如S0和S1采用的就是复制算法。
Old区
采用标记清除或标记整理。Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理。
4、垃圾收集器
收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。其中主要的垃圾收集器为CMS和G1。
1)垃圾收集器类型
1.1)串行收集器
Serial
和 Serial Old
,只有一个垃圾收集线程(GC线程)执行,会停顿用户线程。
适用于内存比较小的嵌入式设备。
1.2)并行收集器(吞吐量优先)
Parallel Scavenge
和 Parallel Old
,多个垃圾收集线程(GC线程)并行工作,会停顿用户线程。
适用于科学计算 、后台处理等场景。
1.3)并发收集器(停顿时间优先)
CMS
和 G1
,用户线程和垃圾收集线程(GC线程)同时执行(不一定是并行的,也可能是交替执行),垃圾收集线程(GC线程)执行时不会停顿用户线程的运行。
适用于对时间有要求的场景,如web。
1.4)停顿时间和吞吐量
这两个指标是评价垃圾回收器好坏的标准,其实调优也就是在观察者两个变量。
- 停顿时间
垃圾收集器执行垃圾回收时造成的系统停顿时间,也就是Stop The World的时间。
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验。
- 吞吐量
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
高吞吐量可以高效地利用CPU时间,尽快完成程序的运算任务,所以适用于后台运算等场景。
比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序
的运算任务。
2)Serial 收集器
Serial收集器是最基本、发展历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。
它是一种单线程收集器,也就是说它只会使用一个CPU的一个GC收集线程去完成垃圾收集工作,并且在执行垃圾收集工作时会暂停所有的用户线程。
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器。
3)ParNew 收集器
ParNew 收集器可以理解为Serial 收集器的多线程版本。
优点:在多CPU时,比Serial收集器效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器。
4)Parallel Scavenge 收集器
Parallel Scavenge收集器是一个新生代收集器,它使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注 系统的吞吐量 。
-XX:MaxGCPauseMillis // 控制最大的垃圾收集停顿时间
-XX:GCTimeRatio // 设置吞吐量的大小(0,100)
5)Serial Old 收集器
Serial Old 收集器是Serial 收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算
法",运行过程和Serial 收集器一样。
6)Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理算法"进行垃圾
回收。吞吐量优先。
7)CMS 收集器
Concurrent Mark Sweep 并发标记收集器。JDK 8默认的收集器。
CMS 收集器的目标是降低停顿时间
。采用标记-清除算法
。
官网文档:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector
7.1)优点
并发收集、低停顿
7.2)缺点
产生大量空间碎片、并发阶段会降低吞吐量。
占用CPU资源。
无法处理并发清除阶段产生的浮动垃圾(下一次GC清除)。
7.3)回收过程
并发标记和并发清除,GC线程可以与用户线程一起工作,所以CMS收集器的内存回收过程是与用户线程一起并发地执行的。
- 1、初始标记
CMS initial mark,标记GC Root能直接引用的对象。
会Stop The World,也就是暂停用户线程。
- 2、并发标记
CMS concurrent mark,对每个GC Root对象进行Tracing搜索,在堆中查找其下所有能关联到的对象。 GC线程和用户线程并发执行。
- 3、重新标记
CMS remark, 修正在并发标记阶段因为用户线程的并发执行导致变动的数据。
会Stop The World,也就是暂停用户线程。
- 4、并发清除
CMS concurrent sweep,清除不可达的GC Roots对象。GC线程和用户线程并发执行。
8)G1 收集器
G1(Garbage First)从JDK 7开始使用,到JDK 8已非常成熟,JDK 9默认的垃圾收集器,适用于新老生代。同CMS收集器一样,G1 收集器的目标也是降低停顿时间
。采用标记-整理算法
。
G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。 G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。
官方文档
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection
8.1)优点
1、并行与并发
2、分代收集(仍然保留了分代的概念)
3、空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
4、可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
8.2)回收过程
- 1、初始标记
Initial Marking,标记GC Roots能关联到的对象,修改TAMS的值。
会Stop The World,也就是暂停用户线程。
- 2、并发标记
Concurrent Marking,对每个GC Root对象进行Tracing搜索,在堆中查找其下所有能关联到的对象。 GC线程和用户线程并发执行。
- 3、最终标记
Final Marking,修正在并发标记阶段因为用户线程的并发执行导致变动的数据。
会Stop The World,也就是暂停用户线程。
- 4、筛选回收
Live Data Counting and Evacuation,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划。
8.3)G1内存模型
使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为若干个大小相等的独立区域(Region), 每次分配对象空间将逐段使用内存。
虽然保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是Region集合的一部分。也就是说物理上是不连续的,逻辑上是连续的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4dBrOV6u-1597050912009)(img/G1内存模型.png)]
分区Region
分区Region为堆内存最小可用粒度,默认将整个堆划分为2048 个Region分区。
// 设置分区大小(1MB~32MB,必须是2的幂)
-XX:G1HeapRegionSize=n
卡片Card
每个分区被分为若干个大小为512byte卡片(Card)。堆内存所有分区的卡片都会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找分区内对象的引用时便可通过记录卡片来查找该引用对象。每次对内存的回收,都是对指定分区的卡片进行处理。
8.4)G1分代模型
分代垃圾收集可以将关注点集中在最近被分配的对象上,而无需整堆扫描,避免长命对象的拷贝,同时独立收集有助于降低响应时间。虽然分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想。G1堆内存在逻辑上划分为年轻代和老年代,其中年轻代又划分为Eden空间和Survivor空间。当年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间。
整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化。且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩的大小以及分区的RSet计算得到。当然G1依然可以设置固定的年轻代大小(-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。
8.5)G1分区模型
G1堆内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位。
使用一个Per Region Table(PRT)来记录Region之间的引用,Card之间的引用,对象之间的引用, 以便GC线程根据GCRoot进行可达性分析。 GC 调优时会用到这个知识点。
巨型对象(Humongous Region)
一个大小达到甚至超过分区Region大小一半的对象称为巨型对象(Humongous Object)。
当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。
记忆集合(Remember Set,RSet)
在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
收集集合(Collect Set, CSet)
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区Region。
在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。
候选老年代分区的CSet准入条件,可以通过活跃度阈值
-XX:G1MixedGCLiveThresholdPercent
(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent
(默认10%)设置数量上限。
年轻代收集集合
年轻代收集集合 CSet of Young Collection:应用线程不断活动后,年轻代空间会被逐渐填满。
当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集。在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据任期阈值(tenuring threshold)分别晋升到PLAB中,新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。
同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候是到Survivor分区还是到老年代分区。年轻代收集首先先将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量
-XX:TargetSurvivorRatio
(默认50%)、最大任期阈值-XX:MaxTenuringThreshold
(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。
混合收集集合
混合收集集合 CSet of Mixed Collection:年轻代收集不断活动后,老年代的空间也会被逐渐填充。
当老年代占用空间超过整堆比IHOP阈值
-XX:InitiatingHeapOccupancyPercent
(默认45%)时,G1就会启动一次混合垃圾收集周期。为了满足暂停目标,G1可能不能一口气将所有的候选分区收集掉,因此G1可能会产生连续多次的混合收集与应用线程交替执行,每次STW的混合收集与年轻代收集过程相类似。为了确定包含到年轻代收集集合CSet的老年代分区,JVM通过参数设置混合周期的最大总次数
-XX:G1MixedGCCountTarget
(默认8)、堆废物百分比-XX:G1HeapWastePercent
(默认5%)。通过候选老年代分区总数与混合周期最大总次数,确定每次包含到CSet的最小分区数量;根据堆废物百分比,当收集达到参数时,不再启动新的混合收集。而每次添加到CSet的分区,则通过计算得到的GC效率进行安排。
8.5)转移失败的担保机制Full GC
转移失败(Evacuation Failure)是指当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。可以通过参数-XX:G1ReservePercent
(默认10%)设置保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义。
G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:
1、从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
2、从老年代分区转移存活对象时,无法找到可用的空闲分区
3、分配巨型对象时在老年代无法找到足够的连续分区
由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。
8.6)判断是否需要使用G1收集器?
官方描述 https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases
1、超过50%的Java堆内存被实时数据占用
2、对象分配或晋升的速度差异显著
3、长时间垃圾收集或压缩暂停(超过0.5到1秒)
9、如何选择垃圾收集器
官方文档:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
- 如果允许停顿时间超过1秒,选择并行或JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
10、如何设置垃圾收集器
1)串行
-XX:+UseSerialGC
-XX:+UseSerialOldGC
2)并行(吞吐量优先)
-XX:+UseParallelGC
-XX:+UseParallelOldGC
3)并发收集器(停顿时间优先)
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC