首先如何判断一个对象是否还被引用?


引用计数法

       对象头维护一个count,当对象被引用+1,引用失效-1,等到count=0就回收,但是两个对象相互引用会造成资源浪费

可达性分析

       通过GC Roots的对象作为起点,从这些起点开始乡向下搜索,搜索做过的路称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象时不可用的,会被判定可回收对象。

       作为GC Roots的对象包括:

虚拟机栈中的引用对象

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

方法去常量引用对象

本地方法栈中Native引用对象

  强引用

       垃圾收集器永远不会回首掉被引用的对象

  软引用

       在要发生内存溢出之前,才会把这些对象列进挥手范围之中进行第二次回收。

  弱引用

       生存到下次垃圾收集发生之前

  虚引用

       对象被收集器回收时会收到一个系统通知。

finalize

  finalize()方法决定对象是否会被回收,当对象没有覆盖该方法,或者一斤被虚拟机执行,那么该对象会被回收

  如果对象被判定需要执行该方法,对象会被放入一个队列中,按照较低优先级依次执行,不能保证全部都能执行,在出现内存告急的情况下, 会直接被回收

  执行finalize方法之后,将this引用赋该对象,则不会被回收

回收算法

  标记-清除算法

       标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示作者:

效率低,标记和清除两个过程的效率都不高;空间问题,标记清除过后会产生大量不连续的内存碎片,在大量随便无法存放需要连续内存空间的对象时,不得不触发GC

  复制算法

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

       将内存分为大小相等的两块,每次只使用其中一块,一块用完就讲还存活的对象复制到另一块,然后一次清理掉

       不用考虑空间碎片,但是内存只能使用一半

       新生代就是利用该算法,Eden+from survivor+to servivor,

       小对象放在放from survivor,大对象放Eden,然后MinorGC复制回收放入另一个to survivor,对象年龄+1

       当servivor空间不够用,老年代进行分配担保,直接将上一次新生代收集下来存活对象直接通过分配担保机制进入老年代。

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

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

  标记-整理算法

       标记:遍历GCRoot,存活对象标记

       整理:移动所有存活对象,依次排序,末端之后的地址全部回收

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

  分代收集算法

       根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。

       新生代:复制算法

       老年代:标记-清除算法、标记-整理算法

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

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

垃圾回收器

     新生代垃圾收集器有Serial、ParNew、Parallel Scavenge,G1,属于老年代的垃圾收集器有CMS、Serial Old、Parallel Old和G1.其中的G1是一种既可以对新生代对象也可以对老年代对象进行回收的垃圾收集器。然而,在所有的垃圾收集器中,并没有一种普遍使用的垃圾收集器。在不同的场景下,每种垃圾收集器有各自的优势,如下图 :

  Serial 收集器

       Serial是最基本也是发展最悠久的收集器。它是一种单线程垃圾收集器,这就意味着在其进行垃圾收集的时候需要暂停其他的线程

收集过程:暂停所有线程
算法:复制算法
优点:简单高效,拥有很高的单线程收集效率
应用:Client模式下的默认新生代收集器

  ParNew 收集器

       可以把这个收集器理解为Serial收集器的多线程版本,由于存在线程切换的开销,ParNew在单CPU的环境中比不上Serial,**且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数)

收集过程:暂停所有线程
算法:复制算法
优点:在CPU多的情况下,拥有比Serial更好的效果。单CPU环境下Serial效果更好
应用:许多运行在Server模式下的虚拟机中首选的新生代收集器

  Parallel Scavenge 收集器

       Parallel Scavenge收集器类似ParNew收集器,Parallel Scavenge收集器更关注系统的吞吐量。区别在于Parallel Scavenge收集器更关注可控制的吞吐量,

       新生代收集器,复制算法,并行多线程收集器,jdk1.4出现,并无法与CMS配合

       目标是达到一个可控的吞吐量 吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)

       -XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,以及直接设置吞吐量大小-XX:GCTimeRatio

       -XX:+UseAdaptiveSizePolicy参数打开后,就不需要手工指定新生代的大小、eden和survivor比例等参数,虚拟机会根据当前系统运行情况收集性能监控信息,以提供最合适的停顿时间或者最大的吞吐量

吞吐量 = 运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。

  Serial Old 收集器

       老年代收集器,单线程,标记-整理算法

       jdk1.5之前与Paraler Scavenge搭配使用,还可作为CMS收集器的后背预案

  Parallel Old 收集器

       老年代收集器,单线程,标记-整理算法,jdk1.6才出现,

       是Parallel Scavenge收集器的老年版本,在出现之前,Parallel Scavenge只能和Serial Old搭配使用,但是Serial Old性能上拖累,未必能在整体应用上获得吞吐量最大化的效果

通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。

  CMS 收集器

       老年代收集器,标记-清除算法

       并发收集,低停顿

       对CPU敏感,并发设计都对CPU比较敏感;无法处理浮动垃圾,只能通过下次GC处理;会有大量的空间碎片

初始标记:STP,仅标记GC Roots能直接关联到的对象,速度很快

并发标记:并发

重新标记:STP,修正并发标记用户继续操作而导致标记产生变动,比初始标记时间长,比并发标记时间短

并发清除:并发,与并发标记耗时最长

  G1 收集器

       标记-整理,并发并行

并行与并发:充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STP时间。G1可使用并发的方式让java程序运行

分 代 收 集:虽能够独立管理整个GC堆,但能够采用不同的方式去处理新创建的对象和已经存活一段时间、熬过多次GC的就对象以获取更好的收集效果

空 间 整 合:基于标记-整理算法,不会产生空间碎片

可预测停顿:能建立可预测的停顿时间模型

       将整个java堆划为多分区域(Region),新生代和老年代不是物理隔离,是一部分Region的集合。

       G1跟踪各个Region里面的垃圾堆积价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证G1收集器在有限的时间内可以获取尽可能高的收集效率

       G1使用Remembered Set来避免全标扫描,每个Region都有一个与之对应的Remember Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不用的Region中,是便通过CardTable把相关的引用信息记录到被引用Region的Remember Set中,当GC时,在GC根节点枚举范围中加入Remember Set即可保证不对全堆进行扫描