《深入理解Java虚拟机》学习笔记(二)
主题:垃圾收集器与内存分配策略
垃圾回收算法
垃圾回收最主要的就属回收Java堆中的对象,此外方法区也会进行回收,主要是回收不在引用的常量以及卸载类,在大量使用反射、动态代理、CGLib等bytecode框架的场景,需要保证虚拟机有回收方法区的能力。判断类可卸载的条件非常严苛,要同时满足:
- 不存在该类的实例对象
- 加载该类的ClassLoader已经被回收
- 对应java.lang.Class对象不存在任何地方被引用,无法通过反射访问该类方法
如何判断对象可回收
引用计数法
给对象添加个计数器,每被引用一次,计数器加1,引用消失,计数器减1。
问题:难以解决循环引用的问题。
根搜索算法
系统中,任何有用的对象,回溯其引用,都可以追溯到几个固定的区域,这些区域引用的对象定义为GC Roots,包括:
- 虚拟机栈中引用的对象
- 方法区类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈JNI引用的对象
从这些GC Roots对象向下搜索,遍历引用链,那些不可达的对象便是不再使用的对象
何时回收
经过跟搜索后,那些不可达的对象,不代表一定会被回收,只是被判了“缓刑”,真正回收前至少要经历两次标记的过程:
- 搜索后对不可达对象做第一次标记,并进行筛选,筛选出需要执行finalize()方法的对象(条件1.复写了finalize()方法,2.该方法为被虚拟机执行过),并将其放到F-Queue中去
- 虚拟机Finalizer线程(低优先级)去执行F-Queue中对象finalize()方法(但不保证方法执行完),对象可以在finalize()方法中将自己或其它对象重新与可达对象建立引用
- 对F-Queue中对象进行第二次标记,移除被救活的对象,而未被救活的对象将被回收
不建议使用finalize()方法,一方面是运行代价高昂(因为可能存在对象复活,所以必须进行两次标记才能回收),另一方面存在很大的不确定性(首先是垃圾回收的时机对用户程序来说一般是不可预测的,其次虚拟机不保证finalize()方法一定执行完,最后一个对象第一次尝试回收和第二次回收finalize()方法的执行也是不一样的)
如何回收
标记-清除(Mark-Sweep)算法
先标记要回收的对象,再统一回收标记的对象
- 标记和清除的效率都不高
- 内存会存在碎片
标记-整理(Mark-Compact)算法
标记阶段和MS算法一样,接下来将存活对象往一端移动,然后一次性清理边界以外的内存
- 内存碎片化问题解决了
- 整理的开销比较大
复制算法
1:1复制
将内存一分为二,对象仅在一份上分配,当这份空间不足时,将有用对象复制到另外一份,同时将当前这份一次性擦除(这个操作开销很小)。如此往复地在两份空间间复制。
- 算法简单快速,但需要保留一半空闲空间用于复制
Eden + Survivor分配
IBM统计数据,98%的对象是“朝生夕灭”的,因此没必要保留一半空间
- -XX:SurvivorRatio
- 分配担保机制
分代回收算法
根据对象存活周期不同将内存分块,不同的块采用不同的回收算法
通常分为新生代和年老带
内存分配与回收策略
垃圾收集器
- Serial 收集器
- 单线程新生代收集器
- 需要暂停用户线程
- Serial Old 收集器
- Serial收集器的年老代版本
- 需要暂停用户线程
- ParNew 收集器
- Serial多线程版(新生代)
- 需要暂停用户线程
- Parallel Scavenge 收集器
- Parallel Old 收集器
- CMS 收集器
- G1 收集器
blog comments powered by Disqus