JVM垃圾回收机制入门

如何断定对象为垃圾对象

在堆里面存放着Java世界中几乎全部的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些对象已死(可回收).html

引用计数法

在JDK1.2以前,使用的是引用计数器算法。 在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,能够回收了!java

**问题:**若是在A类中调用B类的方法,B类中调用A类的方法,这样当其余全部的引用都消失了以后,A和B还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已是垃圾了。可是该算法并不会计算出该类型的垃圾。

可达性分析法

在主流商用语言(如Java、C#)的主流实现中, 都是经过可达性分析算法来断定对象是否存活的: 经过一系列的称为 GC Roots 的对象做为起点, 而后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 以下图:虽然E和F相互关联, 但它们到GC Roots是不可达的, 所以也会被断定为可回收的对象。 web

注: 即便在可达性分析算法中不可达的对象, VM也并非立刻对其回收, 由于要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相链接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象须要覆盖finalize()方法且没被调用过).算法

在Java, 可做为GC Roots的对象包括:
  1. 方法区: 类静态属性引用的对象;
  2. 方法区: 常量引用的对象;
  3. 虚拟机栈(本地变量表)中引用的对象.
  4. 本地方法栈JNI(Native方法)中引用的对象。

如何回收

回收策略

垃圾收集策略有分代收集和分区收集。多线程

分代收集算法

标记-清除算法(老年代)

该算法分为“标记”和“清除”两个阶段: 首先标记出全部须要回收的对象(可达性分析), 在标记完成后统一清理掉全部被标记的对象. 并发

该算法会有两个问题:oracle

  1. 效率问题,标记和清除效率不高。
  2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会致使在运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾收集。

因此它通常用于"垃圾不太多的区域,好比老年代"。框架

复制算法(新生代)

该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象(非垃圾)复制到另一块上面, 而后把已使用过的内存空间一次清理掉.jvm

优势:不用考虑碎片问题,方法简单高效。 缺点:内存浪费严重。线程

现代商用VM的新生代均采用复制算法, 但因为新生代中的98%的对象都是生存周期极短的, 所以并不需彻底按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 须要依赖老年代进行空间分配担保机制, 这部份内存直接进入老年代。

复制算法的空间分配担保: 在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 因为新生代使用复制收集算法, 为了提高内存利用率, 只使用了其中一个Survivor做为轮换备份, 所以当出现大量对象在Minor GC后仍然存活的状况时, 就须要老年代进行分配担保, 让Survivor没法容纳的对象直接进入老年代, 但前提是老年代须要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是没法明确知道的, 所以Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 若是条件成立, 则进行Minor GC, 不然进行Full GC(让老年代腾出更多空间). 然而取历次晋升的对象的平均大小也是有必定风险的, 若是某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能致使担保失败(Handle Promotion Failure, 老年代也没法存放这些对象了), 此时就只好在失败后从新发起一次Full GC(让老年代腾出更多空间).


标记-整理算法(老年代)

标记清除算法会产生内存碎片问题, 而复制算法须要有额外的内存担保空间, 因而针对老年代的特色, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤再也不对可回收对象直接清理, 而是让全部存活的对象都向一端移动,而后清理掉端边界之外的内存.

方法区回收(永久代)

在方法区进行垃圾回收通常”性价比”较低, 由于在方法区主要回收两部份内容: 废弃常量无用的类.

回收废弃常量与回收其余年代中的对象相似, 但要判断一个类是否无用则条件至关苛刻:

  1. 该类全部的实例都已经被回收, Java堆中不存在该类的任何实例;
  2. 该类对应的Class对象没有在任何地方被引用(也就是在任何地方都没法经过反射访问该类的方法);
  3. 加载该类的ClassLoader已经被回收. 但即便知足以上条件也未必必定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 所以在大量使用动态代理、CGLib等字节码框架的应用中必定要关闭该选项, 开启VM的类卸载功能, 以保证方法区不会溢出.

分区收集

分区算法则将整个堆空间划分为连续的不一样小区间, 每一个小区间独立使用, 独立回收. 这样作的好处是能够控制一次回收多少个小区间

在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减小一次GC所产生的停顿

垃圾回收器

Serial

Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它在进行垃圾收集时,会暂停全部的工做进程,用一个线程去完成GC工做

特色:简单高效,适合jvm管理内存不大的状况(十兆到百兆)。

Parnew

ParNew收集器实际上是Serial的多线程版本,回收策略彻底同样,可是他们又有着不一样。

咱们说了Parnew是多线程gc收集,因此它配合多核心的cpu效果更好,若是是一个cpu,他俩效果就差很少。(可用-XX:ParallelGCThreads参数控制GC线程数)

Cms

CMS(Concurrent Mark Sweep)收集器是一款具备划时代意义的收集器, 一款真正意义上的并发收集器, 虽然如今已经有了理论意义上表现更好的G1收集器, 但如今主流互联网企业线上选用的还是CMS(如Taobao),又称多并发低暂停的收集器。

由他的英文组成能够看出,它是基于标记-清除算法实现的。整个过程分4个步骤:

  1. 初始标记(CMS initial mark):仅只标记一下GC Roots能直接关联到的对象, 速度很快
  2. 并发标记(CMS concurrent mark: GC Roots Tracing过程)
  3. 从新标记(CMS remark):修正并发标记期间因用户程序继续运行而致使标记产生变更的那一部分对象的标记记录
  4. 并发清除(CMS concurrent sweep: 已死对象将会就地释放)

能够看到,初始标记、从新标记须要STW(stop the world 即:挂起用户线程)操做。由于最耗时的操做是并发标记和并发清除。因此整体上咱们认为CMS的GC与用户线程是并发运行的。

**优势:**并发收集、低停顿

缺点:

  1. CMS默认启动的回收线程数=(CPU数目+3)*4 当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 可是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而致使应用程序变慢, 总吞吐量下降.
  2. 没法清除浮动垃圾(GC运行到并发清除阶段时用户线程产生的垃圾),由于用户线程是须要内存的,若是浮动垃圾施放不及时,极可能就形成内存溢出,因此CMS不能像别的垃圾收集器那样等老年代几乎满了才触发,CMS提供了参数-XX:CMSInitiatingOccupancyFraction来设置GC触发百分比(1.6后默认92%),固然咱们还得设置启用该策略-XX:+UseCMSInitiatingOccupancyOnly
  3. 由于CMS采用标记-清除算法,因此可能会带来不少的碎片,若是碎片太多没有清理,jvm会由于没法分配大对象内存而触发GC,所以CMS提供了-XX:+UseCMSCompactAtFullCollection参数,它会在GC执行完后接着进行碎片整理,可是又会有个问题,碎片整理不能并发,因此必须单线程去处理,因此若是每次GC完都整理用户线程stop的时间累积会很长,因此XX:CMSFullGCsBeforeCompaction参数设置隔几回GC进行一次碎片整理(默认为0)。
G1

同优秀的CMS垃圾回收器同样,G1也是关注最小时延的垃圾回收器,也一样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特色是引入分区的思路,弱化分代的概念,合理利用垃圾收集各个周期的资源,解决了其余收集器甚至CMS的众多缺陷。

由于每一个区都有E、S、O代,因此在G1中,不须要对整个Eden等代进行回收,而是寻找可回收对象比较多的区,而后进行回收(虽然也须要STW操做,可是花费的时间是不多的),保证高效率。

新生代收集

G1的新生代收集跟ParNew相似,若是存活时间超过某个阈值,就会被转移到S/O区。

年轻代内存由一组不连续的heap区组成, 这种方法使得能够动态调整各代区域的大小

老年代收集

分为如下几个阶段:

  1. 初始标记 (Initial Mark: Stop the World Event) 在G1中, 该操做附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.
  2. 扫描根区域 (Root Region Scanning: 与应用程序并发执行) 扫描Survivor中可以引用到老年代的references. 但必须在Minor GC触发前执行完
  3. 并发标记 (Concurrent Marking : 与应用程序并发执行) 在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断
  4. 从新标记 (Remark : Stop the World Event) 完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算全部区域的活跃度).
  5. 清理 (Cleanup : Stop the World Event and Concurrent) 在含有存活对象和彻底空闲的区域上进行统计(STW)、擦除Remembered Sets(使用Remembered Set来避免扫描全堆,每一个区都有对应一个Set用来记录引用信息、读写操做记录)(STW)、重置空regions并将他们返还给空闲列表(free list)(Concurrent)

详情请看参考文档

相关文章
相关标签/搜索