分代
GC就发生在Heap和Metaspace
- XX:SurvivorRatio = Eden/S0(=S1) = 8(default)
- XX:NewRatio = Old/Young = 2(default)
术语
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
- 吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
- STW stop-the-world
java对象存活分析
引用计数法
- 在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。
- 缺点
- jdk从1.2开始增加了多种引用方式:软引用、弱引用、虚引用,且在不同引用情况下程序应进行不同的操作。如果我们只采用一个引用计数法来计数无法准确的区分这么多种引用的情况。
- 循环引用导致永远无法回收
可达性分析
- 通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
- GC Roots
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI(Native方法)的引用的对象
- HotSpot实现:
- 使用OopMap记录并枚举根节点
方法区太大了(几百兆),所以需要OopMap,在类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。 - Safepoint
“安全点”简单点说就是程序执行停顿下的这一个点。程序在执行时,并非在所有的地方都能停下来开始GC,只有到达这个“安全点“时才能停顿下来。安全点的选区既不能太少以至于让GC等待时间过长,也不能过于频繁以至于过分增大运行时的负荷。所以,”安全点“的选择基本上是以程序”是否具有让程序长时间执行的特征“为标准来选定的。因为每条执行指令执行的时间都非常地短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,而程序”长时间的运行“实际上就是指令序列的一个复用。例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生”安全点“。而当我们选取好”安全点“之后,我们又是怎样使所有执行线程跑到”安全点“时停顿下来呢?- 中断方式
- 抢先式中断:在GC发生时,首次会把所有的线程全部中断,如果发现有些线程中断点不是安全点,就恢复该线程直到安全点上停止。
- 主动式中断:在GC发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。
- Safe Region
- 安全区域是指在一段代码片中,引用关系不会发生改变,在这个区域内的任意地方开始 GC 都是安全的(比如Thread.sleep()的时候)。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为“安全区域”状态的线程了,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。
- 中断方式
- 使用OopMap记录并枚举根节点
算法
标记-清除算法(mark-sweep)
- 标记->在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。
- 遍历过程效率低。
- 会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,多触发几次垃圾回收。
复制算法(copying)
- 复制算法是为了解决标记-清除算法的效率问题的。基本新生代垃圾收集器全部是复制算法。
- 优点:快,没有碎片。
- 缺点:浪费内存。
- 现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。当然,90%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时(例如,存活的对象需要的空间大于剩余一块Survivor的空间),需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
标记-整理算法(mark-compact)
- 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。所以在老年代不用复制算法,而用标记-整理算法。
- 标记-让所有的存活对象都向一端移动,然后清理掉边界外的内存。
垃圾收集器
| 收集器 | 串并 | 新老 |算法|目标|适用场景|优缺点|JVM参数|推出时间|
| ————- | ——- | ——-|———–|————|————-|————-|
| Serial | 串行 |新生代 |复制算法|响应速度优先|单CPU环境下的Client模式默认收集器Java3前唯一选择|简单高效,单CPU首选|-XX:+UseSerialGC||
| Serial Old | 串行 |老年代 |标记-整理| 响应速度优先| 单CPU环境下的Client模式、CMS的后备预案||默认||
| ParNew | 并行 |新生代 |复制算法 |响应速度优先| 多CPU环境时在Server模式下与CMS配合||-XX:+UseParNewGC||
|Parallel Scavenge| 并行 | 新生代|复制算法 |吞吐量优先 |多CPU,大Heap(>10GB)在后台运算而不需要太多交互的任务|GC自适应的调节策略:Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)是和ParNew的最大区别,配合的老年代收集器只有Serial和Parallel Old|-XX:+UseParallelGC XX:MaxGCPauseMills XX:GCTimeRatio(0-100)这俩参数是矛盾的|JDK 1.4.0|
|CMS | 并发 | 老年代|标记-清除| 缩短GC时用户线程的停顿时间| 集中在互联网站或B/S系统服务端上的Java应用|配合的新生代只有ParNew和Serial|-XX:+UseConcMarkSweepGC|JDK1.5|
|Parallel Old | 并行 | 老年代|标记-整理| 吞吐量优先| 在后台运算而不需要太多交互的任务以及CPU资源敏感的场合||-XX:+UseParallelOldGC|JDK 1.6|
|G1 |并发 | both|标记-整理+复制算法 |响应速度优先| 面向服务端应用,将来替换CMS||-XX:+UseG1GC|JDK1.8|
CMS
基于“标记—清除”算法,分4步:
- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,要STW。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程。
- 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,要STW。
- 并发清除(CMS concurrent sweep)
缺点:
- CPU个数太少(<4个)的话,CMS收集器占用CPU导致应用程序变慢,总吞吐量会降低
CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。 - CMS收集器无法处理浮动垃圾
可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。 - 会产生大量空间碎片
- CPU个数太少(<4个)的话,CMS收集器占用CPU导致应用程序变慢,总吞吐量会降低
GC 参数
XX:+UseCMSCompactAtFullCollection
是非每次CMS GC以后整理内存XX:CMSFullGCsBeforeCompaction
几次CMS GC以后整理内存XX:+CMSClassUnloadingEnabled
允许Class元数据回收XX:UseCMSInitiatingPermOccupancyFraction
永久区占用率达到多少百分比启动CMS GCXX:UseCMSInitiatingOccupancyOnly
只有达到阈值才进行CMS GC
G1
https://www.oracle.com/technetwork/articles/java/g1gc-1984535.html
Young GC + mixed GC(新生代,再加上部分老生代)+ Full GC for G1 GC算法(应对G1 GC算法某些时候的不赶趟,开销很大);
- 优点
- 可预测的停顿
这是G1相对于CMS的另一大优势。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。 - 空间整合
G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,收集后能提供规整的可用内存。
- 可预测的停顿
- 执行过程:
- 初始标记(Initial Marking)
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,要STW。 - 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。 - 筛选回收(Live Data Counting and Evacuation)
筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
- 初始标记(Initial Marking)
Epsilon:低开销垃圾回收器
- 启动:
-XX:+UseEpsilonGC