JVM-GC算法、以及常用的垃圾收集器
总结自深入理解JAVA虚拟机:JVM高级特性与最佳实践(第三版-周志明)
一、运行时数据区域
首先我们来了解下JVM的运行时的数据区域是怎样的
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
Java堆(GC堆)是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、FromSurvivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
方法区(Method Area)(PermGen永久代)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
JDK1.8中进行了较大改动
移除了永久代(PermGen),替换为元空间(Metaspace);
永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
永久代参数(PermSize MaxPermSize)->元空间参数(MetaspaceSize MaxMetaspaceSize)
二:垃圾收集算法
标记-清除算法
阶段一:标记 标记出所有需要回收的对象
阶段二:清除 标记完成后统一回收所有被标记的对象
存在的问题:
效率问题,标记和清除两个过程的效率都不高;
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法
阶段一:标记 标记出所有需要回收的对象
阶段二:复制 把存活着的对象复制到另外一块内存上面,再把已使用过的内存空间一次清理
为了解决效率问题,复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
存在的问题:
内存会缩小到原来的一半,代价太高
应用:
现在的商业虚拟机都采用这种收集算法来回收新生代,新生代的对象大部分是临时创建,用一次就淘汰的那种,所以并不需要按照1:1的比例来划分内存空间。
而是按照8:1:1的比例划分为80%Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
标记-整理算法
为了解决内存碎片问题,标记-整理算法在标记完存活对象后,对标记的对象进行整理。
阶段一:标记 标记出所有需要回收的对象
阶段二:整理 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
存在的问题:
分代收集算法:
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,按照各个年代采用合适的算法,一般是把Java堆分为新生代和老年代
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
三:垃圾收集器
Serial收集器:
是一个单线程的收集器,在进行垃圾收集的时候,会暂停用户线程(stop the world),直到收集结束。
ParNew收集器:
ParNew收集器其实就是Serial收集器的多线程版本
目前只有它能与CMS收集器配合工作
Parallel Scavenge收集器:
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,也是并行的多线程收集器
Parallel Scavenge收集器的特点是达到一个可控制的吞吐量(Throughput)。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%
参数设置-精确控制吞吐量-自适应的调节策略
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间的
-XX:GCTimeRatio 直接设置吞吐量大小
-XX:+UseAdaptiveSizePolicy 这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC(GC Ergonomics)
Serial Old收集器:
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法
Parallel Old收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS收集器:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
(Mark Sweep)CMS收集器是基于“标记-清除”算法实现的
运行过程分为四个阶段
阶段一:初始标记(CMS initial mark)(Stop The World)
阶段二:并发标记(CMS concurrent mark)
阶段三:重新标记(CMS remark)(Stop The World)
阶段四:并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
初始标记:标记一下GC Roots能直接关联到的对象,速度很快
并发标记:进行GC Roots Tracing的过程
重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除:并发清除可回收对象
耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
存在的问题:
CMS收集器对CPU资源非常敏感
CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent ModeFailure”失败而导致另一次Full GC的产生。
收集结束时会有大量空间碎片产生。
参数设置
-XX:+UseCMSCompactAtFullCollection开关参数(默认开启)进行FullGC时开启内存碎片的合并整理过程
-XX:CMSFullGCsBeforeCompaction 执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
G1收集器
Garbage First(简称G1)优先处理回收价值收益最大的那些Region
G1可以面向堆内存任何部分来组成回收集(Collection Set,简称CSet)进行回收,衡量标准不再是它属于那个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1收集器也遵循分代收集理论,但其堆内存的布局是把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region都可以扮演Eden空间、Survivor、老年代空间。
Region中还有一类特殊的Humongous区域,专门用来存储大对象(超过每个Region容量一半的对象)超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待
建立可靠的停顿预测模型?:G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论来实现的,-XX:MaxGCPauseMillis指定停顿时间,表示垃圾收集时间的期望值。
满足期望值?:在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,分析出平均值、标准偏差、置信度等统计信息。通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
运行过程分为四个阶段
阶段一:初始标记(Initial Marking)(Stop The World)
阶段二:并发标记(Concurrent Marking)
阶段三:最终标记(Final Marking)(Stop The World)
阶段四:筛选回收(Live Data Counting and Evacuation)(Stop The World)
除了并发标记外,其余阶段也是要完全暂停用户线程的(Stop The World)
并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。
最终标记:处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
筛选回收:更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
参数设置:
-XX:G1HeapRegionSize Region取值范围为1MB~32MB,且应为2的N次幂。
-XX:MaxGCPauseMillis 设定允许的收集停顿时间,默认值是200毫秒