- 简介
谈到垃圾收集器,不得不了解GC回收需要完成的三件事:
——哪些内存需要回收
——什么时候回收
——如何回收
为什么要了解CG和内存分配呢?
主要是因为需要排查各种内存溢出、内存泄漏问题,当垃圾收集成为系统达到更高并发量的瓶颈时,需要对其进行监控和优化。
- 哪些对象需要回收
如何判断哪些对象存活,哪些对象已死,下面介绍两种判断算法:
引用计数算法:
给一个对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减一;任何时刻计数器都为0的对象就是不可被使用的。
优点:实现简单,判断效率高
缺点:无法解决循环引用问题
根搜索算法(可达性分析法):
通过一系列的名为“GC Roots”对象作为起始点,从这些节点向下搜索走过的路径称为引用链,当一个对象到GC Roots没有任何的引用链相连,说明次对象不可用。
Java中,可作为GC Roots的对象包括一下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中的类静态属性引用的对象;
- 方法去中的常量引用的对象;
- 本地方法栈中JNI(Native方法)的引用的对象
引用
JDK1.2后,Java将引用分为强引用、软引用、弱引用、虚引用,这四种引用强度一次减弱。
强引用:程序中普遍存在的类似“Object obj = new Object()”这类的引用,只要强引用存在,垃圾收集器不会回收被引用的对象。
软引用:有用但非必需的对象。系统将要发生内存溢出之前,会将这些对象列入回收范围之中并进行第二次回收。JDK1.2之后,提供了SoftReference类实现软引用。一般可用于系统缓存。
弱引用:非必需对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾回收时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
JDK1.2之后,通过WeakReference类来实现。
虚引用:也称为幽灵引用或幻影引用,是最弱的一种引用关系。无法通过虚引用来取得一个对象实例。设置虚引用关联的目的是希望在这个对象被回收时收到一个系统通知。JDK1.2后,通过PhantomReference类实现。
对象存活还是死亡判断:
在根搜索算法中不可达的对象,并非是一定会回收的。要宣布一个对象死亡,至少要经历两次标记过程:如果对象没有与GC Roots相连接的引用链,将会被第一次标记并筛选,筛选条件是次对象是否有必要执行finallize()方法。
1).对象没有覆盖finalize方法或者finalized方法已经被虚拟机调用过,则没必要执行。
2).对象需要执行finalize方法,将会被放入F-Queue队列中,等待虚拟机 建立的FInalizer线程执行(虚拟机触发fianlize方法)。然后GC将对F-Queue中的对象进行第二次标记,如果要在fianlize中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可(如把this关键字赋值给某个类变量或对象的成员变量),那在第二次标记时会被移出回收的集合;否则就会被回收。
注意:任何一个对象的finalize方法只会被系统自动调用一次。
垃圾回收区域:
- java堆:不可达对象
- 方法区:废弃常量和无用的类。
判断常量回收:没有任何地方引用了该字面量
判断类回收:
1).该类所有的实例都已经被回收,即java堆中不存在该类任何实例;
2).加载该类的ClassLoader已经被回收;
3).该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足以上三个条件的无用类可以进行回收。
- 什么时候回收
新生代:对象优先分配到Eden区
老年代:大对象直接分配,对应设置参数:-XX:PretenureSizeThreshold
长期存活的对象,对象年龄计数器(默认15),对应参数:-XX:MaxTenuringThreshold
Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半时,大于或等于该年龄的对象直接进入老年代
Minor GC触发条件:Eden区没有足够空间进行分配
Full GC触发条件:
- 调用System.gc()
- 当老年代空间不足时
- 方法区空间不足时
- 多次通过minor GC后进入老年代的平均大小大于老年代的可用内存时
- 如何回收
- 标记清除算法
分为标记和清除两个阶段,首先标记出所有回收的对象,在标记完成后统一回收掉所有被标记的对象。
特点:标记和清除的效率都不高
标记清除后产生大量不连续的内存碎片,导致在需要分配较大对象时没有足够的连续内存而触发垃圾回收。
2.复制算法
新生代分为三部分:Eden区、Survivor From区、Survivor To区,默认比例是8:1:1,每次使用Eden区和其中一块Survivor区,回收时,将存活对象复制到另一块Survivor区,清除掉刚使用的Eden区和Survivor区,每次新生代可用内存空间为90%,当Survivor内存不够时,需要依赖其他内存(老年代)进行分配担保。
3.标记整理算法
标记过程与标记清除算法一样,但标记后不直接进行清理,而是让存活对象移动到一端,清理端边界意外的内存。
特点:解决了内存碎片问题
总结:java堆中分为新生代和老年代
新生代:每次垃圾回收都有大量对象死去,少量存活,选用复制算法;
老年代:对象存活率高,使用标记清除算法或者标记整理算法
常用的垃圾回收器:
垃圾回收器有如下几种:
上面说明:如果两个收集器之间存在连线,说明可以搭配使用。
- Serial收集器
是单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,垃圾收集时必须暂停所有其他的工作线程(简称STW,即Stop The World),直到收集结束。
优点:简单高效,当个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集可以获得最高的单线程收集效率。
新生代:复制算法
对应的Serial Old收集器:老年代,标记整理算法
2.ParNew收集器
是Serial收集器的多线程版本,只适用于新生代
可使用-XX:+UseConcMarkSweepGC选项来默认新生代收集器,也可使用-XX:+UseParNewGC选项来强制指定;
单CPU:由于线程交互的开销,比Serial收集器效果差
多CPU: 开启收集线程数与CPU数量相同,随着CPU数量增加,资源利用率高
3.Parallel Scavenge收集器
是并行的多线程收集器,目标是达到可控制的吞吐量。
吞吐量指CPU运行用户代码时间与CPU消耗总时间的比值,即运行用户代码时间/(运行用户代码时间+垃圾回收时间)。
控制吞吐量的参数:
最大垃圾收集停顿时间:-XX:MaxGCPauseMillis
吞吐量大小:-XX:GCTimeRatio
新生代:复制算法
对应的Parallel Old收集器:老年代,标记整理算法
4.CMS收集器
是一种以获取最短回收停顿时间为目标的收集器,采用标记清除算法。
采用标记清除算法,分为四个步骤:
——初始标记:只标记GC Roots能关联到的对象,速度很快,需要STW;
——并发标记:进行GC Roots Tracing的过程
——重新标记:修正并发标记期间用户程序继续运行导致部分对象标记产生变动的标记记录
——并发清除:
内存回收过程与用户线程是并发执行的;
优点:并发收集、低停顿
缺点:对CPU资源敏感;
无法处理浮动垃圾;
产生大量的内存碎片。
5.G1收集器
采用标记整理算法
精确的控制停顿:指定一个长度为M毫秒的时间片段内,消耗在垃圾回收上的时间不超过N毫秒
不牺牲吞吐量的前提下完成低停顿的内存回收:将整个java堆(新生代和老年代)划分为多个Region,在后台维护一个优先列表,每次根据允许的收集时间,优先收集垃圾最多的区域。
总结:
收集器 | 运行方式 | 区域 | 算法 | 目标 | 使用场景 |
Serial | 串行 | 新生代 | 复制 | 响应速度优先 | 单CPU的client模式 |
Serial Old | 并行 | 老年代 | 标记整理 | 响应速度优先 | 单CPU的client模式,CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制 | 响应速度优先 | 多CPU的server模式与CMS组合 |
Parallel Scavenge | 并行 | 新生代 | 复制 | 吞吐量优先 | 后台运算不需要太对交互的任务 |
Parallel Old | 并行 | 老年代 | 标记整理 | 吞吐量优先 | 后台运算不需要太对交互的任务 |
CMS | 并发 | 老年代 | 标记清除 | 响应速度优先 | 互联网站或BS服务端的java应用 |
G1 | 并发 | both | 标记整理 | 响应速度优先 | 面向服务端应用 |