垃圾回收策略(二)
垃圾回收:即收集已经“死去”的对象。Java内存运行时数据区中程序计数器、虚拟机栈、本地方法栈三个部分的随线程而生,随线程而灭。每个栈帧中分配多少内存在类结构确定时就是可知的,因此这三个区域的内存分配和回收都具备确定性,不需过多考虑回收问题,因为方法结束或线程结束时,内存就回收了。而Java堆和方法区则不一样,一个接口中的多个实现类的内存可能不一样,一个方法中的分支需要的内存也可能不同,只有在..
垃圾回收:
即收集已经“死去”的对象。Java内存运行时数据区中程序计数器、虚拟机栈、本地方法栈三个部分的随线程而生,随线程而灭。每个栈帧中分配多少内存在类结构确定时就是可知的,因此这三个区域的内存分配和回收都具备确定性,不需过多考虑回收问题,因为方法结束或线程结束时,内存就回收了。而Java堆和方法区则不一样,一个接口中的多个实现类的内存可能不一样,一个方法中的分支需要的内存也可能不同,只有在程序处于运行期间才能知道会创建哪些对象,这部分内存分配和回收都是动态的,垃圾收集器所关注的也是这部分内存。
判断对象是否死去(可回收):
判断算法主要有两种:
1.引用计数算法(据说废弃)。
2.可达性分析算法。
Java是通过可达性分析来判断对象是否存活的。
可达性分析算法:通过一系列的称为“GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,则证明此对象是无用的。如图:对象5、6、7虽然互有关联,但是他们到GC Roots是不可达的,所以被判定为可回收对象。
Java语言中,可作为GC Roots的对象包括下面几种:
1.虚拟机栈中引用的对象。
2.方法区中静态属性引用的对象。
3.方法区中常量引用的对象
4.本地方法栈中JNI引用的对象。
引用:
无论是通过引用计数算法还是可达性分析算法判断对象的引用链是否可达,判断对象是否存活都与“引用”有关。
Java中的引用定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表这一个引用。这种规定过于狭隘,一个对象在这种定义下只有被分为引用或者没有被引用两种状态。对于一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当空间还足够时,则保存在内存之中;如果内存空间在垃圾收集后还是很紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。JDK1.2之后Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
强引用:指程序中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉引用的对象。
软引用:描述一些还有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常以前,将会把这些对象进行回收,如果还是没有足够的内存,才会抛出内存溢出异常。此特性可用于进行数据缓存。如:SoftReference类。
弱引用:也是描述非必须对象,被弱引用关联的对象只能生存到下一次收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。如:WeakReference类。
虚引用:也成为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知。如:PhantomReference类。
对于对象的“自我拯救”,Java中用finalize()实现,对象类可以重写该方法实现一次救赎机会,但finalize()方法并不推荐使用。
方法区的垃圾回收:
很多人认为方法区(HotSpot中的永久代)是没有垃圾收集的,Java虚拟机规范中确实不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低。在堆中,尤其在新生代中,常规一次垃圾收集一般可以回收70~95%的空间,而永久代效率远低于此。
永久代主要回收两部分内容:
废弃常量
无用的类
1.回收废弃常量与Java堆中对象类似,以常量池中字面量回收为例,如果“abc”已经进入了常量池,如果系统没有任何一个String对象叫做“abc”,如果有回收必要,“abc”会被系统清理出常量池。
2.判定一个类是否是“无用的类”且需要卸载类的条件则相对苛刻许多。需要满足以下3个条件:
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2)加载该类的ClassLoader已经被回收。
3)该类对应的java.lang.Class对象没有在任何地方引用,无法在任何地方通过反射访问该类的方法。
在大量使用反射、动态代理、CGLIB等ByteCode框架、动态生成JSP以及PSGI这类频繁自定义的ClassLoader的场景都需要虚拟机具备类卸载的功能。
HotSpot中的实现:
可达性分析中会发生:
1.Stop The World。
2.枚举根节点。
Stop The World: 可达性分析对执行时间的敏感体现在GC停顿上,在整个分析期间,整个执行系统看起来就像冻结在某个时间点上,不能出现在分析过程中对象引用关系还在不断变化的情况。这导致GC进行时,必须停顿所有Java执行线程(Stop The World)。即使在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也必须要停顿。
至于为什么在GC时要发生STW,有一个很适合的比喻:你妈妈在给你打扫房间的时候,肯定让你在老实呆着,否则她一边打扫,你一边扔纸屑,这房间还能打扫完?
枚举根节点:简言之就是列举出所有“GC Roots”。在可达性分析中,通过GC Roots 节点找引用链判断对象在链情况。而可以作为GC Roots的节点主要是全局性引用(常量、类变量)与执行上下文(栈帧中的本地变量表)中,现在很多应用仅方法区就有数百兆。如果要逐个检查GC Roots节点,那必然会消耗很多时间。目前主流Java虚拟机使用的都是准确式GC,所以在执行系统停顿下来后,并不需要一个不漏的检查所有执行上下文和全局引用的位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的。在类加载完成时,HotSpot就把对象内具体偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息。
安全点:在Oop的协助下HotSpot可快速完成GC Roots枚举,但有一个现实的问题,Oop内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。事实上,HotSpot的确没有为每条指令都生成OopMap,而只在“特定的位置”记录这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在任何地方都能停顿下来开始GC,只有达到安全点才能暂停。SafePoint的选定既不能太少以至于让GC等待太长,也不能过于频繁以至于过分增大运行时负荷。所以安全点的选定是以程序“是否具有让程序长时间执行的特征”。如:方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。对于SafePoint,如何在GC发生时让所有线程,都跑到最近的安全点上再停顿。现在有两种方案可供选择:
1.抢先式中断
2.主动式中断
1)抢断式中断:不需要线程配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让其跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
2)主动式中断,不直接对线程操作,设置一个标志位,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的。
安全区域:使用SafePoint似乎已经完美的解决了如何进入GC的问题,但实际情况却并不一定。SafePiont机制保证了程序执行时,在不太长的时间内就会遇到可以进入的GC的SafePoint。但当程序“不执行”的时候,也就是线程没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“跑”到安全的地方去中断挂起,JVM显然不可能等待线程重新分配CPU时间。对于这种情况,就需要安全区域(Safe Resgion)来解决。安全区域是指在一段代码之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,我们也可以把SafeRegion看做事被扩展了的SafePoint。在线程执行到了Safe Region中的代码时,首先标记自己已经进入了Safe Region。那样,在这段时间里JVM要发起GC时,就不用管标记已为Safe Region状态的线程了,在线程要离开Safe Region时,首先会去查询是否可以离开的标志位,如果已完成GC或者未GC则可以安全退出,否则它就必须等到收到可以安全离开的信号为止。
更多推荐
所有评论(0)