给对象添加引用计数器,一个地方引用它计时器值+1;当引用失效,计数器值-1;计数器为0的对象表示对象不可能引用。
缺陷:难以解决对象之间循环引用的情况
objA = objB;
objB = objA;
除此之外,这两个对象再无任何引用,但由于按照引用计数法他们的计数值都不为0,故不能被GC收集。
这个算法的基本思路是:通过一些系列"GC Roots
“的对象作为起始点,从这些节点向下搜索,搜索走过的的路径称为”引用链
"。当一个对象与GCRoots之间不存在一条引用链时,即它不可达GC_Roots,则它会被判定为可回收的对象。
根据图论理解:那些可以被回收的对象不存在一条路径到达GC_Roots。
GC_Roots对象可以是:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的Native方法)引用的对象
在JDK1.2之后,java对引用的概念进行扩充,将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Week Reference)、虚引用(Phantom Reference)。并提供相应的类用来实现我们的引用。
指在程序代码中普遍存在的,类似“Object obj = new Object()”这样的引用,只要强引用还在,GC就不会回收该对象。
用来描述一些有用但非必须的对象,对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象后列进回收范围之中进行第二次回收。如果这次回收还没有足够内存,才会抛出OOM。
用来描述非必须对象,但是它比软引用更弱一些,被弱引用关联的对象只能活到下一次GC发生之前。当GC时,无论内存是否足够,都会回收该对象。
也称为幽灵引用或幻影引用,它是最弱的一种引用关系。虚引用的存在,不会对对象的存活时间构成影响,但也不能通过虚引用实例化该对象。为该对象设置虚引用的唯一目的是:在该对象被GC回收时收到一个系统通知。
在可达性分析算法中那些不可达的对象,也并非是"非死不可",这时候它们处于"缓刑"阶段。真正宣告一个对象死亡,至少要经历两次标记过程:
在可达性分析过程中不可达的对象,会被标记一次,并且进行筛选,筛选条件:是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或者finalize()方法被虚拟机调用过,则虚拟机会视为"没有必要执行"。
对于有必要执行finalize()方法的对象,将会把这个对象放入F-Queue队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去触法它的finalize()方法,(但虚拟机并不承诺会等待这个方法执行结束,这样做的原因是由于一个对象的finalize()方法里面可能进行了耗时操作,从而导致F-Quene队列等待时间过长,甚至导致GC崩溃)在这个方法中对象可以进行自救,只要在该方法内把对象引用与GC_Roots上任意一个结点关联起来就会使它自救成功。它会在第二次标记时从"即将回收"的集合移除,如果此时自救失败,那对象基本上会被回收掉。
一个对象 finalize()方法只能调用一次,即第一次可以帮助对象逃离,但第二次就会失效。并且此方法的缺点是:使用需要高昂的运行代价,不确定性大,无法保证各个对象的调用顺序。
finalize()能做的工作,可以被try-finally或其他方式做的更好、更及时。
分为
标记阶段:标记出所有需要回收的对象
清除阶段:在标记完成后,统一进行回收所有被标记的对象。
不足:效率慢,标记和清除两个过程的效率都不高;空间碎片化,标记-清除后会产生大量不连续的内存碎片,会导致以后程序运行过程中需要给分配一个比较大的对象时,找不到一块足够大的连续内存分配给它,从而不得不提前触发一次垃圾收集操作。
复制算法的出现是为了解决上面存在的效率问题。
思想:将内存分为等大的两块,每次只用一块,当这一块用完时,就将还存活的对象复制到另一块上面,然后把满了的这块内存清理掉。
优点:实现简单,运行高效
不足:牺牲了一般的内存
发展:考虑到新生代的对象98%都死是"朝生夕死"的特性,将内存分为一个较大的Eden空间和两个较小的Survivor空间,每次使用Eden空间和其中一个Survivor,回收时将上述两部分内存上还存活的对象,复制到没使用的Survivor上,然后清理之前使用的那两块内存。
HotSpot虚拟机默认Eden和Survivor空间比例8:1。
即每次可以使用80%+10%的内存,用10%的内存来存储最终存活的对象。但是,我们没有办法保证每次回收存活对象占用内存不多于10%,当Survivor内存不够用时,需要依赖其他(这里指老年代)进行分配担保
。
根据老年代的特点,标记-整理算法被提出来了。
思想:标记过程与标记-清除的标记过程一样,然后让所有存活的对象都向一端移动,最后清理掉端边界以外的内存。
思想:根据对象存活周期的不同将内存划分为几块。
一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的算法。
新生代:每次垃圾收集时都有大批对象死去,只有少量存活,那就选用复制算法。
老年代:对象存活率高、没有额外空间对他进行担保,就必须采用"标记-清理"或者"标记-整理"算法来进行回收。
实现:可达性分析从GC_Roots节点找引用链操作。
注意:可达性分析会有GC停顿,因为这项分析工作必须在一个确保一致性的快照中进行—这里的“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,从而才可以保证分析结果的准确性。
由于目前主流java虚拟机使用的都是准确式GC,所以当执行系统停顿下来以后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机有办法直接得知那些地方存放着对象引用。
HotSpot虚拟机:使用一组称为OopMap(OOP:Ordinary Object Pointer,普通对象指针)的数据结构来存储这些信息,在类加载的时候,HotSpot就把对象内,什么偏移量上是什么类型数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样GC在扫描时就可以直接得知这些信息了。
概念:在"特定的位置"记录了这些信息(OopMap中存的信息),这些位置称为安全点。
GC停顿只能在到达安全点处才可以发生。
关于安全点需要考虑两个问题:
1)安全点的选定
:要求即不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。所以,安全点的选定基本上是以"是否具有让程序长时间执行的特征"为标准选定的。
因为每条指令的执行时间都非常短暂,所以单条指令(计算机指令)不符合安全点选定标准。
"长时间执行"的最明显特征就是指令序列复用,例如:方法调用、循环跳转、异常跳转等,所以具有这些功能的指令(如:HotSpot的轮询指令test指令)才会产生安全点。
2)如何在GC发生时让所有线程(不包括JNI调用的线程)都‘跑’到最近的安全点再停顿下来。
:两种解决放案:抢先式中断和主动式中断。
①使用抢先式中断:GC发生时,首先把所有线程全部中断,然后将中断不在安全点的线程恢复,让它‘跑’到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
②使用主动式中断:当GC需要中断线程时,不直接对线程操作,而是通过设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方也是和安全点重合的。
既然有了安全点,为什么还需要安全区域?
因为在实际情况中,GC回收时,可能会发生在某些程序‘不执行’(比如,线程处于Sleep状态或者Blocked状态)的时候,这时候线程无法‘走’到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间,对于这种情况就需要安全区域来解决。
总结一句话:使‘不执行’程序GC过程中处于安全位置。
安全区域:是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任何位置开始GC都是安全的。(可以看作是安全点的扩展)
具体操作:在线程执行到安全区域中的代码时,首先标识自己进入了安全区域,在这段时间里JVM要发起GC时,就不用管标识为安全区域的线程了,要离开时需要检查系统是否完成根节点枚举操作,完成了则线程继续执行,否则必须等待直到收到可以安全离开安全区域的信号为止。
这里讨论的收集器基于JDK1.7Update14之后的HotSpot虚拟机。下图为7种作用域不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
性质:单线程的收集器,新生代收集器。
算法:新生代采用复制算法,老年代采用标记-整理算法。
特点:Stop The World.
工作原理:它使用单个线程或cpu去完成垃圾收集工作,并且在它垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
优点:简单而高效(与其他单线程比)
适用于Client模式
的虚拟机的新生代
。
性质:多线程的收集器,其实就是Serial收集器的多线程版本。新生代收集器。
算法:与Serial收集器一样
特点:与Serial收集器一样,也具备Stop The world.
工作原理:就是在Serial收集器的单个线程或cpu的基础上变成多线程,多cpu完成垃圾收集工作,其他没有太多创新之处。
优点:随着使用cpu数量的增加,它对于GC时系统资源的有效利用还是很有好处的,它默认开启的收集线程数与cpu的数量相同,在cpu非常多的情况下,可以使用-XX:ParallelGCTheads参数来限制垃圾收集的线程数。
它也是Server模式
下的虚拟机首选的新生代
收集器。
性质:并行多线程,新生代收集器。
算法:复制算法
特点:它与ParNew看上去一样,但它与其他收集器的关注点不同,CMS收集器等关注的是尽可能地缩短垃圾收集是用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。
吞吐量=运行用户代码总时间/(运行用户代码总时间+垃圾收集时间)
它提供了两个参数用于精准控制吞吐量
,分别是:控制最大垃圾收集停顿的时间的 -XX:MaxGcPauseMilis参数和直接设置吞吐量大小的 -XX:GCTimeRatio参数。
①MaxGCPauseMilis参数允许的值是一个大于0的毫秒数,收集器尽可能保证内存回收花费的时间不超过设定值。
注意:这个值并不是设置的越小,系统垃圾收集速度就会越快。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。例如:原来10s收集一次,每次收集500MB,每次停顿100ms,现在5s收集一次,每次收集300MB,每次停顿70ms。停顿时间确实由100ms降到了70ms,但吞吐率根据公式计算也降了下来。
我们取内存大小1500MB,设用户运行时间为t,则每次收集300MB的吞吐率为t/(t+350);而每次收集500MB的吞吐率为t/(t+300)。显然,后者比前者的吞吐率大。
②GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如设置此参数为19,那么允许的最大GC时间就占总时间的5%(即1/(1+19))。默认值是99,就是允许最大1%的垃圾收集时间。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称为"吞吐量优先"收集器。
还有一个-XX:+UseAdaptiveSizePolicy参数
,它是一个开关参数,当这个参数打开后,就不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的允许情况收集性能信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式称为GC自适应调节策略
。
性质:单线程收集器,老年代
算法:"标记-整理"算法
特点:是Serial收集器的老年代版本。两大用途:①JDK1.5以及之前版本中与Parallel Scavage收集器搭配使用;②作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。其主要给Client模式
下的虚拟机使用。
性质:多线程收集器,老年代
算法:“标记-整理"算法
特点:是Parallel Scavenge收集器的老年代版本,这个收集器在JDK1.6中才开始提供的。
在此之前,新生代如果选择了Parallel Scavenge收集器(多线程),则老年代只能选择Serial Old(单线程)收集器,别无选择,因为Parallel Scavenge收集器不能与CMS(JDK1.5推出的老年代垃圾收集器)配合。由于老年代的Serial Old收集器在服务端应用性能上的‘拖累’,使得使用Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,因为单线程的老年代收集器无法充分利用服务器多cpu的处理能力,在老年代很大而且硬件比较高级的环境中,这种"新生代Parallel Scavenge+老年代Serial Old” 的模式甚至还不一定有 “新生代ParNew+老年代CMS” 的组合‘给力’。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑,“新生代Parallel Scavenge+老年代Parallel Old”。
目标:以获取最短回收停顿时间为目标的收集器。
需求背景:目前很大一部分java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统的停顿时间最短,以用户带来较好的体验。CMS就非常符合这类应用的需求。
工作过程(分为四个步骤):
①初试标记。使用单线程,需要“Stop The World”,初试标记仅仅只是标记了一下GC Roots能直接关联到的对象,速度很快。
②并发标记。使用并发技术,使并发标记线程与多个用户线程并发运行,进行了GC Roots Tracing的过程。
③重新标记:使用多线程,需要“Stop The World”,这个过程是为了修正并发标记期间用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿的时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
④并发清理:使用并发技术,进行GC回收。
在这四个阶段中,耗时最长的是并发标记和并发清理阶段,由于并发技术使得收集器线程可以与用户线程一起工作,所以总体上,CMS收集的内存回收过程是与用户线程一起并发执行的。
优点:并发收集、低停顿
缺点:①收集器对CPU资源非常敏感。由于使用并发技术,使在并发阶段,垃圾收集线程会占用一部分CPU资源,从而导致应用程序变慢。
CMS默认启动回收线程数是(CPU数量+3/4),也就是在CPU数量不少于4个的时候,并发回收时,垃圾收集线程占用不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是如果CPU数量少于4个比如2个的时候,就会有50%的CPU资源被分配给回收线程,这使得应用程序的执行速度一下降低了50%,这是非常难以接受的。
为了应付上面所述的情况,虚拟机提供了一种“增量式并发收集器”(i-CMS)的CMS收集器变种,所做的事情就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程独占资源的时间,这样使得垃圾回收的过程更长,但对用户应用程序的影响也会显得少一点,即用户应用程序执行速度下降没那么明显。实践证明,i-CMS效果一般,在目前版本中被声明为“deprecated”,即不在提倡用户使用。
②CMS收集器无法处理浮动垃圾,可能出现“Concurren Mode Failure” 失败而导致另一次Full GC的产生。
浮动垃圾:在并发清理阶段,用户线程产生的垃圾,由于出现在标记过程之后,CMS无法在本次收集中清理掉它们,只好留做下一次GC时候再清理掉的那一部分垃圾。
由于用户线程还需要运行,所以CMS收集器不能等老年代快满时再清理,它需要预留一部分空间提供并发收集时的应用程序使用。
-XX:CMSInitiatingOccuoancyFraction参数,这个参数值表示老年代清理触发百分比
JDK1.5中,默认设置上述参数值为68%,这是一个偏保守的设置。
JDK1.6中,CMS收集器启动阈值为92%,要是,CMS运行期间预留的内存不足,就会出现“Concurren Mode Failure” 失败,这是虚拟机启动预备方案:临时启用Serial Old收集器来重新对老年代的垃圾收集,这样停顿时间就很长了。
所以-XX:CMSInitiatingOccuoancyFraction
参数设置的太高容易导致大量“Concurren Mode Failure” 失败,性能反而降低了。
③由于基于“标记-清除”算法实现,所以收集结束时会导致大量空间碎片产生。空间碎片过多时,将会对大对象分配带来很多麻烦,往往会出现老年代还有很大空间剩余,但无法找到足够大的连续空间来分配给大对象,从而导致不得不提前触发一次Full GC。
为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection
开关参数(默认为开启状态),用于CMS收集器“Concurren Mode Failure” 失败后,要进行Full GC时开启内存碎片的合并整理过程,内存合并整理过程是无法并发的,空间碎片问题没了,但停顿时间不得不变长。
虚拟机设计者还提供了另一个-XX:CMSFullGCsBeforeComPaction
参数,它用于设置执行多少次不压缩的Full GC后,跟着来一次代压缩的,默认值为0,表示每次进入Full GC都进行碎片整理。
性质:G1是一款面向服务端应用的垃圾收集器。
与其他GC收集器相比,G1具备如下特点:
①并行与并发:G1能充分利用多CPU、多核环境的下的硬件优势,使用多个CPU(或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿java线程执行GC操作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
②分代收集:分代概念在G1中被保留,G1可以独立管理整个GC堆,它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
使用G1收集器时,Java堆的内存布局与其他收集器差别很大,它将整个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离了,它们都是Region(不需要连续)的集合。
③空间整合:与CMS的“标记-清理”算法不同,G1从整体上看是基于“标记-整理”算法实现的,从局部上看,是基于“复制”算法实现的。这两种算法都意味着G1运作期间不会产生内存碎片,也不存在大对象无法分配到连续空间而引发下一次GC。
④可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还建立了可预测的停顿时间模型,能让使用者在明确指定一个长度为M的毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是实时Java(RTSJ)的垃圾收集器的特征了。
实现过程中的问题:
1.可预测停顿时间模型建立:G1之所以可以建立该模型,是因为它可以有计划地避免在整个Java堆中进行安全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值大的Region。这种采用Region划分内存空间+按优先级回收的方式,保证了G1收集器在有限时间内可以高效收集。
2.Region不是孤立的,Region可以与整个java堆的任意对象发生引用关系,垃圾收集时并不能按照Region为单位,在不扫描整个Java堆的前提下,需要解决回收一个Region时的可达性分析。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set
来避免整个堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference(引用)类型的数据进行读写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便会通过CardTable把相关引用信息记录在到被引用对象的Region的Remembered Set中。当GC时,在GC根节点加入Remembered Set即可保证不对堆完整扫描也不会有遗漏。
在以前的分代收集中,这个问题也是存在的,只是在G1中该问题更加突出而已。如,回收新生代时不得不同扫描老年代那么Minor GC的效率会下降不少,所以解决办法是检查老年代中的对象是否引用了新生代中的对象,如果是,则添加到类似Remembered Set数据结构中。
G1收集器的运作过程:
初试标记、并发标记、最终标记和筛选回收四步。与CMS运作过程相似。
区别在于:
①并发标记阶段:是为了修正,并发标记期间因用户程序继续运行而导致标记产生变动的那部分标记记录,虚拟机将这段时间对象变化,记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可以并行执行。
②筛选回收阶段:首先对各个Region回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。为提高回收效率,会有线程停顿。
评价:如果你的应用追求低停顿,那G1现在已经可以作为一个选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。
前提:Client模式虚拟机,Serial/SerialOld模式(ParNew/Serial Old收集器组合的规则也基本一致)的内存分配和回收的策略。
大多数情况下,对象在新生代Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
例子:-Xms20M -XMx20M -Xmn 10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
即,java堆20M,不可扩展,10M给新生代,10M给老年代,Eden区与Survivor区按照8:1分配
然后我们连续new 3个2MB对象,再new 一个 4MB对象
这时虚拟机分配时,会先把3个2MB对象放入Eden区,此时Eden区还剩2MB空间,发生一次Minor GC,由于前面那三个对象都是存活的,并且Survivor区只有1MB,无法放下这三个2MB对象,所以通过分配担保机制,这三个2MB对象提前转移到老年代去。
最终:Eden区占用4MB存放那个4MB对象,Survivor空闲,老年代占用6MB存放3个2MB对。
大对象:需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。
大对象对虚拟机的内存分配是一个坏消息,经常出现大对象容易导致内存还有不少空间时,就提前触发GC以获取足够的连续空间来分配给它。
虚拟机提供了一个-XX:PretenureSizeThreshold
参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存复制操作。
-XX:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。
年龄计数器:对象在Survivor区中每‘熬过’一次Minor GC,年龄+1。
原理:当它的年龄增加到一定值(默认15岁),就会晋升到老年代中。对晋升老年代的年龄阈值,可以通过-XX:MaxTenuringThreshold
参数设置。
为了更好地适应不同程序的内存情况,虚拟机并不是永远地要求对象年龄必须达到阈值才能晋升到老年代,如果Survivor空间中的相同年龄所有对象的大小超过Survivor空间大小的一半,则年龄大于等于该年龄的对象就可以进入老年代。
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,则Minor GC可以确保是安全的。
如果不成立,虚拟机会查看HandlePromotionFailure
设置值(boolean类型)是否允许担保失败。如果允许,那么虚拟机会继续检查老年代最大可用的连续空间是否大于历次(之前)晋升到老年代对象的平均大小,如果大于,就尝试一次Minor GC。
如果小于,或者HandlePromotionFailure设置为不允许冒险,那这时也要改为进行一次Full GC。
关于冒险:由于虚拟机是比较老年代最大可用的连续空间是否大于历次(之前)晋升到老年代对象的平均大小,故这次能否成功并不是确定的,因为这次会有多少对象活下来在回收之前是不知道的。
如果允许冒险,并且根据以往经验是可行的,但是实际运行是不可行的,也会导致担保失败,那就只好重新发生一次Full GC。