JAVA 垃圾回收读书笔记

对象已死

  在JAVA代码运行中,会不停的建立对象,由于内存空间不是无限的,Java虚拟机必须不停的回收无用的数据空间。那么虚拟机是怎么判断对象空间是须要被回收的呢,也就是怎么样的数据算是垃圾数据呢?java

引用计数法

  引用计数法是指给没一个对象中添加一个引用计数器,每当一个地方引用了该对象,就让该对象的引用数+1,当一对象的引用值变为0的时候,就表示该对象已经没法访问。垃圾回收器就能够回收这个数据空间。这种算法简单粗暴,有些语言会使用该算法来判断对象是否已经死亡。程序员

  可是该算法有一个比较大的问题。当两个对象互相引用对方,且没有其余引用时,则此时这两个对象引用计数都是1,垃圾回收器将永远没法回收他们。以下方代码所示:面试

 1 public class ReferenceCounter {
 2 
 3     private ReferenceCounter reference;
 4 
 5     public ReferenceCounter getReference() {
 6         return reference;
 7     }
 8 
 9     public void setReference(ReferenceCounter reference) {
10         this.reference = reference;
11     }
12 
13     public static void main(String[] args) {
14         ReferenceCounter referenceCounterA = new ReferenceCounter();
15 
16         ReferenceCounter referenceCounterB = new ReferenceCounter();
17         referenceCounterA.reference = referenceCounterB;
18         referenceCounterB.reference = referenceCounterA;
19 
20         referenceCounterA = null;
21         referenceCounterB = null;
22     }
23 }
View Code

  由于引用计数法没法处理对象之间相互引用的问题,因此JAVA虚拟机没有使用该算法来校验对象是否能够回收。算法

可达性分析算法

  可达性分析算法是大部分JAVA虚拟机所采用的主流算法。该算法有一个根节点称之为GCRoots,经过一系列的根节点引用,若是经过GCRoots可以访问到的数据所有是有效数据,不可回收。反之,则为垃圾数据,等待垃圾回收器的回收。以下图所示:安全

  

  object一、object二、object3三个对象经过GCRoots能够访问到,所以这三个对象是不可回收的。object4,object5,object6经过GCRoots是不能访问到的,由于这几个对象是能够回收的。那么,GCRoots又是什么呢?在JAVA语言里,能够做为GCRoot的对象有这几种服务器

  • 虚拟机栈(栈帧中的本地变量表)中应用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(NATIVE方法)引用的对象

  当一个对象对于GCRoots不可达时,并发直接被垃圾回收。会先执行该对象的fnalize()方法,此时该对象有一次自救机会,将本身关联到GCRoot上。若是对象在finalize()方法中将本身关联到GCRoots上,该对象将不会被垃圾回收器回收。可是虚拟机并不保finalize()执行完毕以后才进行垃圾回收,所以finalize()方法并不能必定自救成功。而且若是一个对象被自救过一次以后,仍旧脱离GCRoot,第二次将再也不执行finalize()方法。finalize()方法运行代价高昂,不稳定性高,只是JAVA诞生之初为了让C/C++程序员接受而作出的一种妥协,有些说法说finalize()能够用来关闭外部资源,可是try{}finally{}能够执行得更好,JAVA程序员彻底能够无视finalize()的用法。多线程

方法区回收

  方法区的数据会被回收吗?(方法区即HotSpot中的永久代,JDK8已经移除,用元空间替代)
并发

  这是一道常见的老面试题,先说这道题的答案,方法区的数据是会被回收的。方法区回收主要分为两部分:无用常量和无用的类。框架

  回收无用常量:假如咱们在代码中生成一个常量"123456",该字符串会被储存在常量池中。当该常量已经没有被任何引用所持有,也就是代码已经没法经过引用获取到该常量的时候,这个常量就是能够被回收的。ide

  回收无用的类 : 回收一个无用的类相比起来就比较复杂,须要保证如下几点

  • 该类的全部实例都已经被回收
  • 加载该类的classLoad已经被回收
  • 该类对应的java.lang.Class没有被其余任何地方引用,没法在任何地方经过反射获取到该类

  知足上面三个条件的java类能够被回收,但不必定会被回收,须要经过-XnoClass参数进行控制,还能够经过-verbose:class 和 -XX:+TraceClassLoading、-XX:+TraceClassUnLoding查看类加载和卸载信息。在大量使用反射、动态代理、CGLib等byteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都须要虚拟机具有类卸载的功能,保证永久代不会溢出。

 

垃圾回收算法

标记-清除算法

   标记清除算法是最基础的算法,算法分为标记-清除两个阶段。第一阶段将全部须要回收的对象进行标记,第二阶段将全部被标记的数据进行清除。该算法有两个不足:一个是效率问题,标记和清除两个阶段的效率都不高。二是空间问题,该算法在清除以后会有大量的不连续空间。大量不连续空间可能会致使JVM在分配大对象的时候,没有足够的空间。

复制算法

  复制算法是将内存空间分为大小相同的两块,每次只使用其中一块,当垃圾回收的时候将不须要回收的数据复制到另一块内存中,清理剩余的内存。由于年轻代的数据大部分都是朝生夕死,因此该算法在不少商用虚拟机的年轻带上使用。常见的内存方式就是将年轻带分为Eden区和两个Survivor区,每次年轻带使用Eden区和一个Survivor区。当进行垃圾回收的时候,将Eden区和Survivor区中存活的对象复制到另一个Survivor区中(若是Survivor区中的内存不足,经过分配担保原则,将存在对象放入年老代)。HotSpot中的Eden和Survivor的内存空间比例为8:1:1,能够经过-SurvivorRatio进行调整,假设将这个数据改成4,则Eden和Survivor区域的大小就分为4:1:1。

标记-整理算法

  标记-整理算法感受更像是对标记-清楚算法的一种改良。标记整理算法在对内存进行标记完成后,将全部存活的对象往内存的一个地方进行移动,而后删除边界外的全部内存。由于年老代没有能够分配担保的内存,所以没办法使用复制算法。

 

分代收集算法

  分代收集算法就没啥好说的了,就是对年轻代和年老代使用不一样的收集算法进行收集。

HotSpot的算法实现

枚举根节点

  在如今的应用中,内存愈来愈大,分析对象是否能够回收时,若是使用上面讲过的可达性分析算法来计算,必然会花费不少时间。并且为了保证JVM在计算时候,内存中的对象引用不是在一直变化中,必须暂停全部工做线程。这是STOP THE WORLD的一个重要缘由,为了快速分析哪里内存能够回收,哪里内存不能够回收,HotSpot引入了一个OoPMap来记录。在类加载过程完成的时候,HotSpot就将对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程当中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。而后当GC发生时候,就不须要扫描整个栈,而只须要扫描OopMap就能够获取到想要的信息,完成可达性分析。

安全点

  在OopMap的协助下,HotSpot能够快速且准确的完成GCRoot枚举。可是若是为每一条指令都生成OopMap,那么将须要大量的额外空间,这个GC的空间成本将会很是高。其实,HotSpot只有在特定的位置上记录这些信息,这些位置称为安全点,也就是并不是在全部地方都能停顿下来进行GC,必须停留在安全点才能进行GC,通常选择在执行时间长的地方生成安全点,如如下:

  • 循环的末尾 
  • 方法临返回前 / 调用方法的call指令后 
  • 可能抛异常的位置

对于安全点的另一个考虑,是让GC时候,让全部的线程都跑到一个安全的地方再停下来。有两种方案:抢断式中断和主动式中断。

抢断式中断:不须要其余线程配合,当GC发生的时候,先暂停全部的线程,若是线程不在安全点上,则让线程跑到下一个安全点。(几乎没有虚拟机采用这种方式)

主动式中断:GC发生的时候,设置一个标识符,全部的线程跑到安全点的时候就去轮询这个标识符,发现GC标识符为真的时候,就将本身挂起。轮询标识符的位置和安全点的位置和建立对象须要分配内存的地方。

安全区域

  在使用SafePoint时候,能够基本实现GC的安全执行。但还有一种状况,当线程没有分配CPU时间的时候,例如线程处于Sleep或者Block状态的时候。JVM将这种一段时间内关系不会发生变化的区域称为安全区。当线程执行到安全区,先将本身标识进入Safe Region,表示已经进入了安全区域。当这段时间里发生了GC,就不用管标识本身为Safe Region状态的线程。若是线程要离开安全区域的时候,要先检查系统是否完成了根节点枚举,或者完成了整个GC。

垃圾收集器

  如上图所示,年轻代采用的垃圾收集器为Serial、ParNew、Parallel Scavenge。年老代的垃圾收集器为CMS、Serial Old、Parallel Old。G1收集器能够对年轻代和年老代共同收集。图中的连线表示能够搭配使用,例如能够年老代若是选用CMS,则年轻代可使用serial和ParNew来进行垃圾收集。

新生代收集器

  Serial垃圾收集器:serial垃圾收集器是历史最古老的垃圾收集器,并且从名字就能知道它是经过单线程进行垃圾收集。Stop The World这个词在当时就从这里产生。虽然这个收集器很老了,可是由于它的简单高效,而且客户端的内存通常不会太大,因此在客户端仍是默认的垃圾收集器。Serial收集器在新生代使用复制算法。

  ParNew收集器:ParNew跟Serial相比,惟一的区别就是将Serial的单线程变动为多线程,ParNew和Serial之间也公用不少代码。可是它确是JDK8以前新生代首选收集器,由于只有ParNew和Serial能和CMS收集器配合。在单CPU的环境中,效率不如Serial,即使双核也不能保证必定比Serilal强,可是随着核数逐渐增多,ParNew多线程的优点才逐渐体现。

  Parallel Scavenge收集器:Parallel Scavenge所用算法跟ParNew差很少,可是Parallel Scavenge所要达到的目标是实现可控制的吞吐量,吞吐量=(运行代码时间)/(运行代码时间+垃圾回收时间)。停顿时间短的虚拟机适合用于用户交互,吞吐量大的收集器适合用于服务器计算。

老年代收集器

  SerialOld垃圾收集器:Serial Old收集器是Serial的老年代版本,一样是单线程收集器,它采用的是标记整理算法。

  Parallel Old垃圾收集器:Parallel Old是Parallel Scavenge老年代版本,使用多线程的标记-整理算法。直到Parallel Old出现,Parallel Scavenge在有了合适的应用组合。在Parallel Old出现以前,Parallel Scavenge只能和SerialOld配合,总体性能被拖累,并不实用。

  CMS垃圾收集器:CMS收集器是以最短回收停顿为目标的收集器不少B/S系统的服务器上很注重响应速度,长时间的回收停顿是没法接受的,所以CMS垃圾收集器就很是符合他们的需求。CMS基于标记-清除算法,相比其余收集器稍微复杂。CMS回收过程分为4个步骤:

  • 初始标记(标记GCRoots能直接关联到的目标,STOP THE WORLD)
  • 并发标记(进行并发的GCRoot  Tracing操做,GCRoots Tracing就是可达性分析,能够跟工做线程一块儿运行)
  • 从新标记(修改由于并发标记时候,工做线程引发的标记变化,STOP THE WORLD
  • 并发清除(能够跟工做线程一块儿运行)

  由于CMS收集器能让工做线程和垃圾回收线程一块儿进行,因此程序响应时间受GC影响比较小。可是CMS也有三个明显缺点:一、CMS对CPU资源敏感,由于并发收集,占用了工做线程。二、CMS没法处理浮动垃圾,由于CMS在清除工做的时候跟工做线程一块儿并发,此时有可能产生新的垃圾。致使GC完成后,垃圾回收后,系统内存仍然不足,引发Full GC。三、CMS采用的"标记-清除"算法会致使内存不连续。

 

G1收集器(Garbage-First)

  G1垃圾收集器在JDK9中,被设置为默认垃圾收集器。G1垃圾收集器再也不是以上面的分区方式来进行垃圾收集。而是在保留eden、Survivor、Tenured区的前提下,将内存划分为一个个更小Region区。G1跟踪每一个区域内存的回收价值,在后台维护一个优先列表,每次根据垃圾回收容许的时间,选择回收价值最大的Region进行回收,这就是Garbage-First名字的由来。

  经过对每一个小Region区进行垃圾回收,实现了化整为零的回收思路。可是若是某个Region里的对象被其余Region的对象所引用呢?其实这个问题再其余垃圾回收器中也有相同问题,可是由于G1的分区更多,因此这个问题在G1中会更加突出。为了避免用在收集某个Region的时候,扫描整个堆引用,虚拟机都是采用Remembered Set来记录引用信息(年老代引用年轻带也是经过这个)。G1中每个Region区都维护一个RemerberSet来记录,假设A区Region中的对象被B Region区对象所引用时候,就经过CardTable被相关引用信息记录在A区的Remerber Set中。

 

   Remerber Set是个hashMap格式,key指向被引用的区域,Value指向引用该对象对应的Card Table。而Card Table记录数据为0或者1,且对应某个区域的内存。所以,当GC发生时候,只须要对当前Region GC根节点枚举的范围中加入Remerber Set便可保证不对全堆扫描也不会有遗漏。

     G1收集器的运做大体能够划分为如下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

  这四个步骤跟CMS的回收大体相同,筛选回收的时候,G1对各个Region进行回收价值和回收成本进行排序,而后用户指望的GC停顿进行回收。可是SUN公司透露其实这个阶段也是能够并发的。

内存分配

   JVM的内存分配根据选择不一样的垃圾回收器和不一样的虚拟参数会略有不一样,大体规则以下:

  

  

Tips

对象变老

  在Eden区正常发育的对象,没经历一次Minor GC,就给活下来的对象年龄+1,当年龄达到默认的15岁的时候,这些对象晋升到老年代。15岁是默认参数,能够经过+XX:MaxTenuringThreshold来进行设置。

动态年龄断定

  新对象除了常规等年龄大于年龄阈值晋升老年代,若是Survivor区中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的直接进入老年代。好比Survior区中8岁的对象占了Survior一半空间以上,则8岁或者8岁以上的直接进入老年代。

空间分配担保

  在发生Minor GC以前,虚拟机先检查老年代最大的连续空间是否大于新生代全部对象总空间,若是这个条件成立,那么Minor GC能够确保安全。不然,要看HandlePromotionFailure是否容许,若是不容许,则直接进行Full GC。设置HandlePromotionFailure为容许的状况下,则取以前每一次晋升到老年代大小的平均值做为参考,决定是否使用Full GC让老年代腾出更多空间。通常状况下,HandlePromotionFailure会打开,避免Full GC执行得太过频繁。

相关文章
相关标签/搜索