天天看点

java垃圾回收算法及收集器java内存回收算法及收集器

java内存回收算法及收集器

为什么要进行垃圾回收?

如果不进行垃圾回收,jvm就无法给新创建的对象分配对象了,系统无法长时间运行,如果不进行垃圾回收,我们的java服务将每隔一段时间就需要重启一次?这样你能接受吗?

什么时候进行垃圾回收?

在无法分配内存的时候进行垃圾回收,已目前普遍使用的分代回收方法为例,在Eden区无法为创建新的区域给新的对象的时候,会开始进行垃圾回收Minor GC,如果是老年代无法创建对象了就会进行Major GC。

什么叫做Full GC?为什么需要关注Full GC 的时间?

Full GC指暂停其他操作只进行GC 操作所用到的总时间叫做Full GC的时间,因为MinorGc也有线程暂停执行,所以理论在MinorGC也是会有停止一切的时间,只是这个时间比较短,几乎可以忽略不计。一般来说指MajorGc中所有线程暂停执行时间的总和(首次标记时间+重新标记时间之和)。

一、垃圾回收算法

java目前常用有4中回收算法,7种垃圾回收器。
           

1、标记-清除算法

分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象,如下图。
缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。
           
java垃圾回收算法及收集器java内存回收算法及收集器

2、标记-整理算法

复制算法:把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环,如下图。
缺点:实际可使用的内存空间缩小为原来的一半,比较适合。
           
java垃圾回收算法及收集器java内存回收算法及收集器

3、复制算法

先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存,如下图
           
java垃圾回收算法及收集器java内存回收算法及收集器

4、分代收集

把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。
           
java垃圾回收算法及收集器java内存回收算法及收集器
在这些区域的垃圾回收大概有如下几种情况:

1.大多数情况下,新的对象都分配在Eden区,当Eden区没有空间进行分配时,将进行一次Minor GC,清理Eden区中的无用对象。清理后,Eden和From Survivor中的存活对象如果小于To Survivor的可用空间则进入To Survivor,否则直接进入老年代);Eden和From Survivor中还存活且能够进入To Survivor的对象年龄增加1岁(虚拟机为每个对象定义了一个年龄计数器,每执行一次Minor GC年龄加1),当存活对象的年龄到达一定程度(默认15岁)后进入老年代,可以通过-XX:MaxTenuringThreshold来设置年龄的值。
当进行了Minor GC后,Eden还不足以为新对象分配空间(那这个新对象肯定很大),新对象直接进入老年代。
2.占To Survivor空间一半以上且年龄相等的对象,大于等于该年龄的对象直接进入老年代,比如Survivor空间是10M,有几个年龄为4的对象占用总空间已经超过5M,则年龄大于等于4的对象都直接进入老年代,不需要等到MaxTenuringThreshold指定的岁数。
3.在进行Minor GC之前,会判断老年代最大连续可用空间是否大于新生代所有对象总空间,如果大于,说明Minor GC是安全的,否则会判断是否允许担保失败,如果允许,判断老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则执行Minor GC,否则执行Full GC。
4.当在java代码里直接调用System.gc()时,会建议JVM进行Full GC,但一般情况下都会触发Full GC,一般不建议使用,尽量让虚拟机自己管理GC的策略。
5.永久代(方法区)中用于存放类信息,jdk1.6及之前的版本永久代中还存储常量、静态变量等,当永久代的空间不足时,也会触发Full GC,如果经过Full GC还无法满足永久代存放新数据的需求,就会抛出永久代的内存溢出异常。
6.大对象(需要大量连续内存的对象)例如很长的数组,会直接进入老年代,如果老年代没有足够的连续大空间来存放,则会进行Full GC。
           

二、垃圾回收的数据

1、引用计数

给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;每当有一个地方不再引用它时,计数器值减1,这样只要计数器的值不为0,就说明还有地方引用它,它就不是无用的对象。如下图,对象2有1个引用,它的引用计数器值为1,对象1有两个地方引用,它的引用计数器值为2。

java垃圾回收算法及收集器java内存回收算法及收集器

问题:无法判断出循环引用的问题,若对象1引用了对象2,且对象2引用了对象1,那么引用计数器将一直为1,一直无法回收对象

2、可达性分析

了解可达性分析算法之前先了解一个概念——GC Roots,垃圾收集的起点,可以作为GC Roots的有虚拟机栈中本地变量表中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,就说明此对象是不可用的,是死对象。如下图:object1、object2、object3、object4和GC Roots之间有可达路径,这些对象不会被回收,但object5、object6、object7到GC Roots之间没有可达路径,这些对象就被判了死刑。

java垃圾回收算法及收集器java内存回收算法及收集器
哪些可以作为GC ROOTS?
  • Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
  • Thread - 活着的线程
  • Stack Local - Java方法的local变量或参数
  • JNI Local - JNI方法的local变量或参数
  • JNI Global - 全局JNI引用
  • Monitor Used - 用于同步的监控对象
  • Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。

三、垃圾收集器

java垃圾回收算法及收集器java内存回收算法及收集器

1、Serial

Serial是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。就比如妈妈在家打扫卫生的时候,肯定不会边打扫边让儿子往地上乱扔纸屑,否则一边制造垃圾,一遍清理垃圾,这活啥时候也干不完。

如下是Serial收集器和Serial Old收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,Serial收集器以单线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。

java垃圾回收算法及收集器java内存回收算法及收集器

适用场景:Client模式(桌面应用);单核服务器。可以用-XX:+UserSerialGC来选择Serial作为新生代收集器。

2、ParNew

ParNew就是一个Serial的多线程版本,其它与Serial并无区别。ParNew在单核CPU环境并不会比Serial收集器达到更好的效果,它默认开启的收集线程数和CPU数量一致,可以通过-XX:ParallelGCThreads来设置垃圾收集的线程数。

如下是ParNew收集器和Serial Old收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,ParNew收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。

java垃圾回收算法及收集器java内存回收算法及收集器

适用场景:多核服务器;与CMS收集器搭配使用。当使用-XX:+UserConcMarkSweepGC来选择CMS作为老年代收集器时,新生代收集器默认就是ParNew,也可以用-XX:+UseParNewGC来指定使用ParNew作为新生代收集器。

3、Paraller Scavenge

Parallel Scavenge也是一款用于新生代的多线程收集器,与ParNew的不同之处是,ParNew的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge的目标是达到一个可控制的吞吐量。吞吐量就是CPU执行用户线程的的时间与CPU执行总时间的比值【吞吐量=运行用户代代码时间/(运行用户代码时间+垃圾收集时间)】,比如虚拟机一共运行了100分钟,其中垃圾收集花费了1分钟,那吞吐量就是99% 。比如下面两个场景,垃圾收集器每100秒收集一次,每次停顿10秒,和垃圾收集器每50秒收集一次,每次停顿时间7秒,虽然后者每次停顿时间变短了,但是总体吞吐量变低了,CPU总体利用率变低了。

java垃圾回收算法及收集器java内存回收算法及收集器

适用场景:注重吞吐量,高效利用CPU,需要高效运算且不需要太多交互。可以使用-XX:+UseParallelGC来选择Parallel Scavenge作为新生代收集器,jdk7、jdk8默认使用Parallel Scavenge作为新生代收集器。

4、Serial old

Serial Old收集器是Serial的老年代版本,同样是一个单线程收集器,采用标记-整理算法。

java垃圾回收算法及收集器java内存回收算法及收集器

适用场景:Client模式(桌面应用);单核服务器;与Parallel Scavenge收集器搭配;作为CMS收集器的后备预案。

5、Paraller old

Parallel Old收集器是Parallel Scavenge的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与Parallel Scavenge收集器搭配,可以充分利用多核CPU的计算能力。

java垃圾回收算法及收集器java内存回收算法及收集器

适用场景:与Parallel Scavenge收集器搭配使用;注重吞吐量。jdk7、jdk8默认使用该收集器作为老年代收集器,使用 -XX:+UseParallelOldGC来指定使用Paralle Old收集器

6、CMS

MS收集器是一种以最短回收停顿时间为目标的收集器,以“最短用户线程停顿时间”著称。整个垃圾收集过程分为4个步骤:

① 初始标记:标记一下GC Roots能直接关联到的对象,速度较快

② 并发标记:进行GC Roots Tracing,标记出全部的垃圾对象,耗时较长

③ 重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短

④ 并发清除:用标记-清除算法清除垃圾对象,耗时较长

整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集可以看做是和用户线程并发执行的

java垃圾回收算法及收集器java内存回收算法及收集器

CMS收集器也存在一些缺点:

  • 对CPU资源敏感:默认分配的垃圾收集线程数为(CPU数+3)/4,随着CPU数量下降,占用CPU资源越多,吞吐量越小
  • 无法处理浮动垃圾:在并发清理阶段,由于用户线程还在运行,还会不断产生新的垃圾,CMS收集器无法在当次收集中清除这部分垃圾。同时由于在垃圾收集阶段用户线程也在并发执行,CMS收集器不能像其他收集器那样等老年代被填满时再进行收集,需要预留一部分空间提供用户线程运行使用。当CMS运行时,预留的内存空间无法满足用户线程的需要,就会出现“Concurrent Mode Failure”的错误,这时将会启动后备预案,临时用Serial Old来重新进行老年代的垃圾收集。
  • 因为CMS是基于标记-清除算法,所以垃圾回收后会产生空间碎片,可以通过-XX:UserCMSCompactAtFullCollection开启碎片整理(默认开启),在CMS进行Full GC之前,会进行内存碎片的整理。还可以用-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩(不进行碎片整理)的Full GC之后,跟着来一次带压缩(碎片整理)的Full GC。

    适用场景:重视服务器响应速度,要求系统停顿时间最短。可以使用-XX:+UserConMarkSweepGC来选择CMS作为老年代收集器。

为什么初始标记与重新标记标记的是啥?为啥需要stop the world?

1、 因为初始标记后、重新标记标记的都是GC-ROOTS直接引用的对象,根据这些数据做可达性分析。

2、 第一次stop the world我理解是因为需要避免标记时有新的标记对象出现,后面一次是因为在并发标记的时候会有新的满足条件的GC-ROOTS引用对象出现。

7、G1

G1 收集器是jdk1.7才正式引用的商用收集器,现在已经成为jdk9默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1进行垃圾收集的范围是整个堆内存,它采用“化整为零”的思路,把整个堆内存划分为多个大小相等的独立区域(Region),在G1收集器中还保留着新生代和老年代的概念,它们分别都是一部分Region,如下图

java垃圾回收算法及收集器java内存回收算法及收集器

每一个方块就是一个区域,每个区域可能是Eden、Survivor、老年代,每种区域的数量也不一定。JVM启动时会自动设置每个区域的大小(1M~32M,必须是2的次幂),最多可以设置2048个区域(即支持的最大堆内存为32M*2048=64G),假如设置-Xmx8g -Xms8g,则每个区域大小为8g/2048=4M。

为了在GC Roots Tracing的时候避免扫描全堆,在每个Region中,都有一个Remembered Set来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个Remembered Set来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据。

G1收集器可以“建立可预测的停顿时间模型”,它维护了一个列表用于记录每个Region回收的价值大小(回收后获得的空间大小以及回收所需时间的经验值),这样可以保证G1收集器在有限的时间内可以获得最大的回收效率。

如下图所示,G1收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和CMS收集器前几步的收集过程很相似:

java垃圾回收算法及收集器java内存回收算法及收集器
  1. 初始标记:标记出GC Roots直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行
  2. 并发标记:从GC Root开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行
  3. 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录
  4. 筛选回收:筛选回收阶段会对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是Garbage First的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。

适用场景:要求尽可能可控GC停顿时间;内存占用较大的应用。可以用-XX:+UseG1GC使用G1收集器,jdk9默认使用G1收集器