垃圾回收
GC垃圾回收的三种思考,哪些内存需要回收,什么时候回收,如何回收。
3.2 对象已死
堆里面存放着Java世界的所有对象实例,垃圾回收器在对堆进行回收之前,需要确定哪些对象会活着,哪些已经死去
3.2.1 引用计数法
给对象添加一个引用计数器,没有一个地方引用了该对象,计数器就加1,当引用失效的时候就减一,客观的来说引用计数法,是一个很不错的算法,效率也很高,主流的java虚拟机没有才用此引用计数法来进行管理内存,主要的原因是因为引用计数法无法解决对象中的相互引用问题。对象objA 和对象objB 都有字段instance,赋值令objA.instance=objB及objB.instance=objA,这两个对象除了这个引用外再无别的引用,实际这个两个对象已经不能访问了,但是他们相互引用着对方,导致计数器都不为0,于是引数算法无法通知GC进行垃圾回收。
3.2.2 可达性算法
这个算法的基本思路就是通过一系列的称为GC Roots的对象为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,就可以认为这个对象不可用的。
在java语言中,可以作为GC Roots的对象包括
虚拟机栈(栈帧中的本地变量表)中的引用对象
方法区中类静态属性的引用对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象。
3.2.3 引用
1.强引用就是指代码中普遍存在的,如 Object obj = new Object();这类的引用,只用引用还在,垃圾收集器永远不会回收调引用的对象
2.软引用用来描述一些还有用但是非必须的对象。对于这些对象如果将要发生内存溢出的异常时,将会对这些对象进行第二次回收,如果这次回收还没有足够的内存抛出内存溢出的异常
3.弱引用用来描述非必须的对象,但是强度比软引用还要弱一定,被弱引用所关联的对象只能存在到下一次垃圾回收之前。当垃圾回收器工作的时候,无论内存是否足够,都会回收被弱引用关联的对象。
4.虚引用也可称之为幽灵引用或者幻影引用,它是最弱的一种引用关系。设置虚引用关联的唯一目的就是能在这个对象被收集器回收的时候收到系统通知。
3.2.4 生存还是死亡
即使可达性分析算法中不可达的对象,也并不是“非死不可”的,这个时候他处于“缓刑”阶段,要真正的宣告一个对象死亡,至少需要两次标记过程:如果对象在进行可达性分析后没有与GC Roots相连的引用链,那它第一次标记并且进行一次筛选,筛选的条件就是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都被视为没有必要执行。
如果判定这个对象有必要执行finalize()方法,那么这个对象会被放在一个F-Queue的队列中,稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行它,这里所谓的“执行”是指虚拟机去触发他,但不承诺会等待他运行结束,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端)将可能导致F-Queue队列永久处于等待,甚者有可能导致内存崩溃,finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()方法中成功拯救自己---只要重新与引用链任何一个对象建立关联即可,譬如把自己的关键字付给某个类变量或者对象的成员变量,那么第二次标记的时候将被移除即将回收的集合;如果这个对象到这个时候还没有拯救自己,那么基本上它就真的被回收了,finalize()方法只会被虚拟机调用一次,该对象面临下一次回收的时候他的finalize()方法不会被再次执行。这个finalize()方法并不鼓励程序员操作他,建议忘记这个方法。
3.2.5 回收方法区
很多人认为方法区没有垃圾回收的,因为在方法区垃圾回收性价比一般比较低,在堆中对新生代进行回收一般能回收70%~80%的控件,而永久带回收的效率远远低与此。
永久带主要回收:废弃常量和无用的类。判断一个常量是否无用比较简单,比如一个String 常量在常量池里面有一个abc的值,如果在系统中没有一个叫做abc的常量,也没有其他地方去引用这个常量,那么这个常量就可以被清理出常量池。常量池中的其他类接口,方法,字段的符号引用也类似。
判断一个类是否是无用的就比较困难,需要满足下面三个条件
1.该类的所有实例化的实例都已经被会后,也就是java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收了。
3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类。
3.3 垃圾收集算法
3.3.1 标记-清除算法,
标记清除算法分为两个阶段,阶段一:首先标记需要回收的对象,阶段二:在标记完成之后统一回收对象。标记清除算法主要有两个不足,第一个是效率问题,标记和清除两个过程的效率都不高,第二个问提主要是空间问题,标记清除会产生大两不连续的空间碎片,连续空间碎片太多的化,如果需要分配一个大对象,就不得不提前出发另一次垃圾回收动作
3.3.2 复制算法
为了解决效率问题,提出了复制算法,将一块内存分为两块区域,当一块内存用完后,将剩余的对象复制到另一块内存区域上,然后将已经使用过的内存空间一次清理完成,这样每次都是完美清理一块内存区域,不会产生连续的空间碎片,实现简单运行效率快,缺点就是把内存区域缩小了,现代的商用计算机主要的新生代都采用此方式,IBM的研究表明的新生代的对象百分之九十八都是朝生夕死所有不需要按照1:1来划分空间,而是将内存区域划分为一块较大的内存区域和两块较小的内存区域,大的内存空间叫做Eden,两块较小的内存区域叫做Surivor空间,HotSpot将内存空间划分比例为8:1。
3.3.3 标记-整理算法
复制算法主要是对象存活率较高的对象,效率将会遍地,更为关键不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对所有对象都100%存活的极端情况,所以老年代就不能采用这种算法。
根据老年代的特点提出了另外一种算法就是标记-整理算法,标记整理算法主要是让所有存活的对象向另外一端移动,然后清理到端边界以外的内存。
3.3.4 分代算法
当前商业的逊尼基垃圾收集器都采用分代收集算法,这种算法没有什么新颖的思想主要是把不同的内存区域划分为几块,一般是把堆内存划分为,新生代和老年代,这样可以跟对各个年代的特点才用最适合的收集算法,新生代每次垃圾回收的时候都会有大量对象死去,所以才用复制算法,只需要付出很少存活对象的复制成本就可以完成收集,而老年代的对象存活率高,没有额外的空间进行分配担保,就需要使用标记清除和标记整理算法进行回收。
3.4 HotSpot算法的实现
对象存活判定算法和垃圾收集算法,而在HotSpot虚拟机上实现这些算法时,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。
3.4.1枚举根节点
可达性分析算法从GC Roots节点找引用链为例,可作为GC Roots的节点主要在全局性的引用(常量或者静态属性)与执行上下文(例如栈帧中的本地变量)。
可达性算法对执行时间还体现在GC上,因为这项工作必须在能确保一致性的快照中进行,这里的一致性的意思是指在真个分析期间整个执行系统必须看起来像被冻结在某个执行时间点,不可能出现分析过程中对象间的引用还不断的变化的情况,该点不满足的话分析结果的准确性就无法保证。这也就是导致GC进行时必须停顿所有线程的原因,即使是在号称几乎不会发生停顿的CMS收集器中,枚举根节点时必须要停顿。
目前主流的java虚拟机使用的都是准确式GC,所以当系统停顿下来的时候,并不需要一个不漏的检查完所有执行上下问和全局的引用位置,虚拟机应当是有办法的值哪些地方存放着对象的引用,在HotSpot中,是使用一组名字叫做OopMap的数据结构,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么对象数据计算出来,在Jit编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这用GC扫描就可以得知这些信息,
3.4.2 安全点
在OopMap的协助性,HotSpot就可以快速且尊却地完成GC Roots枚举,很现实的问题OopMap内容变化的指令非常多,如果为每一条指令生成OopMap,那么需要大量额外的空间,这样GC成本非常高。
实际上HotSopt也没有为每条指令都生成OopMap,只有在特定的位置信息记录,这些位置称之为安全点。安全点的选定基本上是是否具有让程序长时间执行的特征为标准进行选定的--因为执行指令的时间非常短暂,才能达到指令序复用,例如方法的调用,循环跳转,异常跳转等,所以具有这些功能的指令才能产生安全点。
如何才能让程序执行到安全点程序就停顿下来,抢占式终端和主动式中断,一般的虚拟机都才用主动式中断,当GC需要去中断线程的时候,不直接对线程进行直接操作,仅仅是简单地设置一个表示,所有的线程都去轮训这个标识,发发现中断的标识为真的时候,就中断挂起,轮训标识的地方和安全点是重合的,另外加上创建对象需要分配内存的地方。
3.4.3 安全区域
安全点似乎完美的解决了如何进入GC的问题,实际情况并一定,安全点只是保证了程序执行时,在不太长的时间内就遇到进入GC的安全点,如果程序不执行,安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域进行GC都是安全的,我们可以把安全区域看做是安全点的扩展。
网友评论