垃圾回收

一.对象已死么

1.1引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时候计数器为0的对象就是不可能再被是使用的。

1.2可达性分析算法:通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。下图,object5、object6、object7虽然互相有关联,但是他们到GC Roots是不可达的,所以它们将会被判定是可回收的对象。

在java语言中,可作为GC Roots的对象包括以下几种:

①.虚拟机栈(栈帧中的本地变量表)中引用的对象

②.方法区中类静态属性引用的对象

③.方法区中常量引用的对象

④.本地方法栈中引用的对象

 2.再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

3.生存还是死亡

要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一个标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。

如果这个对象有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列里,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。然后GC将对F-Queue中的对象进行第二次小规模的标记,然后对象两次标记之后被回收(除非finalize方法中把自己赋值给某个变量)

4.回收方法区

永久代的垃圾回收主要是回收两部分内容:废弃常量和无用的类。

回收废弃常量与回收java堆中的对象非常类似。例如:一个字符串“abc”已经进入常量池,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池的“abc”常量,如果此时发生内存回收,而且必要的话,这个常量将会被清理。

二.垃圾收集算法

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

  1.Mark-Sweep(标记-清除)算法

  这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

  从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作

  2.Copying(复制)算法

  为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

  这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半

  很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低

  3.Mark-Compact(标记-整理)算法

  为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

  

  4.Generational Collection(分代收集)算法

  分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation)老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法

  目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact标记整理算法

  注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

三.HotSpot的算法实现

1.枚举根节点 

从可达性分析中从GC Roots节点找引用为例,可作为GC Roots的节点主要是全局性的引用与执行上下文中,如果要逐个检查引用,必然消耗时间。 
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就行被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性就无法得到保证。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。 
由于目前主流的Java虚拟机都是准确式GC,做一档执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机应当有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。 
2.安全点 
在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。 
实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。 
对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。 
而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。 
3.安全区域 
使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。 
安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。 
当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止

 四.垃圾收集器

1.serial收集器(串行) jdk1.3.1之前

Serial收集器是一个单线程收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

特点:简单而高效,对于其他限定单个cpu的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,停顿时间完全可以控制在几十毫秒最多一百毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以serial收集器对于运行在client模式下的虚拟机来说是一个很好的选择。

2.parnew收集器

 parnew收集器其实就是serial收集器的多线程版本。

3.parallel scavenge收集器

 Parallel Scavenge收集器使用复制算法的收集器,又是并行的多项成收集器。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)。主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillils;设置吞吐量大小的-XX:GCTimeRatio

4.serial old收集器

 Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。主要有两大用途:一种是在JDK1.5之前与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

5.parallel old收集器(jdk1.6)

 parallel old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。jdk1.6之前,新生代Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器别无选择。由于老年代Serial Old收集器在服务端应用性能上的拖累,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果。

直到Parallel Old收集器出现后,“吞吐量”收集器终于有了名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,可以考虑Parallel Scavenge加Parallel Old收集器。

6.cms收集器

 CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的。

运作过程:初始标记;并发标记;重新标记;并发清除。其中初始标记、重新标记这两个步骤仍然需要“Stop the world”。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS优点:并发收集、低停顿。

CMS缺点:

1).CMS收集器对CPU资源非常敏感。其实 ,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。

2).CMS收集器无法处理浮动垃圾,可能出现“concurrent mode failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就会产生新的垃圾,这部分垃圾出现在标记过程之后,CMS无法在档次收集中处理,只好下次清理,这部分垃圾就交浮动垃圾。

3).CMS是一个基于“标记-清除”算法实现的收集器,所以收集结束时会有大量空间碎片产生。空间碎片过多时,就会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不Full GC。

7.G1收集器(jdk1.7)

 G1收集器特点:

1)并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop the world停顿的时间,部分其他收集器原本需要停顿java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

2)分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已经存货了一段时间、熬过多次GC的旧对象以获得更好的收集效果。

3)空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器。G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。

4)可预测的停顿:降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样,使用G1收集器时,Java堆的内存布局与其他收集器有很大区别,它将这个java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

 G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护了一个优先列表,每次根据允许的收集时间,有限回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器运作大致有几个步骤:

1)初始标记

2)并发标记

3)最终标记

4)筛选回收

 五.内存分配与回收策略

对象的内存分配,往大方向说,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

5.1对象有限在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少 一次的Minor GC(但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

5.2大对象直接进入老年代

所谓的大对象是指需要大量连续内存空间的java对象,最典型的就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的短命大对象,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前出发垃圾收集以获取足够的连续空间来存放它们。

5.3 长期存货的对象进入老年代

虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),将会晋升到老年代中。

5.4动态对象年龄判断

虚拟机并不是永远地要求对象的年龄必须大到了MaxTenuring Threshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThresoold中要求的年龄。

5.5空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果小于,或者HandlePromotionFalure设置不允许冒险,那这时也要改为进行一次Full GC。

 

 

 

 

总结:垃圾回收

1.判断对象是否死

  两种算法:引用计数算法;可达性分析算法

  四种引用类型:强引用、软引用、弱引用、虚引用

  生存还是死亡:两次标记之后判定对象已死

  回收方法区:永久代的垃圾回收效率比较低

2.垃圾回收算法

  标记-清除算法

  复制算法

  标记-整理算法

  分代收集算法

3.HotSpot算法实现

  枚举根节点:虚拟机当然有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。

  安全点:程序执行时

 

posted @ 2017-04-05 14:15  刘尊礼  阅读(120)  评论(0)    收藏  举报