HotSpot算法实现
枚举根节点:从GC Roots节点中找出引用链的操作。
GC Roots对象主要在全局性引用(常量/类静态属性)与执行上下文(栈帧中的本地变量表)中,虚拟机不会逐个检查这些地方,HotSpot使用OopMap数据结构来记录。当类加载完成时,OopMap中就记录下对象内为引用的位置,和栈、寄存器中引用的位置。当GC扫描时就可以直接得知引用信息。
Stop The World:在可达性分析期间,需要这期间的一致性——不能一边分析,对象的引用关系还一直变化,所以GC时必须停顿所有的Java执行线程。
安全点:为了避免对每条指令都生成OopMap,而选择的记录信息的特定的位置。只有在安全点,程序才会停下来GC。安全点的选定标准:是否具有让程序长时间执行的特征,例如,方法调用、循环跳转、异常跳转。
让所有线程都到最近的安全点再停顿的方法:
- 抢先式中断:先中断所有线程,再让不在安全点的线程跑到安全点中断。(几乎不用)
- 主动式中断:设置标志,各线程主动轮询标志,发现中断标志为真时就自己中断挂起。其中轮询标志的地方和安全点重合。
安全区域:针对不执行的线程(sleep或blocked状态)设置。当线程进入安全区时,就标识自己,GC时就不用管这些线程。当线程要离开安全区时,要先检查系统是否完成根节点枚举(或GC),要完成了才能安全离开安全区。
垃圾收集器
新生代垃圾收集器
- 对于单CPU环境,ParNew收集器不如Serial收集器效果好。其默认开启的GC线程数=CPU数量,也可以使用-XX:ParallelGCThreads设置线程数。
- 其他收集器专注缩短停顿用户线程的时间,Parallel Scavenge专注于控制吞吐量。吞吐量=CPU执行用户代码的时间/(运行用户代码时间+垃圾收集时间)。高吞吐量即意味着可以高效利用CPU,尽快执行完客户代码。
-XX:MaxGCPauseMillis:设定GC停顿的时间
-XX:GCTimeRatio:设定垃圾收集时间占总时间的比率,即吞吐量的倒数。
-XX:+UseAdaptiveSizePolicy:打开此参数后,虚拟机动态调整新生代大小、Eden与Survivor区的比例、晋升老年代对象大小等细节的参数。
老年代垃圾收集器
CMS收集器(Concurrent Mark Sweep)
- 以获取最短停顿时间为目标的收集器
- 适用于追求停顿时间最短的B/S系统服务端
-
基于标记-清楚算法
- 初始标记(CMS initial mark)
STOP THE WORLD,标记GC Roots能直接关联的对象,速度最快 - 并发标记(CMS concurrent mark)
进行GC Roots Tracing - 重新标记(CMS remark)
STOP THE WORLD,修正在并发标记期间有变动的对象标记记录 - 并发清除(CMS concurrent sweep)
回收对象
缺点
- 对CPU资源敏感
在并发标记和并发清理,会占用CPU资源,导致程序变慢,总吞吐量降低。CMS默认启动的回收线程数=(CPU数量+3)/4,当CPU较少时,会对程序产生很大影响。 - 无法处理浮动垃圾(Floating Garbage)
浮动垃圾指,在并发清理阶段产生的新的需要回收的对象。使用CMS收集器,需要预留一部分内存给用户线程使用,如果在并发清理期间,预留内存无法满足程序需要,就会出现"Concurrent Mode Failure",然后临时启用Serial Old收集器来重新进行老年代的垃圾收集,导致停顿时间变长。可以使用参数-XX:CMSInitiatingOccupancyFraction
来设置CMS触发的百分比。 - 空间碎片
CMS使用标记-清理算法,会产生大量空间碎片。空间碎片过多时会影响大对象的分配。使用-XX:+UseCMSCompactAtFullCollection
开关参数,在要进行Full GC前,开启内存碎片的合并整理过程。但此操作不能并发,会导致停顿时间增加。-XX:CMSFullGCsBeforeCompaction
则设置,在执行n次不压缩的Full GC后,执行一次碎片整理的Full GC。
G1收集器(Garbage-first)
适用于服务器端。
优点:
- 并行与并发
与CMS相似。 - 分代收集
可以不与其他收集器配合,独立管理整个GC堆,并在不同的代里使用不同的收集算法。 - 空间整合
G1运作期间不会产生内存空间碎片,有利于程序长时间运行,不会因为没有足够的空间分配给大对象而提前触发GC。 -
可预测停顿
可以建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段中,消耗在垃圾收集上的时间不超过N毫秒。
G1将Java堆划分为多个大小相等的独立区域(Region),虽然也分新生代、老年代,但他们都是一部分Region的集合,不存在物理隔离。G1根据各个Region中垃圾堆积的价值大小(回收后可以获得的空间大小和回收所需时间的经验值),在后台维护一个优先列表,每次根据用户允许的收集时间,优先回收价值最大的Region。其难点在于,Region不是孤立的,一个对象会被其他Region的对象引用。在做可达性分析寻找可回收对象时,需要扫描整个Java堆才能保证准确性。G1使用Remembered Set来避免全堆扫描。每个Region对应一个Remembered Set,当程序对引用类型的数据进行写操作时,虚拟机产生一个Write Barrier暂时中断操作,检查引用的对象是否处于不同的Region,如果是的话,就通过CardTable把相关应用信息记录到被引用对象所属的Region的Remembered Set中。进行内存回收的时候,在GC根节点枚举范围加上Remembered Set。
- 初始标记
- 并发标记
- 最终标记
STOP THE WORLD,可并发执行最终标记。修正并发标记期间有变动的对象记录,虚拟机将这些变化记录在线程Remembered Set Logs里面,再合并到Remembered Set中。 - 筛选回收
STOP THE WORLD,可并发执行。对各个Region的回收价值和成本进行排序,根据用户的需求来指定回收计划。
GC日志
// 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
[GC (Allocation Failure) [PSYoungGen: 7128K->616K(9216K)] 7128K->6768K(19456K), 0.0057651 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 616K->0K(9216K)] [ParOldGen: 6152K->6658K(10240K)] 6768K->6658K(19456K), [Metaspace: 2542K->2542K(1056768K)], 0.0066751 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
- GC/FULL GC:说明垃圾回收的停顿类型,full gc是发生了stop the world的。
- PSYoungGen/ParOldGen:发生GC的区域。
PSYoungGen:Parallel Scavenge收集器新生代
DefNew:Default New Generation,使用Serial收集器中的新生代
ParNew:ParNew收集器的新生代
ParOldGen:Parallel Old 的老年代 - 7128K->616K(9216K):GC前该内存区域已使用容量->GC后已使用容量(总容量)
- 7128K->6768K(19456K)(方括号外):GC前java堆已使用容量->GC后已使用容量(java堆总容量)
- 0.0057651 secs:GC所占用时间(秒)。
- [Times: user=0.00 sys=0.00, real=0.01 secs] :【用户态消耗CPU时间,内核态消耗CPU时间,墙钟时间(Wall Clock Time)】。墙钟时间 = 非运算等待耗时(磁盘IO/线程阻塞等待时间) + CPU时间。
内存分配与回收策略
-
优先在Eden上分配
优先在Eden上分配,Eden空间不够时,发起Minor GC,将存活对象移到Survivor中去。 -
大对象直接进入老年代
大对象,即需要大量连续内存空间的Java对象(如很长的字符串、数组)。通过-XX:PretenureSizeThreshold
参数,大于这个值的对象都会直接在老年代中进行分配。 -
长期存活对象进入老年代
计算对象的对象年龄计数器,新生代的对象每熬过一次Minor GC,年龄就加1。年龄超过-XX:MaxTenuringThreshold
参数的对象,就会被晋升到老年代。 -
动态对象年龄判断
在Survivor空间中,相同年龄所有对象的大小总和大于Survivor空间的一般时,大于这个年龄的对象就会被晋升到老年代。 -
空间分配担保
Minor GC前,虚拟机先检查老年代的最大可用的连续空间,是否大于新生代所有对象总空间。如果不成立,就查看HandlePromotionFailure
设置的值是否允许担保失败。如果允许,检查老年代最大可用的连续空间,是否大于历次晋升到老年代的对象的平均大小。如果大于,就尝试进行Minor GC,否则就进行Full GC。