垃圾回收算法

三:垃圾收集算法

1.对象已死?

  1. 引用计数算法:在对象中添加一个引用计数器,当有一个地方引用它时,计数值就加一,当引用失效时,计数器就 减一,计数器为零的对象就是不可能再被使用的。PS:虽然占用了一些空间来进行计数,原理简单,效率也高,但是单纯地引用计数很难解决对象相互循环引用的问题。
  2. 可达性分析算法:通过根对象(GC Roots)作为起始节点集,从这些节点开始,向下搜索,走过的路径叫引用链,如果某个对象通过GC Roots不可达,该对象就是不可能再被使用的。可作为GC Roots的对象: 1.在虚拟机栈(栈帧中的本地变量表)中引用的对象(参数,局部变量,临时变量);2.类静态属性引用的对象;3.在方法区中常量引用的对象(StringTable);4.Native方法引用的对象;5.虚拟机内部的引用(基本数据类型对应的Class对象,异常对象,系统类加载器);6.被同步锁持有的对象;7.返回虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存。PS:还有对象可以临时加入,某个区域的对象可能被堆中其他区域的对象。
  3. JDK1.2之后,Java对引用的概念进行了扩充,分为强引用,软引用,弱引用,虚引用。
    强引用:是程序代码中普遍存在的引用赋值,只要该关系存在,就永远不会回收引用的对象。
    软引用:描述一些还有用,但非必须的对象。被该引用关联的对象,在系统将要OOM之前,会把这些对象列入回收范围进行第二次回收,若还没有内存,才会OOM。
    弱引用:关联的对象只能生存到下一次垃圾收集为止。
    虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,唯一目的是该对象回收时收到一个系统通知。
  4. 要宣告一个对象死亡,至少要经历两次标记过程,如果进行可达性分析发现没有与GC Roots相连接的引用链,将会被第一次标记,随后筛选是否执行finalize()方法,假如没有覆盖该方法,或者已被调用过,将不会执行。
  5. 如果执行finalize(),该对象会被防止在一个叫F-Queue的队列之中,稍后会有一条虚拟机自动建立,低调度优先级的Finalizer线程去执行方法。PS:只是执行,并不承诺一定会等待它结束,原因:如果某个对象的finalize()的方法执行缓慢或者发生了死循环,很可能导致F-Queue队列中的其他对象永久处于等待,甚至崩溃。finalize只会执行一次且是对象逃脱回收的最后一次机会。(不被推荐使用)
  6. 方法区垃圾收集的性价比很低,主要回收两部分内容:废弃的常量(没有地方引用这个字面量)和不再使用的类型(1.所有的实例都被回收;2.加载该类的类加载器被回收;3.该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法)。类是否卸载可以通过-Xnoclassgc控制。
  7. 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载
    器的场景中,通常都需要Java虚拟机具备类型卸载的能力,保证不会对方法区造成过大的内存压力。

2.垃圾收集算法

1. 分代收集理论建立在3个分代假说之上:1.弱分代假说:绝大多数对象都是朝生夕灭的;2.强分代假说:熬过越多次垃圾收集的对象就越难以消亡;3.跨代引用假说:跨代引用相对于同代引用仅占极少数。
2. 不用为了少数的跨代引用去扫描整个老年代,也不必专门记录哪个对象存在跨代引用,只需要在新生代建立一个记忆集,这个结构把老年代分为若干小块,标识出那一块内存会存在跨代引用,加入到GC Roots进行扫描。
3. 标记清除算法:缺点:1.执行效率不稳定,标记和清除执行效率都随对象数量增长而降低;2.内存空间的碎片化问题。
4. 标记复制算法:将可用内存划分为大小相等的两块,每次只使用其中一块。当一块的内存用完了,就将存活着的对象复制到另一块上面,再把已使用过的内存一次清理掉。(简单,运行高效,空间浪费大)
5. 优化的半区复制分代策略:把新生代分为1块Eden区和2块survivor区,每次分配只使用Eden和一块survivor,发生垃圾收集时,将Eden和survivor中存活的对象一次性复制到另一块survivor区。PS:无法百分百报纸每次回收都只有不多于10%的对象存活,需要依赖老年代进行分配担保。
6. 是否移动对象都存在弊端,移动则内存回收时更复杂,不移动则内存分配更复杂。不移动对象停顿时间会更短,但是从整个程序的吞吐量来看,移动对象更好,因此关注吞吐量的Parallel Scavenge基于标记-整理,关注延迟的CMS基于标记-清除算法。
7. 和稀泥式解决方案:虚拟机多数时间都采用标记-清除算法,直到内存空间的碎片化程度已经影响对象分配时,再采用标记-整理算法收集一次,获得规整的空间。CMS就是使用该方法。

3.HotSpot的算法细节实现

  1. 所有收集器在根节点枚举都是必须暂停用户线程的。当用户线程停顿后,虚拟机使用OopMap来直接得到哪些地方存放着对象引用。一旦类加载动作完成时,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来。这样在扫描时就可以直接得知这些信息,并不需要一个不漏从方法区等GC Roots开始查找。
  2. 在OopMap的协助下HotSpot可以快速准确完成GC Roots枚举,但不可能会每一条指令都生成对应的OopMap,安全点:虚拟机只会在安全点记录OopMap信息,安全点的选定既不能让收集器等待时间过长,也不能太过频繁,一般会在指令序列的复用(方法调用,循环跳转,异常跳转)的指令产生安全点。
  3. 如何让所有线程都跑到最近的安全点停顿下来:1.抢先式中断:不需要执行代码配合,垃圾收集时,系统会中断全部用户线程,如果有线程不在安全点,就恢复执行,让它跑到安全点。(使用较少)
    2.主动式中断:不直接对线程操作,只是设置一个标志位,各个线程会不停地去轮询这个标志,一旦发现标志为真就在离自己最接近的安全点主动中断挂起,轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在堆上分配内存的地方,避免没有足够内存分配新对象。
  4. 当线程处于Sleep或Blocked状态,无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己。这时候就要引入安全区(在一段代码片段中,引用关系不会发生变化),当线程执行到这部分代码时,首先会标识自己已经进入了安全区域,这段时间内虚拟机发起垃圾收集就不必去管这些线程了,当要离开安全区域时,它要检查是否完成了根节点枚举(其他需要暂停用户线程的阶段),完成就继续,否则就一直等待。
  5. 记忆集是一种用于记录非收集区域指向收集区域的指针集合的抽象数据结构。收集器只需要判断某一块非收集区域是否存在收集区域的指针就行,不需要全部细节。
    卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。卡表是记忆集的一种具体实现,定义了记忆集的记忆精度,与堆内存的映射关系等。最简单就是一个字节数组。只要卡页中有一个对象存在跨代指针,就将数组元素的值标识为1,称为变脏。在垃圾收集时,只要筛选出变脏的元素,就把他们加入GC Roots扫描。
  6. 通过写屏障来维护卡表状态,直到G1出现之前,都只用到写后屏障。相当于AOP的环形通知。虚拟机会为所有赋值操作生成相应的指令。
  7. 除了写屏障的开销外,卡表在高并发场景下还有"伪共享"问题。现代中央处理器的缓存系统中是以缓存行为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响而导致性能低,这就是伪共享问题。如果处理器的缓存行为64字节,一个卡表元素占1个字节,64个卡表元素将共享一个缓存行,避免伪共享的简单解决方案:不采用无条件的写屏障,而是先检查卡表标记,只有当未被标记过,才变脏。
  8. 只有以下2个条件同时满足,才会产生"对象消失"的问题。(原本黑色标为白色)1.插入了至少一条黑色到白色对象的新引用。2.删除了全部灰色到白色对象的直接或间接引用。因此,产生了2种解决方案:1.增量更新:黑色对象插入新的指向白色对象的引用之后,就变回灰色对象了,并被记录。并发扫描结束后,就以记录过的对象为根,重新扫描。2.原始快照:当灰色对象要删除白色对象的引用,就将这个引用记录下来,并发扫描结束后,就以记录的对象为根,重新扫描一次。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值