垃圾收集器与内存分配策略
关于GC的三件事:
那些内存需要回收?
什么时候回收?
如何回收?
Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方栈3个区域随着线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行起会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大体上可以认为是编译期可知的),因此这几个内存区域的分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存就自然跟着回收了。而java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间时才知道会创建那些对象,这部分内存的分配和回收是动态的,垃圾回收器所关注的是这部分内存。
1、判断对象生存或者死亡的方法:
1.1引用计数法:给对象添加一个引用计数器,每当有一个地方引用过它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。在java虚拟机中不使用引用计数法的原因是它很难解决对象之间相互循环引用的问题。
1.2可达性分析算法(java、C#、Lisp):该算法的基本思想就是通过乙烯类成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可达。如下图所示,对象object5 object6 object7虽然相互有关联,但是它们的GC Roots是不可达的,所以他们将会被回收。
在java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈JNI(即一般说的Native方法)引用对象。
1.3再谈引用:在JDK1.2之后,java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。
强引用:强引用就是指在程序代码之中普遍存在的,类似于“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用:弱引用是用来描述非必须对象的,但是它的强度比软引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
1.4可达性分析算法中的不可达对象:在可达性分析算法中,有些不可达的对象,也并非是“非死不可”的,这时候它们都暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(不推荐使用finalize()方法)
1.5回收方法区(永久代):永久带的垃圾收集主要回收两部分内容:废弃常量和无用的类。
废弃常量:回收废弃常量与java堆中的对象非常相似。以常量池中的字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
无用的类:一个类需要同时满足下面3个条件才能算是“无用的类”:(1)该类的所有实例都已经被回收,也就是java堆中不存在改类的任何实例。(2)加载该类的ClassLoader已经被回收。(3)改类对应的java.lang.Class对象没有在任何地方引用,无法在任何地方通过反射来访问该类的方法。虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnclassgc参数进行控制。在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
2、垃圾收集算法
2.1标记-清除算法:最基础的收集算法是“标记-清除”算法,如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它有两个不足,一是效率太低,标记和清除两个过程;另一个是空间问题问题,标记清除之后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作
2.2复制算法:复制算法的出现是为了解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活着的对象复制到另外一块上面,然后把已经使用过的内存空间一次清理掉。这样使得每次都是对阵个半区进行内存回收,内存分配时也就不用考虑内存碎片的等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
2.3标记-整理算法:让所有存活的对象都向一端移动,然后直接清理掉端便捷以外的内存。
2.4分带收集算法:分代手机算法根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老生代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。
3、HotSpot的算法实现
上面介绍了对象存活判定算法和垃圾收集算法,而在HotSpot虚拟机上实现这些算法时,必须对算法执行严格有效的考量,才能保证虚拟机高效的运行。
3.1枚举根节点:从可达性分析算法中从GC Roots节点找到引用链这个操作为例,可作为GC Roots的节点主要在全局的引用(例如常量或类静态属性分)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间,因此HotSpot虚拟机实现,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据结构计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中那些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。还有就是可达性分析算法对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行-----这里“一致性”的意思是指在整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用还在不断变化的情况,改点不满足的话分析结果准确性就无法得到保证。
3.2安全点:前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点,即程序执行时并非在所有的地方都能停顿下来开始GC,只有在到达安全点才能暂停。对于安全点,另外一个需要考虑的问题是如何在GC发生时让所有线程都(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:“抢先中断式”和“主动中断”。
3.3安全区域:安全区域是指在一段代码段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。
4、垃圾收集器
垃圾收集算法是内存回收的方法论,而垃圾收集器就是内存回收的具体实现。
4.1、Serial(串行)收集器:串行收集器是一个单线程收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾回收时必须暂停其他所有的工作线程,直到它结束。Client端用的比较多的新生代收集器。
4.2、ParNew(并行新生代)收集器:ParNew收集器其实就是Serial收集器的多线程版本。主要运行在Server模式下的新生代收集器。
并行:指多条垃圾收集线程同时执行工作,但此时用户线程仍然处于等待状态。
并发:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户线程在继续执行,而垃圾收集程序运行于另外一个CPU上。
4.3、Parallel Scavenge(并行清除)收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓的吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
4.4、Serial Old收集器:Serial Old是serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在Client模式下的虚拟机使用。
4.5、Parallel Old收集器:Parallel Old是Parallel Scanenge收集器的老年代版本,使用多线程和“标记-整理”算法。
4.6、CMS收集器:CMS收集器是一种以获取最短回收停顿 时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,它分为4个步骤:初始标记,并发标记,重新标记,并发清除。
4.7、G1收集器