垃圾回收
JVM内存区域
判断需要对哪些对象进行垃圾回收(主要关注于Java堆)
有两种方法:
- 引用计数法(Reference Counting)(主流java虚拟机不使用此方法)
给对象添加一个引用计数器,每次引用计数器值加1;当引用失效时,计数器值就减1;当计数器为0时,对象就是不可能再被利用的。
无法解决对象之间相互循环引用的问题 - 可达性分析法(Reachability Analysis)
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
可作为GC Roots的对象包括下面几种: - 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态变量属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象
- 虚拟机内部的引用,如基本数据类型对应的Class对象
- 所有被同步锁(synchronized)持有的对象
GC管理的区域是Java堆,虚拟机栈、方法区、本地方法栈不被GC所管理,因此这些区域内引用的对象作为GC Roots,是不会被GC回收
引用类型:强、软、弱、虚
reference类型的数据中存储的数值代表的是另一块内存的起始地址,称该reference数据是代表某块内存、某个对象的引用
扩展引用的概念:强引用、软引用、弱引用、虚引用
- 强引用:传统的引用定义,Object obj = new objec()
- 软引用:有用但非必需的对象。在发生内存溢出异常前,这些对象被列入回收范围中进行第二次回收。如果回收后内存仍旧不足,会抛出内存溢出异常。
- 弱引用:描述非必需对象、强度比软引用更弱,关联的对象生存到下一次垃圾回收为止。
- 虚引用:无法通过虚引用来取得一个对象实例。设置虚引用关联的目的是这个对象被收集器回收时得到系统通知
可达性分析算法判断是否回收对象
宣告一个对象的死亡(回收)需要经过两次标记:
- 对对象进行可达性分析后发现没有与GC Roots相连接的引用链,那么将被第一次标记。随后进行一次筛选,判断该对象是否有必要执行finalize()方法。
- 如果对象没有覆盖finalize()方法或finalize()方法已经被虚拟机调用过了,那么虚拟机将这两种情况视为“没有必要执行”
- 如果对象被判定为有必要执行finalize()方法,那么该对象会被放置在一个名为F-Queue队列中,稍后由一个虚拟机自动建立的、低调低优先级的Finalizer线程去执行它们的finalize()方法。【这里的执行,指虚拟机会触发这个方法开始运行,但并不承诺等待它运行结束。(避免执行缓慢和死循环)】
- finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行二次小规模的标记,如果对象要在finalize()中成功拯救自己,只需要重新与引用链上的任何一个对象建立关联即可。
注:finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前最后事情(对象可以趁这个时机挣脱死亡的命运)
垃圾收集算法与分代收集理论
可划分为两种类型:
- 引用计数式垃圾收集(Reference Countiong GC)【主流Java虚拟机未涉及】
- 追踪式垃圾收集(Tracing GC)
分代收集理论
当代虚拟机垃圾收集齐遵循了分代收集(Generational Collection)理论进行设计。
建立在两个分代假说至上:
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象朝生夕灭
- 强分代假说(Strong Generational Hypothesis):熬过月多次垃圾收集的对象越难以消亡
拓展假说:
- 跨代引用假说(Integenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占少数
【根据这条假说,可以再新生代建立全局数据结构(记忆集,Remembered Set),把老年代划分成若干小块,标识出老年代哪一块内存会出现跨代引用。发生Minor GC时。包含了跨代引用的小块内存里的对象会被加入GC Roots进行扫描】
分代假说奠定了垃圾收集的原则:收集齐将Java划分出不同的区域,将回收对象依据其年龄(对象熬过垃圾收集过程的次数)分配到不同的区域中存储。【如Minor GC、Major GC、Full GC】
垃圾收集算法详述
- 标记-清除算法
- 标记-复制算法
- 标记-整理算法
标记-清除算法
算法分为标记和清除两个阶段:
- 标记所有需要回收(或存活)的对象。
- 标记完成后,清除所有被标记(未被标记)的对象。
缺点:
- 标记清除两个过程执行效率随对象数量增长而增长
- 内存空间碎片化【弥补方式:分区空闲分配链表】
标记-赋值算法
半区复制的垃圾收集算法,按容量大小划分为相等大小的两块,每次使用其中一块,当一块内存用完了,就将还活着的对象赋值到另外一块上面,再把已使用过的内存空间一次清理掉。
缺点:
- 如果内存中多数对象都是存活的,会产生大量的内存空间复制的开销【如果多数可回收,算法需要复制的就是占少数的存活对象】
- 空闲浪费多
注:改进
一块Eden空间和两块小的Survivor空间(8:1:1),每次分配内存只使用Eden和其中一块Survivor。垃圾收集时,将Eden和Survivor中仍存活的对象一次性复制到另外一块Survivor空间上,然后清理掉Eden和已使用过的那块Survivor空间。
【Survivor空间不够则需要老年代进行担保】
标记-整理算法
标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
缺点:
- 需要必须暂停用户程序
中和方式:
- 平时使用标记-清除算法,暂时容忍内存碎片的存片,直到内存空间的碎片化程度已经大到影响对象分配时,在采用标记-整理算法收集一次,以获得规整的内存空间。