垃圾回收机制与内存分配策略

一、 概述

  说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC的历史比Java久远,早在1960年诞生于MIT的Lisp这门语言中就使用了内存动态分配和垃圾回收技术。当Lisp还在胚胎时期时,人们就在思考GC需要完成的3件事情

  • 哪些内存需要回收?

  • 什么时候回收?

  • 如何回收?


二、 对象是否存活?

  我们都知道JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

  垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到 判断对象是否存活 的算法!
  

2.1 引用计数算法

2.1.1 算法分析

  引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

2.1.2 优缺点

  优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

  缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

2.1.3 引用计数算法的缺陷

public class ReferenceFindTest 
{
    public static void main(String[] args) 
    {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
          
        object1.object = object2;
        object2.object = object1;
          
        object1 = null;
        object2 = null;
    }
}

  这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将 object1object2 赋值为null,也就是说 object1object2 指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

2.2 可达性分析算法

  可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

扫描二维码关注公众号,回复: 8823671 查看本文章

在这里插入图片描述

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

  a) 虚拟机栈(栈帧中的本地变量表)中引用的对象;

  b) 方法区中类静态属性引用的对象;

  c) 方法区中常量引用的对象;

  d) 本地方法栈中JNI(Native方法)引用的对象。

2.3 再谈引用

  无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。

  • 强引用

  在程序代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器 永远不会回收掉被引用的对象

  • 软引用

  用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用

  也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象 只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用

  也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在 这个对象被收集器回收时收到一个系统通知

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

  即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

  第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;

  第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

  第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

2.5 回收方法区

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

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


三、常用的垃圾收集算法

3.1 标记-清除算法(Mark-Sweep)

  标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
在这里插入图片描述

3.2 复制算法

  复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
  在这里插入图片描述

3.3 标记-整理算法

  标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:

在这里插入图片描述

3.4 分代收集算法

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

Java GC、新生代、老年代
  Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。

  新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

  这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

堆的内存模型大致为:
在这里插入图片描述
      堆大小 = 新生代 + 老年代

–Xms、-Xmx:指定堆的大小

–XX:NewRatio:指定新生代 ( Young ) 与老年代 ( Old ) 的比例【默认1:2】

默认的,Eden : from : to = 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。

因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间
在这里插入图片描述

3.4.1 年轻代(Young Generation)的回收算法【复制算法

  a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

  b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。当老年代也满了后,就会报out of memory的异常。

d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

3.4.2 年老代(Old Generation)的回收算法【标记算法

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

c)所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配

3.4.3 持久代(Permanent Generation)的回收算法

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


四、HotSpot的算法实现

4.1 枚举根节点

  在可达性分析中,可以作为GC Roots的节点有很多,但是现在很多应用仅仅方法区就有上百MB,如果逐个检查的话,效率就会变得不可接受。

  而且,可达性分析必须在一个一致性的快照中进行-即整个分析期间,系统就像冻结了一样。否则如果一边分析,系统一边动态表化,得到的结果就没有准确性。这就导致了系统GC时必须停顿所有的Java执行线程。

  目前主流Java虚拟机使用的都是准确式GC

准确式内存管理:虚拟机可以知道内存中某个位置的数据具体是什么类型。

所以当执行系统都停顿下来之后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应该有办法直接知道哪些地方存放着对象引用。在HotSpot实现中,使用一组称为 OopMap 的数据结构来达到这个目的。 OopMap会在类加载完成的时候,记录对象内什么偏移量上是什么类型的数据,在JTI编译过程中,也会在特定的位置记录下栈和寄存器哪些位置是引用。这样,在GC扫描的时候就可以直接得到这些信息了。

4.2 安全点

  如果OopMap内容变化的指令非常多,HotSpot并不会为每条指令都产生OopMap,只是在特定的位置记录了这些信息,这些位置成为“安全点”(SafePoint)。程序执行时只有在达到安全点的时候才停顿开始GC。一般具有较长运行时间的指令才能被选为安全点,如方法调用、循环跳转、异常跳转等。

  接下来要考虑的便是,如何在GC时保证所有的线程都“跑”到安全点上停顿下来。这里有两种方案: 抢先式中断 (Preemptive Suspension) 和主动式中断 (Voluntary Suspension)。

  • 抢先式中断

  把所有线程中断,如果某个线程不在安全点上,就恢复线程让它跑到安全点上。几乎没有虚拟机采用这种方式。

  • 主动式中断

  需要中断线程时,不直接对线程操作,而是设置一个GC标志,各个线程会轮询这个标志并在需要时自己中断挂起。这样,轮询标志的地方和安全点是重合的。

4.3 安全区域

  安全点机制保证程序执行时,在不太长的时间内就会遇到可进入GC的安全点,但是,程序“不执行”的时候呢,程序不执行就是没有分配CPU时间,这时线程无法响应JVM的中断请求,JVM显然不太可能的等待线程重新被分配CPU时间。

   安全区域是指一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。

  在线程执行到安全区域代码时,首先标识自己进入安全区域,当这段时间里JVM发起GC,不用管标识为安全区域的线程了。 在线程要离开安全区域时,要检查系统是否已经完成了根节点枚举,如果完成,线程继续执行,否则等待直到收到可以安全离开安全区域的信号为止。

五、常见的垃圾收集器

在这里插入图片描述

5.1 Stop the world

  顾名思义,“Stop the world”就是  JVM 由于要执行 GC 而停止了其他应用程序的运行,在任何 GC 算法中都可能会发生。假设有这么一个场景,你的程序正在愉快的运行,突然之间 JVM 要清理垃圾了。然后程序就陷入了10分钟的等待,是不是很抓狂?当然一般情况下会让你等待这么久,但是“Stop the world”会在一定程度上影响用户体验这是毋庸置疑的。所以,多数GC优化通过减少 Stop-the-world 时间来提升系统性能。

5.2 垃圾收集器和垃圾回收算法的关系

  说完“Stop the world”我们回到正题,回到我们垃圾收集器的模块,如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

  我们费尽心机对垃圾收集器进行比较,目的不是挑出一个最好的垃圾收集器,而是找到最合适的。 现在为止还没有最好的垃圾收集器出现,更没有说出现万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果 “完美垃圾收集器” 真的面世了,我们还需要做这些工作吗?

5.3 常用的垃圾收集器

5.3.1 Serial 收集器【新生代

  Serial收集器是最基本、历史最悠久的垃圾收集器。它是一个单线程收集器,“单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是 它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。 它会在用户不可见的情况下把用户正常工作的线程全部停掉。想象一下,当你结束一天的工作回到家中,喝着冰阔乐刷着副本正要打Boss,突然你的电脑说他要休息5分钟,你会是什么感觉?

  存在即合理,当然Serial 收集器也有优于其他垃圾收集器的地方,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
在这里插入图片描述
        Serial / Serial Old收集器运行示意图

5.3.2 ParNew 收集器【新生代

  ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为(控制参数、收集算法、分配规则、回收策略等等)和 Serial 收集器完全一样。

  除了支持多线程收集,ParNew 相对 Serial 似乎并没有太多改进的地方。但是它却是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。ParNew单核状态下不如Serial,多核线程下才有优势。
在这里插入图片描述
        ParNew / Serial Old收集器运行示意图

5.3.3 Parallel Scavenge 收集器【新生代

  Parallel Scavenge 收集器是一个新生代收集器,也是采用复制算法+并行。听起来和ParNew差不多对不对,那么它有什么特别之处呢?

  Parallel Scavenge 收集器关注点是吞吐量(CPU运行代码的时间与CPU总消耗时间的比值)

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

CMS 等垃圾收集器的关注点更多的是缩短用户线程的停顿时间(提高用户体验)。停顿时间越短就越适合和用户进行交互(响应速度快,可以优化用户体验),而高吞吐量则可以高效的利用CPU时间,尽快完成用户的计算任务。

  Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

  • -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
  • -XX:GCTimeRatio :设置吞吐量大小
  • -XX:+UseAdaptiveSizePolicy :开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden 与Survivor区的比例(-XX:SurvivorRatio)、 晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应调节策略

5.3.4 Serial Old 收集器【老年代

  Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

在这里插入图片描述
        Serial / Serial Old收集器运行示意图

5.3.5 Parallel Old 收集器【老年代

  Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
在这里插入图片描述
        Parallel Scavenge / Parallel Old收集器运行示意图

5.3.6 CMS 收集器【老年代

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常重视服务的响应速度,以期给用户最好的体验。。

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

  • 初始标记

   需要“Stop the world”,仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快。

  • 并发标记

  并发追溯标记,程序不会停顿。

  • 重新标记

   需要“Stop the world”修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

  • 并发清除

   清理垃圾对象,程序不会停顿
在这里插入图片描述
        Concurrent Mark Sweep 收集器运行示意图

CMS一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感
    在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低【CMS默认回收线程数:(CPU数量 + 3)/ 4】
  • 无法处理浮动垃圾

浮动垃圾:并发清理阶段用户线程还在运行,伴随程序运行产生的新的垃圾

解决:CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了在进行收集,需要预留有足够的内存空间提供并发收集时的程序运作使用

  CMS运行期间预留的内存无法满足程序需要时,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集

  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间 碎片产生。

5.3.7 G1 收集器

  G1 (Garbage-First) 是一款面向服务器的垃圾收集器,开发人员希望在未来可以换掉CMS收集器,它有如下特点

  • 并行与并发

  G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。

  • 分代收集

  虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。

  • 空间整合

  与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记–整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。这就意味着不会产生大量的内存碎片

  • 可预测的停顿

  这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,小号在垃圾收集上的时间不得超过N毫秒。

  G1收集器将整个Java堆内存划分为若干个内存大小相等的独立区域(Region),年轻代和老年代不再物理隔离,他们都是一部分Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最高达的Region。
  在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set 来避免全局扫描的
  G1中每个Region都有一个与之对应的RememberedSet,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier 暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

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

  • 初始标记

   需要“Stop the world”,仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快。

  • 并发标记

  并发追溯标记,从GC Root开始对堆中对象进行可达性分析,找出存活对象,程序不会停顿。

  • 最终标记

   需要“Stop the world”修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Rembered Set Logs 数据合并到 Remembered Set 中,需要“Stop the world”,可并行执行。

  • 筛选回收

   对各个Region 的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划


5.3.8 理解GC日志

在这里插入图片描述
在这里插入图片描述

5.3.9垃圾收集器参数总结

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。SerialOld收集器将作为CMS收集器出现ConcurrentModeFailure失败后的后备收集器使用
UseParallelGC 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old ( PS MarkSweep)的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收
PretenureSize Threshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
SurvivorRatio 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表 Eden : Survivor=8 : 1
MaxTenuringThreshold 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进人老年代
UseAdaptiveSizePolicy 动态调整Java堆中各个区域的大小以及进人老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden和Survivor区的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数
GCTimeRatio GC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效
MaxGCPauseMillis 设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效
CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效
UseCMSCompactAtFullCollction 开关参数,默认开启,设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效
CMSFullGCsBeforeCompaction 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。默认值0,表示每次进入Full GC 时都进行碎片整理,仅在使用CMS收集器时生效

六、GC堆

  由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC GC和Full GC。

6.1 Minor GC【复制算法

  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

6.2 Full GC【标记——清除算法

  对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

  • 年老代(Tenured)被写满;
  • 持久代(Perm)被写满;
  • System.gc()被显示调用;
  • 上一次GC之后Heap的各域分配策略动态变化;

七、内存分配与回收策略

  对象的内存分配,往大方向来讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。

7.1 对象优先在Eden分配

  大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
--XX:+PrintGCDetails 收集器日志参数,在发生垃圾回收时打印内存回收日志

7.2 大对象直接进入老年代

大对象: 需要大量连续内存空间的Java对象

典型: 很长的字符串及数组。

  经常出现大对象容易导致内存还有不少空间时就提前触发垃圾回收以获取足够的连续空间来“安置”它们。虚拟机提供参数--XX:PretenureSizeThreshold ,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在 Eden 区以及两个Survivor 区之间发生大量的内存复制。

7.3 长期存活的对象将进入老年代

  如果对象在Eden 出生并经过了一次 Minor GC 后仍然存活,并且能被 Survivor 容纳,将被移动至 Survivor 空间中,并且对象年龄设为1,对象在Survivor区中每“熬过”一次Minor GC后,年龄都会增加1岁,当年龄达到一定程度(默认是15岁)时,就会晋升到老年代中。

--XX:MaxTenuringThreshold:设置对象晋升老年代的年龄阈值

7.4 动态对象年龄判定

  虚拟机并不总是要求对象必须达到一定年龄才能晋升老年代,如果Survivor空间中相同年龄的对象大小超过Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代中,无需等到特定要求的年龄。

7.5空间分配担保

  在发生Minor GC时,虚拟机会先检查老年代中最大可用的连续空间是否大于新生代所有对象总空间,如果该条件成立,那么 Minor GC 可以确保是安全的。若不成立,则虚拟机查看 HandlePromotionFailure设置值是否允许担保失败。
  若允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;若小于,或 HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次 Full GC
  新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的就是内存回收后新生代中的所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
  取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致 担保失败(Handle Promotion Failure),如果出现了担保失败,那就只好在失败后重新发起一次Full GC

发布了6 篇原创文章 · 获赞 0 · 访问量 132

猜你喜欢

转载自blog.csdn.net/qq_42216744/article/details/103240830
今日推荐