Java垃圾回收

411 阅读9分钟

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、算法描述

  1. 首先【标记】出所有需要回收的对象; 
  2. 所有【存活的对象】向一端移动,然后直接清理掉端边界以外的内存;

如图所示:

  • 标记整理-回收前

  • 标记整理-回收后

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)

关注点

尽可能【缩短】垃圾收集时用户线程的【停顿时间】。

步骤

  1. 初始标记(CMS initial Mark):只标记GC Roots【直接关联到】的对象,速度很快;
  2. 并发标记(CMS concurrent mark):进行【GC Root Traing】过程;
  3. 重新标记(CMS remark):修正【并发标记】期间因用户程序继续运行而导致【标记产生变动】的部分对象记录,时间较长;
  4. 并发清除(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

参考文档:

docs.oracle.com/javase/8/do…

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过程

  1. 初始标记,与CMS一样,只标记【GC Roots】能直接关联到的对象;
  2. 并发标记,耗时较长,从【GC Roots】开始对堆中对象进行可达性分析,找出存活的对象;
  3. 最终标记,修正在【并发标记】期间因用户程序进行运作而导致【标记产生变动】的记录;
  4. 筛选回收,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

总结

学习和掌握GC回收:

1、根据不同收集器特性,选择系统合适的收集器;

2、通过GC参数调优,提升系统性能问题;

3、GC日志分析,找出内存中创建不合理的对象(大对象或频繁创建的对象);