Java垃圾回收机制

1. 概述


在编写Java程序时,一般不用内存管理,不用像C++一样需要在程序中手动释放内存。JVM的垃圾收集器会自动对内存进行释放,不用程序员担心,虽然如此,但是了解一下其内部工作机制是很有必要的。

1.1 GC名词解释

  • Minor GC: 针对新生代的垃圾回收;
  • Young GC:针对新生代的垃圾回收,和Minor GC等价;
  • Old GC:针对老年代的垃圾回收;
  • Full GC:针对新生代、老年代、永久代的整体内存空间的垃圾回收;
  • Major GC:这个名词容易混淆,有些人把Major GC跟Old GC等价起来,认为他就是针对老年代的GC,也有人把Major GC和Full GC等价起来,认为他是针对JVM全体内存区域的GC,建议不要使用该名词
  • Mixed GC:G1中特有的概念,是指在G1中,一旦老年代占据堆内存的45%,就会触发对新生代和老年代都进行回收的混合回收。

1.2 垃圾回收触发时机

新生代回收
当新生代中的对象越来越多,快撑满了,此时若有新的对象需要创建的话,就会触发垃圾回收。

Full GC

  • Young GC之前,如果“老年代连续可用内存空间大小<历次新生代Young GC后进入老年代的平均对象大小”,说明此次"Young GC后进入老年代的对象大小" 可能超过 "老年代连续可用内存空间大小",此时会触发一次Old GC让老年代腾出更多的内存空间,然后再执行Young GC;
  • Young GC之后,发现剩余对象太多,老年代都放不下了;
  • 老年代内存占用达到一定比例,超过-XX:CMSInitiatingOccupancyFaction。(CMS)
  • 存放类信息、常量池的永久代满了.

1.3 回收哪些对象

JVM在进行垃圾回收的时候,到底会回收哪些对象呢?如果一个对象的引用链路上没有一个GC Roots,那么该对象就是垃圾对象,在垃圾回收的时候就会被回收。
可以作为GC Roots的对象有:

  • 方法的局部变量;
  • 类的静态变量;

例如,以下程序:

public void call() {
    User user = new User();
    ....
}

当程序执行到call方法时,会在当前线程的Java虚拟机栈中,为call方法创建一个栈帧。局部变量表中user变量引用着堆空间的User对象。假如在call方法的栈帧还未出栈时发生了GC,此时User对象被局部变量user引用着,所以User对象不会被回收掉。

也就是说,只要你的对象被方法的局部变量、类的静态变量给引用了,就不会被回收。这里为什么是强应用呢?我们知道,在Java中引用类型有4种:强、软、弱、虚。

正常情况下,垃圾回收是不会回收软引用对象的,但是如果在垃圾回收之后,发现内存仍然不足,此时就会把这些软引用对象给回收掉,哪怕它被变量引用着。

弱引用和虚引用只要发生垃圾回收就会被回收掉。

1.4 Stop The World

Stop The World指的是在垃圾回收的时候,应用程序需要暂停一会等待垃圾回收完毕。也就是说,垃圾回收会暂停我们写的Java系统中所有的工作线程,让我们的程序停止运行。

之所以需要Stop The World是因为不能JVM在进行回收垃圾,应用系统却在生产垃圾(系统运行就会创建对象,创建出来的对象可能在垃圾回收期间成为新的垃圾)。

垃圾回收停顿的时间越少越好,垃圾回收器在不断升级过程中,都在朝着降低Stop The World持续的时间努力。不过目前而言,再好的垃圾回收器也只是更加降低Stop The World的时间,而没法避免。

2. 垃圾回收算法

JVM中垃圾回收算法有以下几种:

  • 引用计数法;
  • 复制算法;
  • 标记清除;
  • 标记整理。

引用计数法是看当前对象有多少引用,有一个引用相应的应用计数就加1,当引用计数为0时,表示没有任何引用,即可以被垃圾回收。不过在实际应用中,一般很少采用引用计数法来进行垃圾回收。

复制算法就是把相应的内存区域划分成两块,一块使用一块空闲,当发生垃圾回收时,将在使用中的存活对象复制到未被使用的区域。

Java垃圾回收机制_第1张图片
复制算法

标记清除算法会先标记出是存活对象还是垃圾对象,标记完之后,再将标记为垃圾的对象进行清理,此种方式容易形成内存碎片。

Java垃圾回收机制_第2张图片
标记清除

标记整理算法也和标记清除一样,先对存活对象/垃圾对象进行标记。标记完成之后再进行清理,针对清理过程中形成的内存碎片,标记整理算法会将多个小块的内存碎片整理成一块整的内存区域。

Java垃圾回收机制_第3张图片
标记整理

3. 垃圾回收的方式

JVM进行垃圾回收的时候,会起一个守护线程,专门用于垃圾回收。根据垃圾回收线程和应用线程运行的时机,可将垃圾回收方式按以下方式划分:

  • 串行(Serial)回收:只使用一个线程进行垃圾回收,用于单线程的客户端。
  • 并行(Parallel)回收:多个垃圾回收线程并行工作,常用于科学计算、大数据处理等和用户交互少的场景。
  • 并发(CMS)回收:用户线程和垃圾回收线程可以同时执行,适用于对垃圾回收停顿时间有要求的场景。
  • G1:将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

4. 老年代垃圾回收


对象进入老年代的时机

我们创建的对象会优先放入新生代中。那在什么情况下,对象会进入老年代?对象存在以下进入老年代的时机:

  • 在新生代躲过15次GC;
  • 动态年龄判断;
  • 大对象直接进入老年代;
  • YoungGC后存活对象太多,无法放入Survivor;
  • 老年代空间分配担保规则

老年代的垃圾回收速度会比新生代垃圾回收速度慢10倍,如果频繁出现老年代的垃圾回收,会影响到系统的性能,同时系统会出现卡顿现象。那为啥老年代垃圾回收会慢呢?主要是由于以下几种原因:

  • 老年代存活的对象很对,所以追踪到GC Roots的链路就长了;
  • 清理的时候并不是对一整块内存进行清理,而是先找到零零散散的垃圾对象再进行清理;
  • 清理完垃圾对象之后还会对内存碎片进行整理。

5. 具体的垃圾回收器


Hotspot中的具体的垃圾回收产品有7中,它们分别是:

  • Serial,串行方式,用于新生代,采用复制算法。
  • ParNew,并行回收,用于新生代,采用复制算法。
  • Parallel Scavenge,并行回收,用于新生代,采用复制算法。
  • Serial Old,串行回收,用于老年代,采用标记-整理算法。
  • Parallel Old,并行回收,用于老年代,采用标记-整理算法。
  • CMS,并发回收,用于老年代,采用标记-清除算法。
  • G1,应用于新生代和老年代,整体上采用标记-整理算法,ju

其中SerialParNewParallel Scavenge适用于新生代,Serial OldParallel OldCMS适用于老年代。

Java垃圾回收机制_第4张图片

图中的连线表示,新生代使用的垃圾回收器和老年代使用的垃圾回收器是相互关联的,可以在JVM参数中只显式配置其中一个。例如,新生代配置使用Serial垃圾回收器,老年代就会自动使用Serial Old垃圾回收器。
其中红色叉叉表示Java8版本开始,对应的垃圾回收组合不推荐使用。

5.1 Serial/Serial Old收集器


Serial和Serial Old收集器都是串行收集器,只会使用一个线程进行垃圾回收,垃圾回收的时会停止应用线程进行Stop The World状态。

其中Serial采用复制算法,Serial Old采用标记-整理算法。

Java垃圾回收机制_第5张图片

对应的JVM参数是:-XX:+UseSerialGC。启用此参数之后,新生代使用Serial,老年代使用Serial Old

5.2 ParNew


ParNew收集器是Serial收集器的多线程版本,它是很多虚拟机运行在Server模式下新生代的默认垃圾收集器。其主要场景是配合老年代的CMS GC工作。

Java垃圾回收机制_第6张图片

启用ParNew收集器所对应的JVM参数:-XX:+UseParNewGC。如果不指定老年代的GC收集器,老年代默认会启用Serial Old
限制GC回收线程数量:-XX:ParallelGCThreads,默认开启和CPU相同的线程数。

5.3 Parallel


Java垃圾回收机制_第7张图片

Parallel(Parallel Scavenge)ParNew都是新生代的多线程并行收集器,它们各自的关注点不同,Parallel主要关注:

  • 可控制的吞吐量:吞吐量Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大的吞吐量。

使用一下参数启用Parallel垃圾收集器

-XX:+UseParallelGC  或者使用
-XX:+UseParallelOldGC 相互激活

-XX:ParallelGCThreads=数字N表示启动GC线程的个数。

5.4 Parallel Old

Parallel Old收集器是Parallel收集器的老年代版本,使用多线程的标记-整理算法。

5.5 CMS


CMS是一种以获得最短GC停顿为目标的采用并发标记清除的垃圾收集器,它尽量允许在GC的时候,用户线程可以并发执行。

Java垃圾回收机制_第8张图片

为了避免长时间的STW,CMS将垃圾回收分成4个阶段:

  • 初始标记:只标记GC Roots直接关联的对象,速度非常快,需要STW;
  • 并发标记:对所有对象进行到GC Roots的追踪,比较耗时,和用户线程一起运行,不需要STW;
  • 重新标记:针对并发标记阶段,用户线程产生的新变化进行重写标记,需要STW;
  • 并发清除:对垃圾对象进行清除,和用户线程一起运行,不需要STW。

CMS非常适合堆内存大、CPU核数多的服务端应用上,也是G1出现之前,大型应用老年代的首选收集器。

开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC,开启该参数后会自动将-XX:+UseParNewGC打开。

CMS回收器在并发标记和并发清理两个阶段,系统工作线程和垃圾回收线程同时工作,会导致有限的CPU资源被消耗。CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4。

5.5.1 Concurrent Mode Failure


并发清理阶段,由于工作线程不停止工作,所以可能会发生:新的对象进行老年代以及产生新的垃圾对象(这种称为“浮动垃圾”)。
浮动垃圾在本次垃圾回收并没有被标记出来,所以会一直等到下次GC的时候再对其进行回收。
在并发清理期间,新对象进入老年代,可能会存在老年代空间不够,此时该如何处理?CMS会预留一定比例的空间以供并发清理期间新对象进入老年代。这个比例可以通过-XX:CMSInitiatingOccupancyFaction(jdk6中默认为92%)进行设置,当老年代已使用的空间超过这个比例时会触发一次Old GC
如果预留的空间还是不够容纳并发清理期间进入老年代对象,此时会发生Concurrent Mode Failure。系统没法再一边回收,一边将对象放入老年代。此时会自动启用Serial Old垃圾回收器代替CMS,直接让系统程序Stop The World,重新标记垃圾对象,并不允许创建新的对象,然后再一次性回收垃圾对象。

5.5.2 内存整理


CMS采用“标记-清理”算法,所以会产生内存碎片问题,太多的内存碎片容易导致更加频繁的Full gc。所以CMS提供了-XX:+UseCMSCompactAtFullCollection参数用来设置是否在并发清理之后,是否要对内存碎片进行一个压缩,即将存活对象都挪到一起。
除此之外,还有一个类似的参数-XX:CMSFullGCsBeforeCompaction表示在进行多少次FullGC之后进行一次内存碎片的压缩,其默认是0。

5.6 G1


G1是JDK1.7引入的,可以使用-XX:+UseG1GC开启。

5.6.1 Region

G1区别于上面几个垃圾收集器,上面几个垃圾收集器必须按照新生代和老年代进行划分,且只能对新生代或者老年代其中一块区域进行回收。而G1是将整个堆内存划分成一块块的Region,其既能回收新生代又能回收老年代的对象。如下图所示:

Java垃圾回收机制_第9张图片

图中可以看到G1还是会有新生代(Eden和Surivor)和老年代的这些概念,新生代包含哪些Region,老年代包含哪些Region。只不过这个过程是自动变化的,对某个region来说,可能一开始谁都不属于,然后被分配给了新生代,然后垃圾回收之后,又被分配给了老年代。故此G1中不存在新生代给多少内存、老年代给多少内存的说法。

G1中,大对象存在专门的Region,大对象的判定规则是一个大对象超过了一个Region大小的50%,就认为是个大对象。

默认情况下,G1中有2048个Region,Region的size=堆的大小 / 2048。

5.6.2 回收价值

G1收集器既可以提高吞吐量,又可以减少GC时间。最重要的是STW可控,增加了预测机制,让用户指定停顿时间。可以通过-XX:MaxGCPauseMills参数进行设置,默认值是200ms。

那么G1是有何尽量保证在垃圾回收期间,停顿用户指定的时间?G1会追踪每个Region的回收价值,回收价值指的是如果对某个Region进行垃圾回收,需要耗费多长时间,可以回收多少垃圾。在垃圾回收过程中,G1会尽量把垃圾回收对系统的影响范围控制在用户指定的时间范围内,同时在有限的时间内回收掉更多的垃圾对象。
例如,如果存在Region1和Region2,region1回收的话,需要花费100ms,能回收300M垃圾;region2回收需要200ms,能够回收100M垃圾。这种情况的话G1会优先选择回收region1。

5.6.3 G1的新生代回收

随着新生代的Eden占用的Region越来越多,直到新生代达到了设定的占据堆内存的60%,此时会触发新生代的GC,G1就会进入Stop The World状态,并采用复制算法来进行垃圾回收。回收过程和ParNew一样:把Eden区存活的对象放入S1,然后清空Eden区,再次回收的时候,将Eden区和S1区的存活对象放入S2,清空Eden区和S1区,如此反复。

5.6.4 G1对象进行老年代的时机

G1收集器中的对象进入老年代的时机和其他收集器几乎是一样的:

  • 对象在新生代躲过了很多次的垃圾回收,达到了-XX:MaxTenuringThreshold参数设置的年龄,对象就会进入老年代;
  • 动态年龄判定规则如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%。此时就会判断一下,例如年龄为1岁,2岁,3岁的对象的大小总和超过了Survivor的50%,此时3岁以上的对象全部会进入老年代;
  • 存活对象在Survivor放不下了,对象会直接进入老年代。

5.6.5 G1的老年代回收

当老年代对应Region的大小占据了堆内存一定比例(默认为45%,可以通过-XX:InitiatingHeapOccupancyPercent来变更)时,会触发一次混合回收,混合回收会对新生代和老年代一起进行垃圾回收。

G1的混合回收过程和CMS很类似,标记阶段分为:初始标记、并发标记和重新标记,这3个标记过程所做的事和CMS都一样。不一样的是最后一个混合回收阶段,它会对新生代、老年代和大对象的Region中的对象进行清理,并且它所采用的是复制算法
此外,混合回收阶段是允许执行多次的。比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。我们可以通过-XX:G1MixedGCCountTarget设置执行次数,默认是8次。

如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去
此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发 一次失败。
一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。

https://www.cnblogs.com/aspirant/p/8663872.html

你可能感兴趣的:(Java垃圾回收机制)