JVM 垃圾回收机制
垃圾回收机制
垃圾回收(Garbage Collection,GC)针对堆(以及方法区)中的垃圾数据进行清理,防止内存泄漏,有效的使用可以使用的内存。
Java 的引用类型
如果判断对象是否为垃圾,与对象是否被引用有关,Java 中有 4 种引用类型:
强引用(StrongReference):只要引用存在,垃圾回收器永远不会回收。Object obj = new Object();
弱引用(WeakReference):生存到下一次垃圾回收,无论当前内存是否足够,第二次垃圾回收都会回收被弱引用关联的对象。
软引用(SoftReference):在发生内存溢出之前进行回收,如果回收后没有足够的内存会OOM。
虚引用(PhantomReference):不会对对象的生命周期有任何影响,无法通过引用取到对象实例,在对象被回收前收到一个系统通知。
判断对象可回收
引用计数法
为对象添加一个引用计数器,每当有一个引用,计数器加 1,当引用失效计数器减 1。任何时候当计数器为 0 时,对象不再被使用。
但是这种做法可能会遇见循环引用的问题(例如:A引用B,B引用A),计数器始终不为 0。
可达性分析法
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法,通过一系列的 GC Roots 对象作为起点进行搜索,如果在 GC roots 和一个对象之间没有任何可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象。
作为 GC Roots 中的对象有:
虚拟机栈(栈帧中的本地变量表)中的对象;
方法区中静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈(即一般说的Native方法)中的对象
系统类加载器(Application ClassLoader)加载的对象
如何判断常量
运行时常量池中的废弃常量要回收,即常量池中的字符串没有任何 String 对象引用的话,是可以被回收的
如何判断类
类的回收与对象的回收方法不同,但是回收类至少要满足以下条件:
类的所有实例被回收
加载类的 ClassLoader 被回收
类对应的 java.lang.Class 对象没有被引用
STW
STW 是 GC 中很重要的概念,全称 Stop the world,即程序全局暂停时间,GC 优化算法都是围绕减少 STW 的时间或频率。
为什么需要 STW
如果没有 STW,会出现浮动垃圾(即标记完是存活对象,线程随之结束,可能对象已经变成了垃圾),回收性能差、效率低。
垃圾回收算法
标记-清除
最基础的垃圾回收算法,分为两个阶段:1. 标记存活的对象;2. 清除未标记的对象
标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。有如下缺陷:
空间问题,产生大量内存碎片,可能发生大对象不能找到连续的可用空间,从而导致 FullGC
复制
为了解决“标记-清除”算法内存碎片化的缺陷,将内存划分为两部分,每次只使用其中一部分,当内存满后将存活的对象复制到另一块内存区域,并清除当前区域,完成回收。有如下缺陷:
虽然不会出现碎片,但需要两倍的存储空间(只有一半的空间被利用)
当存活对象增多,效率会大大降低
优化的复制算法
在复制算法的基础上,使用三个分区进行处理(即新生代的空间划分方式 Eden/S0/S1),默认空间比例 Eden:S0:S1为 8:1:1,有效内存(即可分配新生对象的内存)是总内存的 90%(Eden 和 S0 区都可以分配新生对象)。
IBM 的研究表明,新生代中的对象 98% 是朝生夕死的,所以 8:1:1 的比例是十分合理的。(每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存是会被浪费的)。
标记-整理
结合了以上两个算法,标记阶段和“标记-清除”算法相同,区别是:标记后不清理对象,而是将存活对象移向内存的一端,使内存紧凑排列,然后清除其他区域的对象。
相比“标记-清除”算法会牺牲一些效率,但是不会出现碎片,提高内存利用率。
增量
每次垃圾回收一部分区域,减少 STW 时间。线程切换和上下文转换会造成垃圾回收时间增加。
分代收集
根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老年代(Old Generation)和新生代(Young Generation)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时会有大量垃圾需要被回收,因此不同区域可选择不同的算法。
垃圾收集器
分代收集法是目前大部分 JVM 所采用的方法,可根据应用分别为新生代和老年代选择合适的垃圾收集器(如上图)。新生代垃圾收集器和老年代垃圾收集器的组合使用,G1是一种特殊的垃圾收集器,未采用传统的新生代/老年代划分。最新版本的 JVM 提供的 G1 的升级版本 ZGC。
并行 vs 并发
首先理解并行和并发的概念:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上。
Serial 收集器
启用命令 -XX:+UseSerialGC,相当于 Serial+Serial Old
使用“复制”算法的“单线程”新生代垃圾回收器,在 jvm client 模式下的默认新生代收集器。
单线程回收,回收期间必须 STW(应用线程暂停),单个 CPU 的运行环境比较快,效率较高。图示:
ParNew 收集器
启用命令 -UseParNewGC,相当于 ParNew+Serial Old
Serial 收集器的“多线程”实现,采用“复制”算法的“多线程”新生代垃圾回收器。
多线程“并行“回收,依然会STW,在多 CPU 的运行环境下使用。图示:
默认开启的收集线程数与 CPU 的数量相同,可以使用 -XX:ParallelGCThreads
限制线程数
Parallel Scavenge 收集器
启用命令 -XX:+UseParallelGC,相当于 Parallel+Serial Old;-XX:+UseParallelOldGC,相当于 Parallel+ Parallel Old
采用“复制”算法的“多线程”新生代垃圾回收器,在 jvm server 模式下的默认新生代收集器。
重点关注的是程序达到一个可控制的吞吐量:吞吐量=运行时间 / (运行时间 + 垃圾收集时间)
自适应调节策略是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别,可设置最大停顿时间(-XX:MaxGCPauseMills
)和 GC 时间占比(-XX:GCTimeRatio
)。图示:
Serial Old 收集器
Serial Old 是 Serial 垃圾收集器老年代版本,使用“标记-整理”算法的“单线程”老年代垃圾回收器,在 jvm client 模式下的默认老生代收集器。在 Server 模式下,主要有两个用途: 1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用;2. 作为老年代中使用 CMS 收集器的后备垃圾收集方案。
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 的老年代版本,使用“标记-整理”算法的“多线程”老年代收集器。在 JDK1.6 才开始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。
CMS(Concurrent Mark Swep)收集器
启用命令 -XX:+UseConcMarkSweepGC,相当于 ParNew + CMS + Serial Old
基于“标记-清除”算法的“多线程”老年代“并发”收集器,重点关注的是程序获得最短 STW 时间,适合交互占比多的应用程序。
整个收集过程分主要为以下步骤:
初始标记(CMS initial mark,需要 STW,耗时短),只扫描直接和 GC Roots 关联的对象
并发标记(CMS concurrent mark,耗时长,不需要 STW),基于(1)的结果进行 GC Roots Tracing,所有可到达的对象都在本阶段中标记。
并发预处理(CMS concurrent preclean,耗时长),为了减少(3)的处理时间,标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
重新标记(CMS remark,需要 STW,耗时一般大于(1),远远小于(2)),重新扫描堆中的对象,进行可达性分析,标记活着的对象,处理可能新产生的垃圾。
并发清除(CMS concurrent sweep,耗时长,不需要 STW),激活用户线程,同时清理那些无效的对象。
并发重置(CMS concurrent reset),重置 CMS 收集器的数据结构,等待下一次垃圾回收。
补充说明:
标记清除会产生大量碎片,为大对象分配内存的时候,会出现老年代还有很大的空间,但是缺少足够大的连续空间,不得不开启一次 Full GC。CMS 提供以下两个参数:
-XX:+UseCMSCompactAtFullCollection 收集开关参数,用于在收集器进行 FullGC 完开启内存碎片的合并整理过程。
-XX:CMSFullGCsBeforeCompaction 参数用于设置执行多少次不压缩的 FullGC 后,执行一次带压缩的 FullGC。
G1(Garbage first) 收集器
启用命令 -XX:+UseG1GC
G1 相比与 CMS 收集器,两个最突出的改进是:
基于"标记-整理"算法,不产生内存碎片。
可以控制 STW 时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
版权声明: 本文为 InfoQ 作者【Alex🐒】的原创文章。
原文链接:【http://xie.infoq.cn/article/e60264a35017ed6af727e9601】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论