GC
Java中一个接口的多个实现类所需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才会知道创建了哪些对象,这部分内存的分配时动态的,而程序计数器、虚拟机栈、本地方法栈这几个区域内存的分配和回收都是确定的,因此垃圾收集器关注的就是Java堆。
判断对象是否存活
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数法的优点是实现简单,判定效率高,COM(Component Object Model)、Python使用它进行内存管理。
缺点是它很难解决对象之间的相互循环引用问题
相互循环引用
假设对象A、B都有一个成员变量object
A a=new A();
B b=new B();
a.object=B;
b.object=A;
a=null;
b=null;
实际上这两个对象都不可能再被访问,但是因为它们互相引用导致引用计数不为0无法被回收。为了解决这个问题,Java采用下面的可达性分析算法
可达性分析算法
可达性分析(Reachability Analysis),基本思想(本质是图论)是通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当GC Roots 到一个对象不可达时,证明此对象不可用。
可作为GC Roots的对象包括
- 虚拟机栈中(栈帧中的本地变量表)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Java Native Interface)引用的对象
引用
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用
这个定义过于简单以至于没有实用价值。
JDK1.2之后对引用的概念进行了扩充
- 强引用:在程序代码中普遍存在的类似“Object obj = new Object()”这类的引用,只要强引用还在,垃圾回收器永远不会回收掉被引用的对象。
- 软引用 :描述还有用但并非必须的对象。仅在系统内存不足时进行清理。对应的类是SoftReference
- 弱引用:描述非必需对象。无论内存是否足够都会被清理。对应的类时WeakReference
- 虚引用:最弱的引用关系,一个对象是否有虚引用的存在完全不会对其生存时间造成影响,也无法通过虚引用来取得一个对象实例,为对象设置虚引用的唯一目的是能在这个对象被收集器回收时收到一个系统通知。对应的类是PhantomReference
引用强度:强引用》软引用》弱引用》虚引用
finalize-不可达对象逃脱回收的办法
一个对象真正被回收,至少需要经过两次标记
- 对经过可达性分析无法到达的对象进行第一次标记并筛选,如果对象覆盖了finalize方法且finalize方法没有被虚拟机调用过,有必要执行finalize方法,否则没有必要执行finalize方法,直接清理
- 如果对象被判定为有必要执行finalize方法,这个对象会被放置到一个称为F-Query的队列中,稍后由虚拟机创建的、低优先级的Finalizer会去执行(为了安全,会触发这个对象的finalize方法但不承诺会等待它运行结束)它,如果在finalize方法中对象重新与引用链上的任何一个对象建立联系,在第二次标记中就会被移除“即将回收”的集合,否则就真的被回收。
回收方法区
方法区的回收效率低、条件苛刻。因此一般回收较少。回收方法区(或者说永久代)在大量使用反射、动态代理、CGLib等字节码框架、动态生成JSP及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
回收条件
- 该类的所有实例都已经被回收(Java堆中不存在该类的任何实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
主要有
- 标记-清除算法
- 复制算法
- 标记-整理算法
标记-清除算法
标记需要回收的对象,标记完成后统一清除。
不足是
- 效率不高
- 内存碎片
复制算法
将内存分成大小相同的两块,每次使用一块,清理时将存活的对象复制到另一块,再将已使用过的内存空间清理掉。解决了标记-清除算法的缺陷,但是内存缩小为一半,代价太高。根据研究,不需要分为1:1,将新生代内存分为一块较大的Eden和两块较小的Survivor,Eden:Survivor=8:1,内存利用率90%。每次使用Eden和其中一块Servivor,清理时将Eden和Survivor中存活对象转移到另一块Survivor中。
标记-整理算法
标记后将存活对象向一端移动,直接清理掉端边界之外的内存,解决了内存碎片问题。
分代收集算法
根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代。针对新生代特掉采用复制算法,针对老年代采用标记-清理或标记-整理算法。
HotSpot的实现
Stop The World
可达性分析必须在一个能确保一致性的快照中进行,也就是说至少在枚举根节点(GC Roots)时,必须停顿所有Java执行线程,因此被称为“Stop The World”
标记的实现细节
OopMap
虚拟机通过OopMap直接得知哪些地方存存放着对象引用
安全点
OopMap只在安全点生成,程序执行时只有到达安全点才能GC。安全点的选定标准时一程序“是否具有将程序长时间执行的特征”为标准选定。最明显的特征就是指令序列复用,如方法调用、循环跳转、异常跳转等。
发生GC时让所有线程(不包括执行JNI调用的线程)“跑到”最近的安全点再停顿下来,有两种方案
- 抢先式中断
- 主动式中断
目前的主流时主动式中断
安全区域
在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。可以看作拓展的安全点
垃圾收集器
到目前为止没有最好的收集器,更没有万能的收集器,我们所选择的是对具体应用最合适的收集器。
下图上方是工作在新生代的收集器,下方是工作在老年代的收集器,连线表示两者可以协同工作。G1中并没有对新生代和老年代做过多区分,可以独立工作。
新生代收集器
Serial
复制算法 单线程收集器,最古老,依然是虚拟机运行在Client模式下的默认新生代收集器,工作时Stop The World,停顿时间较长。优点是简单而高效(没有线程交互的开销,专心做垃圾收集)
ParNew
复制算法 Serial的多线程版本,与Serial基本相同,但只有它才能和CMS收集齐协同工作,是运行在Server模式下的蓄奴及首选的新生代收集器
Parallel Scavenge
复制算法 新生代收集器,关注点是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+GC时间)),主要适合在后台运算而不需要太多交互的任务
老年代收集器
Serial Old
标记-整理算法 Serial Old 是Serial收集器的老年代版本。单线程,使用“标记-整理”算法,给Client模式下的虚拟机使用。
两大用途是
- JDK1.5及之前的版本与 Parallel Scavenge配合使用
- 作为CMS收集器的后备预案
Parallel Old
标记-整理算法 Parallel Old (JDK1.6)是Parallel Scavenge收集器的老年代版本。使用多线程和标记-整理算法,在此之前,如果新生代使用Parallel Scavenge,那么老年代只能使用Serial Old。Parallel Old 无法与CMS配合工作
CMS
CMS(Concurrent Mark Sweep),并发收集,低停顿,以获取最短回收停顿时间为目标的收集器。适合Java Web应用。
CMS基于“标记-清除”算法,具体步骤为
- 初始标记(STW,标记GC Roots能关联到的对象,速度快)
- 并发标记(与用户进程并发,进行GC Roots Tracing)
- 重新标记(GC线程并行,STW,修正并发标记阶段变动的对象记录)
- 并发清除(与用户进程并发,清理)
耗费时间:初始标记《重新标记《《并发标记~并发清理
缺点:
- CPU资源敏感,默认启动的线程数为(CPU数量+3)/4,为此有了增量式并发收集器(i-CMS),但是效果并不好,deprecated
- 无法处理浮动垃圾(并发清理阶段用户线程产生的垃圾,只能留到下次清理),为了给用户线程留出运行空间,CMS当老年代使用了68%就会被激活,JDK1.6中启动阈值提升到92%。如果CMS运行期预留内存无法满足程序需要,发生“Concurrent Failure”,虚拟机启动后备预案-临时启用Serial Old。
- 空间碎片,因为CMS式基于“标记-清理”。分配大对象时会触发一次[^Full GC],可通过设置开启空间整理选项
[^Full GC]:老年代GC,也称为 Major GC.对应的新生代GC 称为Minor GC
G1
G1(Garbage First)收集器技术发展的最前沿成果之一。
特点
- 并行与并发
- 分代收集
- 空间整理
- 可预测的停顿,让使用者指定在一个长度为M ms的时间片段里,消耗在GC的时间不得超过N ms。这几乎是实时Java(RTSJ)的收集器的特征。
内存划分
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还有新生代和老年代的概念,但不再是物理隔离的,他们都是一部分Region(无需连续)。
算法
G1收集器整体基于“标记-整理”算法,局部(两个Region)基于复制算法。
可预测的停顿的实现
G1有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region中的垃圾堆积的价值大小,并在后台维护一个优先列表,每次根据允许的时间,优先收集价值最大的Region。
如何避免全堆扫描(适用于各种收集器)
虚拟机使用Remember Set来避免全堆扫描。G1中的每个Region都有一个对应的Remember Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Varrier操作,检查Reference引用的对象是否处于不同的Region中,如果是便通过CardTable把相关引用信息记录到被引用对象所属Region的Remember Set中。内存回收时,在GC根节点的枚举范围中加入Remember Set即可保证不对全堆扫描也不会有遗漏。
运作步骤
- 初始标记(STW)
- 并发标记(与用户线程并发执行)
- 最终标记(GC线程并行执行。将并发标记阶段记录在线程Remembered Set Logs中的记录合并到 Remembered Set中 )
- 筛选标记(GC线程并行执行,(未来)也可以与用户程序并发执行。根据期望停顿时间制定回收计划)
GC日志
33.125: [GC [DefNew: 3324k->152k](3712k),0.0025925 secs] 3323k->152k(11904k),0.0031680 secs]
模式为
GC发生时间:[垃圾回收的停顿类型: [GC发生的区域: GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域内存总容量),该内存区域GC所占时间] GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量),Java堆GC所用时间]
垃圾回收的停顿类型
- GC
- Full GC,发生STW
- [Full GC(System)],发生STW
GC发生的区域,与收集器紧密相关
- DefNew:Default New Generation。Serial收集器新生代
- ParNew:Parallel New Generation。ParNew收集器
- PSYoungGen:Parallel Scavenge。Parallel Scavenge收集器
......
内存分配与回收策略
- 对象优先在新生代Eden区分配,空间不足时发起一次Minor GC。
- 大对象直接进入老年代
- 长期存活对象进入老年代(对象每经历一次Minor GC,年龄加一,达到晋升阈值进入老年代)
- 在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代
- 空间分配担保。发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,Minor GC确保是安全的。否则查看配置书否允许,如果允许查看老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果是尝试Minor GC,否则进行一次Full GC。