JVM之垃圾回收机制(GC)

 2022-09-12
原文地址:https://blog.csdn.net/weixin_72753070/article/details/126011100

1. 简介垃圾回收

在做 Java 开发的过程中,我们会不断地创建很多的对象,这些对象数据会占用系统内存,如果得不到有效的管理,内存的占用会越来越多,甚至会出现内存溢出的情况,所以,我们需要进行对内存进行合理地释放,这个时候 GC 就派上大用场的。

垃圾回收(GC)是由 Java 虚拟机(JVM)垃圾回收器提供的一种对内存回收的一种机制,它一般会在内存空闲或者内存占用过高的时候对那些没有任何引用的对象不定时地进行回收。

202209122045209641.png

2. JVM内存模型

Jdk1.8以前

202209122045227872.png

Jdk1.8以后

202209122045238973.png

虚拟机栈、本地方法栈和程序计数器,这三个区域是线程私有的。比如栈帧的生命周期是和线程关联的,即随线程而生,随线程而死。

虚拟机栈其实就是用来描述 Java 方法执行的,所以每个方法执行的时候都会创建一个栈帧,每个栈帧都包含:局部变量、操作数栈、动态链接、方法出口,当方法执行完成之后,对应的栈帧便会出栈。所以它的内存分配是具备确定性的,因此我们并不需要太过关注包括虚拟机栈在内的这几个线程私有区域的内存使用情况。

相反,另外两个线程共享的区域:方法区和堆内存,则是我们需要重点关注的对象。因为这两个区域主要存放对象、数组等不具有确定性的数据,例如创建对象,每个方法运行的过程中创建的对象的数量是不确定的,即占用的内存是不确定的,可能不需要创建对象,也可能会创建很多对象,所以我们需要一套合理的内存管理机制来对这两个区域进行维护,因此,垃圾回收就应运而生了,并且这两个区域也是垃圾回收器进行垃圾回收的最重要的内存区域。

再来对堆内存和方法区进行一下划分,因为 JVM 是采用分代回收的算法,即根据对象的生命周期进行区分并进行分代存储和回收,其主要分为年轻代、老年代、持久代,见下图:

202209122045248534.png

堆内存主要由年轻代和老年代组成,而方法区主要存储持久代的数据。

注意:从 JDK 1.8 开始,永久代已经被移除了,取而代之的是元空间(Meta Space),它和服务器的内存相关联。

3. JVM判断对象是否存活的算法

GC 的存活标准知道哪些区域的内存需要被回收之后,我们自然而然地想到了,如何去判断一个对象需要被回收呢?对于如何判断对象是否可以回收,有两种比较经典的判断策略。

  • 引用计数算法
  • 可达性分析算法

3.1 引用计数算法(Reference Counting Collector)

一个对象被创建之后,系统会给这个对象初始化一个引用计数器,当这个对象被引用了,则计数器 +1,而当该引用失效后,计数器便 -1,直到计数器为 0,意味着该对象不再被使用了,则可以将其进行回收了。

这种算法其实很好用,判定比较简单,效率也很高,但是却有一个很致命的缺点,就是它无法避免循环引用,即两个对象之间循环引用的时候,各自的计数器始终不会变成 0,所以 引用计数算法 只出现在了早期的 JVM 中,现在基本不再使用了。

202209122045264135.png

3.2 可达性分析法

根搜索算法的中心思想,就是从某一些指定的根对象(GC Roots)出发,一步步遍历找到和这个根对象具有引用关系的对象,然后再从这些对象开始继续寻找,从而形成一个个的引用链(其实就和图论的思想一致),然后不在这些引用链上面的对象便被标识为引用不可达对象,也就是我们说的“垃圾”,这些对象便需要回收掉。这种算法很好地解决了上面 引用计数算法 的循环引用的问题了。

202209122045274986.png

在Java语言中,可作为GC Roots的对象包括下面几种:

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

4. Java中的引用

Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)弱引用(Weak Reference)虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。这样子设计的原因主要是为了描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。也就是说,对不同的引用类型,JVM 在进行GC 时会有着不同的执行策略。所以我们也需要去了解一下。

  • 强引用(Strong Reference)
    Object obj =new Object();

上述Object这类对象就具有强引用,属于不可回收的资源,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠回收具有强引用的对象,来解决内存不足的问题。

  • 软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

  • 弱引用(Weak Reference)

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

  • 幻象引用/虚引用(Phantom References)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

引用总结

1.对于强引用,平时在编写代码时会经常使用。

2.而其他三种类型的引用,使用得最多就是软引用和弱引用,这两种既有相似之处又有区别,他们都来描述非必须对象。

3.被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。

202209122045287697.png

5. 对象死亡(被回收)前的最后一次挣扎

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收。

5.1 finalize()的定义和作用

Java中的对象并不一定会被全部垃圾回收,当你不想要该对象的时候,你需要手动去处理那些“特殊内存”。

Java 允许定义这样的方法,它 在对象被垃圾收集器析构(回收)之前调用 ,这个方法叫做 finalize( ),它用来清除回收对象。 不建议用finalize()方法完成“非内存资源”的清理工作,但建议用于:
① 清理本地对象(通过JNI创建的对象);
② 确保某些非内存资源(如Socket、文件等)的释放:在finalize()方法中显式调用其他资源释放方法。

5.2 finalize()的隐藏问题

  • System.gc()System.runFinalization()方法增加了finalize()方法执行的机会,但不可盲目依赖它们
  • Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
  • finalize()方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize()的执行
  • 对象再生问题:finalize()方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的
  • finalize()方法至多由GC执行一次(用户当然可以手动调用对象的finalize()方法,但并不影响GC对finalize()的行为)

finalize()一般不用!被执行的不确定性太大。不要指望使用finalize()来回收你的对象,它只会在系统进行GC的时候清理特殊内存,不受你的控制!

6. 方法区如何判断是否需要回收

方法区存储内容是否需要回收的判断就与堆中不太一样。方法区主要回收的内容有: 废弃常量无用的类
对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

7. 常用的垃圾回收算法

除了上面研究的标记“垃圾对象”的算法,我们也需要“清理垃圾”的 回收算法

常用的回收算法一般有: 标记-清除算法标记-整理算法复制算法 ,以及系统自动进行判定使用的 适应性算法

7.1 标记 - 清除算法(Tracing Collector)

标记-清除 算法是最基础的收集算法,它是由 标记清除 两个步骤组成的。

标记的过程其实就是上面的 可达性算法(根搜索) 所标记的不可达对象,当所有的待回收的“垃圾对象”标记完成之后,便进行第二个步骤: 统一清除

该算法的优点是当存活对象比较多的时候,性能比较高,因为该算法只需要处理待回收的对象,而不需要处理存活的对象。

但是缺点也很明显,就是在执行完 标记-整理 之后,由于将“垃圾对象”回收掉了,所以原本连续使用的内存块便会变得不连续,这样会导致内存块上面会出现很多小单元的内存区域,这些小单元的内存区域只能够存放比较小的对象,而比较大的对象是无法直接存储的。

即原本空闲 1M 的内存区域,有可能会出现无法直接存放 0.9M 大小的对象。

202209122045298998.png

7.2 标记 - 整理算法(Compacting Collector)

上述的 标记-清除 算法会产生内存区域使用的间断,所以为了将内存区域尽可能地连续使用, 标记-整理 算法应运而生。

标记-整理 算法也是由两步组成, 标记整理

第一步的 标记 动作也是使用的 根搜索算法 ,但是在标记完成之后的动作却和 标记-清除算法 天壤之别,该算法并不会直接清除掉可回收对象 ,而是让所有的对象都向一端移动,然后将端边界以外的内存全部清理掉。

该算法所带来的最大的优势便是使得内存上面不会再有碎片问题,并且新对象的分配只需要通过简单的指针碰撞便可完成。

202209122045309529.png

7.3 复制算法(Copying Collector)

无论是 标记-清除算法 还是 垃圾-整理算法 ,都会涉及句柄的开销或是面对碎片化的内存回收,所以, 复制算法 出现了。

复制算法将内存区域均分为了两块(记为S0和S1),而每次在创建对象的时候,只使用其中的一块区域(例如S0),当S0使用完之后,便将S0上面存活的对象全部复制到S1上面去,然后将S0全部清理掉。

复制算法的优势是:① 不会产生内存碎片;② 标记和复制可以同时进行;③ 复制时也只需要移动栈顶指针即可,按顺序分配内存,简单高效;④ 每次只需要回收一块内存区域即可,而不用回收整块内存区域,所以性能会相对高效一点。

但是缺点也是很明显的:可用的内存减小了一半,存在内存浪费的情况。

所以 复制算法 一般会用于对象存活时间比较短的区域,例如 年轻代 ,而存活时间比较长的 老年代 是不适合的,因为老年代存在大量存活时间长的对象,采用复制算法的时候会要求复制的对象较多,效率也就急剧下降,所以老年代一般会使用上文提到的 标记-整理算法

2022091220453186410.png

7.4 适应性算法(Adaptive Collector)

适应性算法 其实不是一种单独的回收算法,他只是一种智能选择回收算法的机制,也就是该算法会根据堆内存具体的使用情况而自动选用更适合当前情况的回收算法。

8. 分代收集

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为 老年代(Tenured Generation)新生代(Young Generation) ,在堆区之外还有一个代就是 永久代(Permanet Generation)(JDK1.8后移除) 。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

2022091220453296111.png

8.1 新生代(Young Generation)的回收算法

  • 所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  • 新生代内存按照8:1:1的比例分为一个 eden区和两个survivor(survivor0,survivor1) 区。大部分对象在Eden区中生成,回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区。当这个survivor0区也存放满了时,则 将eden区和survivor0区存活对象复制到另一个survivor1区 ,然后清空eden和这个survivor0区,此时survivor0区是空的然后 将survivor0区和survivor1区交换 ,即保持survivor1区为空,如此往复。
  • 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
  • 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

8.2 老年代(Old Generation)的回收算法

  • 在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为 老年代中存放的都是一些生命周期较长的对象
  • 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GCFull GCFull GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

8.3 永久代(Permanent Generation)的回收算法 (JDK1.8以前)

用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。

9. 垃圾收集器

上面讲述的都说垃圾的识别以及垃圾的收集算法,那这些算法都需要由垃圾收集器去执行。

9.1 垃圾回收器总览

2022091220453401212.png

  • 新生代可配置的回收器:Serial、ParNew、Parallel Scavenge
  • 老年代配置的回收器:CMS、Serial Old、Parallel Old

新生代和老年代区域的回收器之间进行连线,说明他们之间可以搭配使用。

9.2 新生代收集器

Serial 垃圾回收器

Serial收集器是最基本的、发展历史最悠久的收集器。俗称为:串行回收器,采用复制算法进行垃圾回收

特点

串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程。

2022091220453503313.png

对于并行能力较弱的单CPU计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。

它存在Stop The World问题,及垃圾回收时,要停止程序的运行。

使用-XX:+UseSerialGC参数可以设置新生代使用这个串行回收器

ParNew 垃圾回收器

ParNew其实就是Serial的多线程版本,除了使用多线程之外,其余参数和Serial一模一样。俗称:并行垃圾回收器,采用复制算法进行垃圾回收

2022091220453602714.png

特点

ParNew默认开启的线程数与CPU数量相同,在CPU核数很多的机器上,可以通过参数-XX:ParallelGCThreads来设置线程数。

它是目前新生代首选的垃圾回收器,因为除了ParNew之外,它是唯一一个能与老年代CMS配合工作的。

它同样存在Stop The World问题

使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器

Parallel Scavenge 收集器

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。

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

-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,可用把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间可以将MaxGCPauseMillis设置的很小,但是会导致GC频繁,从而增加了GC的总时间降低吞吐量。所以需要根据实际情况设置该值。

-Xx:GCTimeRatio:设置吞吐量大小,它是一个0到100之间的整数,默认情况下他的取值是99,那么系统将花费不超过1/(1+n)的时间用于垃圾回收,也就是1/(1+99)=1%的时间。

另外还可以指定-XX:+UseAdaptiveSizePolicy打开自适应模式,在这种模式下,新生代的大小、eden、from/to的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。

使用-XX:+UseParallelGC参数可以设置新生代使用这个并行回收器

这是 JDK1.8 默认收集器

2022091220453681515.png

9.3 老年代收集器

SerialOld 垃圾回收器

SerialOld是Serial回收器的老年代回收器版本,它同样是一个单线程回收器。

用途

  • 一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,
  • 另一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。

使用算法:标记 - 整理算法

ParallelOldGC 回收器

老年代ParallelOldGC回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关注吞吐量的回收器,他使用了标记压缩算法进行实现。

-XX:+UseParallelOldGc进行设置老年代使用该回收器

-XX:+ParallelGCThreads也可以设置垃圾收集时的线程数量。

CMS 回收器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的 Mark Sweep 这两个词可以看出,CMS 收集器是一种 “标记-清除”算法 实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

2022091220453796316.png

CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置:-XX:CMSInitiatingoccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达到68%的时候,会执行CMS回收。

如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器;SerialOldGC进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。

这个过程GC的停顿时间可能较长,所以-XX:CMSInitiatingoccupancyFraction的设置要根据实际的情况。

之前我们在学习算法的时候说过,标记清除法有个缺点就是存在内存碎片的问题,那么CMS有个参数设置-XX:+UseCMSCompactAtFullCollecion可以使CMS回收完成之后进行一次碎片整理

主要优点: 并发收集、低停顿 。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

9.4 G1 收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

G1中的几个重要概念

Region

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示:

2022091220453895817.png

而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:

2022091220453975918.png

在上图中,我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征: * H-obj直接分配到了old gen,防止了反复拷贝移动。

一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定。

SATB

全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  • 灰:对象被标记了,但是它的field还没有被标记或标记完。
  • 黑:对象被标记了,且它的所有field也被标记完了。

Pause Prediction Model

G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。

G1 被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:

  • 并行与并发 :G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集 :虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合 :与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿 :这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1 收集器的运作大致分为以下几个步骤:

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

2022091220454061519.png

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

9.5 ZGC 收集器

与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。

在 ZGC 中出现 Stop The World 的情况会更少!

10. 垃圾回收的时机

垃圾回收分为两种,Full GC 和 Scavenge GC。

Full GC 发生在整个堆内存中,而 Scavenge GC 仅仅发生在年轻代的 Eden 区,所以我们应该尽可能地减少 Full GC 的次数,当然,对于 JVM 的调优,很多情况下也是在想办法对 Full GC 进行调优。

因为 GC 是可能会对应用程序造成影响的,所以触发 GC 也是有一定的条件的,例如:

  • 当应用程序空闲时,GC 有可能会被调用,因为 GC 运行线程的优先级是相对较低的,所以当线程忙的时候,它是不会运行的,当然,内存不足的情况除外;
  • 堆内存不足的时候,GC 会被调用。例如创建对象的时候,若此时内存不足,则会触发 GC 用来给这个对象分配合适的内存,当进行完一次 GC 之后内存还是不足,则会继续进行第二次 GC,若第二次 GC 之后内存还是不足,则一般会提示 “out of memory”异常;

System.gc() 方法会显示触发 Full GC,但是它只是对 JVM 的一个 GC 请求,至于何时触发,还是由 JVM 自行判断的。

GC 的调用开销是比较大的,所以我们需要有针对性地进行调优,一般有如下方案:

  1. 不要显式调用 System.gc()。此函数虽然是建议 JVM 进行 GC,但很多情况下它会触发 GC,从而增加 GC 的频率;
  2. 尽量减少临时对象的使用。在方法结束后,临时对象便成为了垃圾,所以减少临时变量的使用就相当于减少了垃圾的产生,从而减少了GC的次数;
  3. 对象不用时最好显式置为 Null。一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null 有利于 GC 收集器对垃圾的判定;
  4. 尽量使用 StringBuilder 来代替 String 的字符串累加。因为 String 的底层是 final 类型的数组,所以 String 的增加其实是建了一个新的 String,从而产生了过多的垃圾;
  5. 允许的情况下尽量使用基本类型(如 int)来替代 Integer 对象。因为基本类型变量比相应的对象占用的内存资源会少得多;
  6. 合理使用静态对象变量。因为静态变量属于全局变量,不会被 GC 回收;