0、写在前面
在详细介绍JVM的垃圾回收前,带着以下问题去学习:
-
为什么JVM需要垃圾回收?
-
哪些区域需要垃圾回收?
-
什么时候开始回收?
-
如何找到需要回收的对象?
-
垃圾回收有哪些算法?
-
什么是分代回收?
-
常用的垃圾收集器?
1、哪些区域需要回收?
JVM的内存区域:
- 程序计数器 : 负责记录【当前线程】【字节码指令】的【操作地址】,每个线程都拥有自己的【程序计数器】,这样才能保证各线程之间指令执行互不影响,字节码解释器工作时通过【改变这个计数器的值】来选取下一条需要执行的【字节码指令】;
- 虚拟机栈 : 每个方法【在执行时】都会创建一个【栈帧】,从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程;
- 本地方法栈 ;
- 堆 : 负责存储Java的对象实例;
- 元数据区(方法区): 负责存储 类信息、常量、静态变量;
其中 程序计数器、虚拟机栈 和 本地方法栈 这是三个区域 都是【线程私有(或者说线程隔离)】,参考下图:
【线程】的生命周期结束后,内存会自动释放,所以与每个线程私有的【虚拟机栈】、【本地方法栈】和【程序计数器】随着线程的结束而结束,自然不需要进行单独处理;
所以,GC需要关注的区域就是【堆】和【方法区】。
2、JVM的堆内存布局
JVM堆主要分为两个区域:
新生代(Young Generation):存活生命周期较短(可以理解为 年龄较小)的对象;
老年代(Old Generation):存活生命周期较长(可以理解为 年龄较大)对象的区域;
新生代又被分为两个区域:Eden区 和 Survivor区,其中新创建(年龄为1)的对象在Eden区创建,经过 Young GC 后仍然活着的对象,复制到 Survivor区;
2-1、内存布局
2-2、JVM内存控制参数
3、堆内存什么时候进行回收?
-
Eden区:当创建新的对象,没有足够空间进行分配时,虚拟机会发起一次【Minor GC】;
-
老年代:除了CMS以外,其他对于老年代的垃圾收集器(Serial Old、Parallel Old)都是到【老年代几乎完全被填满】再进行垃圾收集,而CMS垃圾收集器则是根据【-XX:CMSInitiatingOccupancyFraction】的值来判断是否要进行垃圾啊回收;
4、如何确认要回收的对象?
确认好需要GC的区域后,接下来要解决的是【哪些对象需要回收】的问题;
JVM通过【可达性分析算法】,从【GCRoots】作为起点开始搜索,最终那些与【GCRoots】不相连的对就是可回收的对象。
能作为GCRoots的数据有:
1、【方法区】中的【常量】和【静态变量】,因为他们不会轻易被回收,一旦被回收,它们的ClassLoader也被回收了;
2、【虚拟机栈】和【本地方法栈】中的引用对象,因为【虚拟机栈】和【本地方法栈】的生命周期与线程一致,如果它们还存活,说明线程还活着,需要调用对象去执行方法;
5、常用的垃圾回收算法有哪些?
常用的垃圾回收算法有:
-
标记-清除(Mark-Sweep);
-
标记-整理(Mark-Compact);
-
复制(Copying);
5-1、标记-清除(Mark-Sweep)
5-1-1、算法描述
1、通过GCRoots标记那些需要回收的对象;
2、然后将已标记的对象进行清除。
如图所示:
- 回收前内存状态
- 回收后内存状态
5-1-2、算法总结
缺点:
1、效率问题,【标记】和【清除】的执行效率都不高,导致GC时间变长;
2、因为清除后有大量【不连续】的内存区域,形成空间碎片,当有较大对象创建时,因为【连续空间】不足会触发再次GC;
5-2、标记-整理(Mark-Compact)
针对【标记-清除】算法会产生大量【空间碎片】的问题,JVM提供了【标记-整理】算法;
5-2-1、算法描述
- 首先【标记】出所有需要回收的对象;
- 所有【存活的对象】向一端移动,然后直接清理掉端边界以外的内存;
如图所示:
- 标记整理-回收前
- 标记整理-回收后
5-3、复制算法
JVM提供了【复制算法】用来解决【标记-清除】算法效率不高的问题。
5-3-1、算法描述
将内存按容量分为【大小相等】的两个区域,每次只使用其中一个区域,当一个区域内容用完后,将存活的对象【复制】】到另一块区域,然后把之前使用区域的内存清空。如图所示:
- 复制算法-回收前
- 复制算法-回收后
5-3-2、算法优点
-
没有内存碎片问题;
-
运行简单,效率较高;
5-3-3、算法缺点
内存空间浪费,因为始终都有一半的区域不能使用。
5-4、关于分代回收
上面三种算法各有优点和缺点,如果作为JVM的架构师,需要关心应该选择什么算法。
大多数Java对象都有【存活时间较短】的特点,这些对象回收很能产生很大内存区域,对这部分对象适合采用【复制】算法进行内存回收;
而针对那些存活时间较长的对象,因为回收的内存很少,适合【标记-清除】或【标记-整理】算法;
所以JVM把堆内存分为【新生代】和【老年代】:
【新生代】用来存放那些存活时间较短的对象,使用【复制】算法进行回收;
【老年代】用来存放生命周期较长的对象,使用【标记-清除】或【标记-整理】算法;
对象的生命周期,可以参考下图:
6、常用垃圾收集器总结
6-1、Serial
6-2、ParNew
6-3、Serial Old
6-4、CMS(Concurrent Mark Sweep)
关注点:
尽可能【缩短】垃圾收集时用户线程的【停顿时间】。
步骤:
- 初始标记(CMS initial Mark):只标记GC Roots【直接关联到】的对象,速度很快;
- 并发标记(CMS concurrent mark):进行【GC Root Traing】过程;
- 重新标记(CMS remark):修正【并发标记】期间因用户程序继续运行而导致【标记产生变动】的部分对象记录,时间较长;
- 并发清除(CMS concurrent sweep);
其中 【初始标记】与【重新标记】会【Stop-The-World】;
6-4-1、相关参数
-XX:+UseConcMarkSweepGC,【默认关闭】,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。如果CMS收集器出现【Current Mode Failure】,则Serial Old收集器将作为后备收集器;
-XX:CMSFullGCsBeforeCompaction=0 , 默认值为0(表示每次进入FullGC时进行碎片整理), 设置CMS收集器在【进行若干次垃圾收集后】在启动一次内存碎片整理; 仅在【使用CMS收集器时】生效;每次进入FullGC都进行碎片整理;在压缩前有过几次FullGC;
-XX:+UseCMSCompactAtFullCollection ,【默认开启】;FullGC时,对老年代进行压缩;因为CMS是【标记-清除】,不会整理内存,容易产生碎片;导致连续可用内存空间不足,此时内存压缩就会被启用;CMS收集器顶不住要进行FullGC时,开启内存碎片的合并整理过程,内存整理过程无法并发, 空间碎片问题没有,但是【停顿时间】变长;
-XX:CMSInitiatingOccupancyFraction,CMS不是等到老年代完全填满再进行收集,需要一部分预留空间,该值调高可以降低内存回收次数,从而提高性能;用百分比表示;
6-4-2、CMS收集器说明
6-5、G1
参考文档:
6-5-1、G1的内存结构
G1将整个堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念保留,但已经不是物理隔离的区域;
一个Region的大小可以通过参数【-XX:G1HeapRegionSize】设定,取值范围从【1M到32M,且是2的指数】。如果不设定,那么G1会根据Heap大小自动决定。
6-5-2、G1的特性
- 并发收集,缩短Stop-The-World停顿时间;
- G1堆的内存布局,是将整个Java堆划分为多个大小相等的独立区域(Region);
- 采用【标记-整理】算法,不会产生内存空间碎片(比起CMS的优势);
- 可预测的停顿;G1除了追求低停顿外,还能建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒;
为了实现【可预测】的停顿时间,G1有计划地避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里的垃圾堆积的【价值大小(回收获得的空间大小以及回收所需时间的经验值)】,在后台维护一个【优先列表】,每次根据【收集时间】,优先【回收价值最大】的Region。
6-5-3、Remembered Set
Region之间的对象引用,以及其他收集器新生代与老年代的对象引用,虚拟机都使用【Remembered Set】避免全堆扫描。
G1的每个Region都有一个与之对应的【Remembered Set】,进行内存回收时,在GC根节点的枚举范围中 加入【Remembered Set】保证不对全堆扫描也不会有遗漏。
6-5-4、GC过程
- 初始标记,与CMS一样,只标记【GC Roots】能直接关联到的对象;
- 并发标记,耗时较长,从【GC Roots】开始对堆中对象进行可达性分析,找出存活的对象;
- 最终标记,修正在【并发标记】期间因用户程序进行运作而导致【标记产生变动】的记录;
- 筛选回收,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
总结
学习和掌握GC回收:
1、根据不同收集器特性,选择系统合适的收集器;
2、通过GC参数调优,提升系统性能问题;
3、GC日志分析,找出内存中创建不合理的对象(大对象或频繁创建的对象);