说到垃圾收集需要完成三件事:
- 那些内存需要回收?
- 什么时候回收?
- 如何回收?
对象已死?
对象“死去”:不可能再被任何途径使用的对象
如何判断对象是否死亡了呢?
-
引用计数法
-
在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值就减一; 任何时刻计数器为零的对象就是不可能再被使用的。
这种情况下,没有别的变量引用这两块内存,但是他们两个相互引用,导致不能被回收。 -
但是java并没有使用,因为很难解决对象之间的相互循环引用的问题
-
-
可达性分析算法
思路就是通过一系列称为“GC Roots”的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为“引用链”(Reference Chain) , 如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时, 则证明此对象是不可能再被使用的。
固定可作为GC Roots的对象包括以下几种:- 本地变量表中引用的对象
- 在方法区中类静态属性引用的对象,比如java类的引用类型静态变量
- 在方法区中常量引用的对象,比如字符串常量池里的引用
- JNI中引用的对象
- 虚拟机内部的引用,比如基本数据类型对应的Class对象
- 同步锁持有的对象
- 反应java虚拟机内部情况的JMXBean等
引用
- 强引用
程序代码之中普遍存在的引用赋值,即Object a = new Object() - 软引用
- 弱引用
- 虚引用
举例说明这几个引用
生存还是死亡?
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
- 如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
- 这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。
- finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
需要注意的是一个对象的finalize()方法只会被调用一次,如果在第二次发现Gc Root与其没有连接的时候,那么他会被直接的回收。
回收方法区
在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方
法。
垃圾收集算法(重点)
垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。
分代收集理论
分代收集名为理论,实际上就是一套符合大多数程序运行实际情况的经验法则, 它建立在两个分代假说之上:
- 弱分代假说:绝大多数的对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
-
跨代引用
- 跨代引用相对于同代引用来说仅占极少数。
- 通过记忆集解决跨带引用
-
标记复制
- 它将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。 如果内存中多数对象都是存活的, 这种算法将会产生大量的内存间复制的开销, 但对于多数对象都是可回收的情况, 算法需要复制的就是占少数的存活对象, 而且每次都是针对整个半区进行内存回收, 分配内存时也就不用考虑有空间碎片的复杂情况, 只要移动堆顶指针, 按顺序分配即可。
- 这样实现简单, 运行高效, 不过其缺陷也显而易见, 这种复制回收算法的代价是将可用内存缩小为了原来的一半, 空间浪费未免太多了一点。
- 很多公司的新生代都是通过这个方法去处理
- 实现简单,运行高效
- 浪费空间
- Eden和2个survivor区 8:1
-
标记整理
- 移动对象比较消耗时间
- 得到的是一个比较规整的内存
- 很多都是先使用标记清楚的算法进行处理,等到内存中的碎片是在太多以至于影响分配了的时候,通过该方法整理一次
-
标记清除
- 对于需要回收的对象或者不需要的对象进行标记,然后进行清除
- 主要缺点有两个:
- 第一个是执行效率不稳定, 如果Java堆中包含大量对象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过程的执行效率都随对象数量增长而降低;
- 第二个是内存空间的碎片化问题, 标记、 清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
HotSpot算法实现
- oopMap
在HotSpot中使用一组称为OopMap的数据结构来达到得到哪些地方存放着对象引用的目的。 - 安全点
HotSpot并没有为每一条指令都生成OopMap,指示在“特定的位置”记录了这些信息,这些位置被称为安全点。- 抢先式中断
抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让他一会在重新中断,直到跑到安全电商。 - 主动式中断
当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮训这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
- 抢先式中断
- 安全区域
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化, 因此, 在这个区域中任意地方开始垃圾收集都是安全的。 我们也可以把安全区域看作被扩展拉伸了的安全点- 在这个区域内的线程只能等待遍历Oopmap结束才能出来,同时遍历OopMap不需要考虑在安全区域的线程
记忆集与卡表 写屏障
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡精度:每个记录精确到一块内存区域, 该区域内有对象含有跨代指针。称为“卡表”。用这种方式去实现记忆集, 这也是目前最常用的一种记忆集实现形式,
- 用来解决跨带引用的
在HotSpot虚拟机里是通过写屏障技术维护卡表状态的。
并发的可达性分析
- 三色标记
- 白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
- 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
- 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
为了解决并发扫描时的对象消失问题,有两种解决方案:
- 原始快照
- 无论引用关系删除与否, 都会按照刚刚开始扫描那一刻的对象图快照来
进行搜索。 - g1 shenandoah
- 无论引用关系删除与否, 都会按照刚刚开始扫描那一刻的对象图快照来
- 增量更新
- 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了
- cms