垃圾收集基础
垃圾回收
如何判断对象已经死亡?
- 引用计数法:如果外部有引用就加一,到0的时候就说明不可能再被使用了,但是这样会存在循环引用的问题,就是A引用B,B引用A,但是两个都不会再使用了。
- 可达性分析法:通过一系列称为GC Roots 的根节点作为起始节点集,从这些节点开始。根据引用关系向下搜索,搜索过程所经过的路径被称为“引用链”,如果某个对象到GC Roots没有任何引用链相连,那么就说明是不可达的,也就是说不可能再被使用了
可以作为GC Roots的对象:- java虚拟机栈中的对象
- 在方法区被static修饰的对象
- 在方法区被final修饰的对象
- java虚拟机内部引用的对象
- 本地方法中引用的对象
java引用
- 强引用:类似于 Object obj = new Object()这种引用关系就被称为强引用,虚拟机即使冒着内存溢出的风险也不会回收他们
- 软引用:用softReference来描述的,这种引用关系比强引用弱一些,只有在虚拟机再报内存溢出异常前,才会把这些对象纳入要回收的范围
- 弱引用:用weakReference来描述的,比软引用还要弱一些,不管内存空间是否足够,只要发生GC就会被回收
- 虚引用:是一种最弱的引用,JVM是管理堆内内存的,但是java中提供了Unsaf类可以去申请堆外内存,在申请一块直接内存后,都会在堆内保存一个引用,这个对象被垃圾收集器管理,一旦这个对象被回收,那么这块直接内存也就被回收了
对象是否真的被回收?
即使在引用链中被判定为不可达对象,也不是“非死不可”,要真正宣告一个对象的死亡至少要经过两次标记,如果一个对象在进行可达性分析后被标记为不可达,就会被第一次标记,随后进行一次筛选,筛选条件是看对象是否有必要执行finalize(),如果对象没有覆盖finalize(),或者finalize()已经被虚拟机调用过了,那么就会被认为没有必要执行。
如果被判定为有必要执行,那么就先放入F-Queue中,并在稍后由虚拟机建立一条低调度优先级的线程去执行,但是并不承诺会等待结束。因为如果一个对象finalize执行缓慢,会导致整个回收子系统崩溃,收集器对F-Queue中的对象进行第二次小规模标记,如果对象在finalize中与其他引用链上的对象建立关联,那么就说明自救成功,如果没有,那么就说明自救失败
回收方法区
方法区回收主要是两部分内容:废弃的常量和不再使用的类型
常量回收
如果常量池中的常量没有任何引用指向它就说明可以被回收了
类回收
- 加载该类的类加载器被回收
- 该类产生的实例对象都已经被回收
- 该类的Class对象在任何地方都没有被引用
满足上述三个条件的无用类仅仅只是被允许回收,但是并不是直接就回收,具体的回收还要看JVM的参数配置
垃圾收集算法
分代收集理论两个分代假说:
- 弱分代假说:大多数对象都是朝生夕死的
- 强分代假说:熬过越多次GC的对象越是难以被回收
奠定了多款垃圾收集器的设计原则:将java堆划分出不同的区域,然后将回收对象按照年龄分配到不同的区域中存储。
标记-清除算法
分为两个阶段:首先标记出需要回收的对象,在标记完成后,统一回收掉所有被标记的对象
缺点:
- 执行效率不稳定,是随着回收对象增长而降低
- 内存空间碎片化严重
标记-复制算法
该算法是为了解决标记-清除算法面对大量可回收对象执行效率低的问题。
思想是将内存按容量分为大小相等的两部分,每次只使用其中的一块,当这一块内存用完了,那么就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次性全部清理。
缺点是浪费了内存空间
HotSpot虚拟机具体实现:
默认Eden区与Survivor是8:1,但是新生代真正可用空间为90%,也就意味着有每次回收都有不多于10%的对象存活,但是这谁也不能保证。那么就是说有可能存活超过10%的对象无法安放,因此提出了内存分配担保策略
内存分配担保策略
在进行Young GC 之前,先看老年代的最大连续可用空间是不是大于新生代所有对象的总和,如果大于就进行GC,如果小于,那么就要看JVM参数设置是否允许内存分配担保,如果允许,再判断老年代最大连续可用空间是不是大于历代晋升到老年代的对象平均大小,如果大于,就冒险进行一次GC,如果不参数不允许或者小于平均大小,就触发一次Full GC.
为什么说是冒险?
因为只是判断的历代晋升的平均大小,但是这次具体是多少并不知道,极端情况下,如果全部不被回收。
标记-整理算法
标记-整理也是先将所有需要回收的对象进行标记,但是后续并不是直接对可回收对象进行清理,而是让所有存活的对象都想内存空间一端移动,然后直接清理掉边界以外的内存
标记-整理算法是移动的,它使内存回收更复杂,标记-清理算法是不移动,它使内存分配更复杂。
从垃圾收集的停顿时间看,不移动停顿时间更短,甚至可以不停顿,但是从整个程序的吞吐量来看,移动对象更划算
因为内存分配和访问比垃圾收集器的频率高,这部分耗时增加会使总的吞吐量降低
所以注重吞吐量,应该采用标记-整理算法
注重用户体验(即停顿时间),应该采用标记-清除算法
Hotspot算法实现细节
根节点枚举
所有收集器在根节点枚举这一步骤时都必须暂停用户线程(why?)
因为枚举根节点的时候必须保持在某个快照中才能保证一致性。防止出现根节点和对象的引用关系还在变化
可达性分析中耗时最长的查找引用链过程已经可以和用户线程并发执行
oopMap
我们需要找的是栈中的引用对象,但是栈中除了引用对象,还有其他类型,如果进行全栈扫描太浪费时间,所以采取了空间换时间的的oopMap,在GC发生的时候,程序首先运行到最近的安全点停下来,然后更新oopMap,记录栈中哪个位置代表着引用,然后枚举的时候递归遍历每个栈帧中的oopMap就可以
安全点
选用标准:是否具有让程序长时间执行的特征
还有一个问题就是在发生GC的时候,怎么才能让线程都跑到最近的安全点停下来?
一种是抢占式中断
另一种是主动式中断,当垃圾收集器需要中断线程的时候,不对线程直接进行操作,而是设置一个标记位,各个线程执行过程中不停的主动去轮询这个标志,一旦发生中断标志为真就自己在最近的安全点主动中断挂起。
参考
<<深入理解java虚拟机>>
JVM 之 OopMap 和 RememberedSet

浙公网安备 33010602011771号