垃圾收集器与内存分配策略

  • 垃圾收集器与内存分配策略
    • 1、那些内存需要回收
    • 2、对象已死吗?
      • 1、引用计数法
      • 2、可达性分析法
      • 3、再谈引用
    • 3、垃圾收集算法
      • 1、标记-清除算法
      • 2、复制算法
      • 3、标记-整理算法
      • 4、分代收集算法
      • 5、堆内存分区详解
      • 6、垃圾收集器
        • 1、新生代收集器:
          • 1、Serial收集器
          • 2、ParNew 收集器
          • 3、Parallel Scavenge收集器
        • 2、老年代收集器
          • 1、Serial Old收集器
          • 2、Parallel Old收集器
          • 3、CMS收集器
        • 3、G1收集器
      • 7、内存分配回收策略
        • 1、对象优先在 Eden 分配
        • 2、大对象直接进入老年代
        • 3、长期存活的对象进入老年代
        • 4、动态对象年龄判断
        • 5、分配空间担保

垃圾收集器与内存分配策略

目前内存分配和垃圾回收已经实现了“自动化”,为什么我们还要关注它们呢?答案很简单:当排查各种内存溢出、内存泄露问题时,当垃圾回收成为提高系统并发量的瓶颈时,有必要对内存的动态分配和垃圾回收进行监控和调节。GC需要考虑三个问题:哪些内存需要回收?什么时候回收?如何回收?

1、那些内存需要回收

  • 线程私有区:jvm 内存结构中,我们知道 程序计数器、虚拟机栈、本地方栈这3个区域,是线程私有区域,跟线程的生命周期一样,所以这些内存区域就不用考虑内存回收问题了
  • 线程共享区:java堆跟方法区是线程共享的,随着虚拟机的启动而存在,这部分内存的分配和回收是动态的,我们只有在程序运行时才知道创建了哪些对象,需要多少内存,GC针对的就是这部分内存。

2、对象已死吗?

垃圾回收器对堆内存进行回收之前,需要判断堆中对象是“活着”还是“死去”(不能能再被任何途径使用的对象),有两种方法可以实现:引用计数法可达性分析法

1、引用计数法

给对象添加一个引用计数器,每当有一个地方引用它,计数器就+1,;当引用失效时,计数器就-1;任何时刻计数器都为0的对象就是不能再被使用的。

实现简单效率很高,但是很难解决多个对象之间循环引用的问题(一般不用该方法)

2、可达性分析法

通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

准确高效,但是实现起来比较麻烦

在java中可以作为GC Roots的对象有以下几种:

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

3、再谈引用

无论是引用计数法还是可达性分析算法,判断是否存活都与“引用”有关。在JDK1.2以前,一个对象只有“被引用”或者“没有被引用”两种状态。它无法描述这样一类对象:当内存空间充足时,可以保存在内存中;如果内存空间在垃圾回收后依然很紧张,则可以抛弃这些对象。所以在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为以下四种引用。强度大小为 强引用>软引用>弱引用>虚引用

  • 强引用(Strong Reference)
    • 类似 Object obj = new Object();只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用(Soft Reference)
    • 用来描述一些还有用但不是必须的对象
    • 软引用所关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存异常。JDK 1.2 之后,提供了 SoftReference 类实现软引用。
  • 弱引用(Weak Reference)
    • 描述非必须的对象,强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。
  • 虚引用(Phantom Reference)
    • 也称幽灵引用或者幻影引用,是最弱的一种引用。一个对象是否有虚引用,完全不会对其生存时间够成影响,也无法通过虚引用来取得一个对象实例。为一个对象关联虚引用的唯一目的就是希望在这个对象被收集器回收时,收到一个系统通知。JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。

3、垃圾收集算法

1、标记-清除算法

  • 分为两个阶段 标记,清除首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。
  • 最基础的收集算法,后面的几种垃圾回收算法都是基于这种思路并对其不足进行改进而得到的。
  • 不足有俩个:
    • 效率问题。标记和清除过程的效率都不高。
    • 空间问题。标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致,程序分配较大对象时无法找到足够的连续内存,不得不提前出发另一次垃圾收集动作。

2、复制算法

  • 将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。
  • 解决了标记-清除算法的效率问题。复制算法使得每次都是针对其中的可用内存的半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
  • 不足:
    • 浪费空间。将内存缩小为原来的一半,代价太高昂。
    • 效率问题。在对象存活率较高时,需要执行较多的复制操作,效率会变低。
  • 应用:回收新生代:
    • 因为新生代中的对象大部分很快就死亡,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。每次使用 Eden 和其中的一块 Survivor。
    • 当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
    • Hotspot 虚拟机默认 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存是会被“浪费”的。当然,无法保证每次回收都只有不多于10%的对象存活,如果10%的Survivor 空间不够用,可以临时使用老年代的内存(分配担保机制)。

3、标记-整理算法

复制收集算法在对象存活率比较高的时候,要进行较多的复制,效率会降低,所以老年代(存活率高)一般不采用这种算法,为了解决这一问题,根据老年代的特点提出来了标记整理法

  • 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。
  • 解决了复制算法的空间和效率问题
  • 应用:现在的商用虚拟机都是用这种算法来回收老年代

4、分代收集算法

  • 根据对象的存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点,采用最适当的收集算法。
  • 对于新生代,每次垃圾收集时会有大批对象死去,只有少量存活,所以选择复制算法,只需要少量存活对象的复制成本就可以完成收集。
  • 对于老年代,对象存活率高、没有额外空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法进行回收。

优点:可以根据实际情况选择具体的算法

应用:现在的商用虚拟机的垃圾回收都采用这种算法。

5、堆内存分区详解

为什么要将堆内存分区?

  • 分区是为了对堆中对象进行模块化,以提高 JVM 的执行效率

怎么分的区?

  • 新生代
    • 主要用来存储新创建的对象,内存较小的话,MinorGC会频繁,新生代又分为三个区域:Eden、ServivorFrom、ServivorTo。
    • Eden
    • 当对象在堆创建时,一般进入Eden。如果新创建的对象占用内存很大,则直接分配到老年代。当Eden区内存不够时会触发MinorGC,对新生代区进行一次垃圾回收。
    • ServivorFrom
    • 上一次GC的幸存者,作为这一次MinorGC的被扫描者
    • ServivorTo
    • 保留了这一次MinorGC后的幸存者
    • MinorGC:采用复制算法。
    • 扫描Eden和ServivorFrom,将存活的对象复制到ServivorTo,并将这些对象的年龄+1。(如果ServivorTo已经满,则复制到老年代。)
    • 扫描ServivorFrom时,如果对象已经经过了几次的扫描仍然存活,达到了老年代的标准,JVM会将其移到老年代
    • 扫描完毕后,清空Eden和ServivorFrom,然后交换ServivorFrom和ServivorTo,即ServicorTo成为下一次GC时的ServicorFrom区
  • 老年代
    • 主要用来存储长时间被引用的对象。它里面存放的是经过几次在新生代进行扫描仍存活的对象。因为老年代对象比较稳定,所以MajorGC频率较小。 MajorGC:采用标记-整理算法。

6、垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

垃圾收集器与内存分配策略_第1张图片

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器

在介绍这些收集器各自的特性之前,我们先来明确一个观点:虽然我们是在对各个收集器进行比较,但并非为
了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择
的只是对具体应用最合适的收集器。这点不需要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用
的完美收集器存在,那HotSpot虚拟机就没必要实现那么多不同的收集器了

1、新生代收集器:

1、Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。

垃圾收集器与内存分配策略_第2张图片

一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。“Stop The World”这个名字也许听起来很酷,但这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的

HotSpot虚拟机开发团队为消除或者减少工作线程因内存回收而导致停顿的努力一直在进行着,从Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成果Garbage First(G1)收集器我们看到了一个个越来越优秀(也越来越复杂)的收集器的出现,用户线程的停顿时间在不断缩短。

说到这里,可能大家已经把Serial收集器描述成一个“老而无用、食之无味弃之可惜”的鸡肋了,但实际上到
现在为止,它依然是虚拟机运行在 Client 模式下的默认新生代收集器,在单个 CPU 环境中效率最高,主要应用于客户端

2、ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

应用场景:

  • ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。 除了Serial收集器外,目前只有它能与CMS收集器配合工作。
  • 在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
  • 不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。

优势:

  • 多线程
  • 除了Serial收集器外,目前只有它能与CMS收集器配合工作。

Serial收集器与ParNew收集器的比较:

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的

3、Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

特性

  • 新生代收集器;
  • 采用复制算法;
  • 多线程收集;

看上去和ParNew收集器一样,它有什么特别之处呢?

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

应用场景
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

2、老年代收集器

1、Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集
器的主要意义也是在于给Client模式下的虚拟机使用

2、Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本,JDK1.6中才开始提供。 jdk1.8默认 Server端默认使用 Parallel Scavenge(新生代)、Parallel Old(老年代)

Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器

3、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的
Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,
以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

CMS收集器是基于“标记—清除”算法实现的

运作过程:

  • 初始标记(CMS initial mark)
    • 需要”Stop The World”;
    • 仅标记一下GC Roots能直接关联到的对象,速度很快;
  • 并发标记(CMS concurrent mark)
    • 进行GC Roots Tracing;
    • 刚才产生的集合中标记出存活对象,并不能保证可以标记出所有的存活对象;
    • 和用户线程一起并发执行;
  • 重新标记(CMS remark)
    • 需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短;
    • 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
    • 采用多线程并行执行来提升效率;
  • 并发清除(CMS concurrent sweep)
    • 回收所有的垃圾对象;
    • 和用户线程一起并发执行

垃圾收集器与内存分配策略_第3张图片

整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作,所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行。

优势

  • 并发收集;
  • 低停顿;

劣势

  • CMS收集器对CPU资源非常敏感
    • 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低
    • CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
    • 针对这种情况,曾出现了”增量式并发收集器“(Incremental Concurrent Mark Sweep/i-CMS)。类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间。但效果并不理想,JDK1.6后就官方不再提倡用户使用。
  • CMS收集器无法处理浮动垃圾
    • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    • CMS需要更多的内存:也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败。
    • Concurrent Mode Failure解决方案**:出现“Concurrent Mode Failure”后,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  • CMS收集器会产生大量空间碎片
    • 问题原因:CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
    • 解决方法:
    • -XX:+UseCMSCompactAtFullCollection,默认开启,使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程。但合并整理过程无法并发,停顿时间会变长
    • -XX:+CMSFullGCsBeforeCompaction设置执行多少次不压缩的Full GC后,来一次压缩整理, 为减少合并整理过程的停顿时间。默认为0,也就是说每次执行Full GC都会进行压缩整理。

3、G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,早在JDK 1.7刚刚确立项目目标,目前 jdk1.9 已经正式使用该垃圾收集器

G1是一款面向服务端应用的垃圾收集器,与其他GC收集器相比,G1具备如下特点。
特性

  • 并行与并发。
    • G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间。
    • 部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集
    • 与其他收集器一样,分代概念在G1中依然得以保留。
    • 虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合
    • 与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的
    • 这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿
    • 这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点。但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

Java堆内存布局的改变:

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

也就是说,G1收集器不需要配合其他的收集器,就能自己回收整个堆

G1可预测的停顿的实现原理

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

运作过程

不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

  1. 初始标记(Initial Marking)
    • 仅标记一下GC Roots能直接关联到的对象;
    • 且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
    • 需要”Stop The World”,但耗时很短;
  2. 并发标记(Concurrent Marking)
    • 从GC Root开始对堆中对象进行可达性分析,找出存活的对象;
    • 这阶段耗时较长,但可与用户程序并发执行。
  3. 最终标记(Final Marking)
    • 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
    • 虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中;
    • 需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短;
    • 采用多线程并行执行来提升效率;
  4. 筛选回收(Live Data Counting and Evacuation)
    • 首先对各个Region的回收价值和成本进行排序;
    • 然后根据用户期望的GC停顿时间来制定回收计划;
    • 最后按计划回收一些价值高的Region中垃圾对象,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率;
    • 可以并发进行,降低停顿时间,并增加吞吐量;

垃圾收集器与内存分配策略_第4张图片

优势

  • 并发收集;
  • 低停顿;
  • 分代收集
  • 空间整合
  • 除了追求低停顿外,还能建立可预测的停顿时间模型

7、内存分配回收策略

1、对象优先在 Eden 分配

对象通常在新生代的Eden区进行分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,与Minor GC对应的是Major GC、Full GC。

  • Minor GC

    • 指发生在新生代的垃圾收集动作,非常频繁,速度较快。
  • Major GC

    • 指发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会引起Major GC,一般在GC日志中统称为GC,不频繁。
  • Full GC

    • 指发生在老年代和新生代的GC,速度很慢,需要Stop The World。

来看下面一段代码,虚拟机参数为-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8,即10M新生代,10M老年代,10M新生代中有8M的Eden区,两个Survivor区各1M。

public class EdenAllocationTest
{
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args)
    {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[2 * _1MB];
        byte[] allocation4 = new byte[4 * _1MB];
    }
}

Client 模式下:

垃圾收集器与内存分配策略_第5张图片

Server 模式下:

垃圾收集器与内存分配策略_第6张图片

看到在Client模式下,最后分配的4M在新生代中,先分配的6M在老年代中;在Server模式下,最后分配的4M在老年代中,先分配的6M在新生代中。说明不同的垃圾收集器组合对于对象的分配是有影响的。讲下两者差别的原因:

  • Client模式下:

    • 新生代分配了6M,虚拟机在GC前有 6487K,比6M也就是6144K多,多主要是因为 TLAB 和EdenAllocationTest 这个对象占的空间,TLAB可以通过“-XX:+PrintTLAB”这个虚拟机参数来查看大小。
    • OK,6M多了,然后来了一个4M的,Eden+一个Survivor总共就9M不够分配了,这时候就会触发一次Minor GC。但是触发Minor GC也没用,因为allocation1、allocation2、allocation3三个引用还存在,另一块1M的Survivor也不够放下这6M,那么这次Minor GC的效果其实是通过分配担保机制将这6M的内容转入老年代中。
    • 此时新生代 GC 之后内存够分配了,所以4M顺利进入新生代。
  • Server模式下,

    • 前面都一样,但是在GC的时候有一点区别。
    • 在GC前还会进行一次判断,如果要分配的 内存>= Eden区大小的一半,那么会直接把要分配的内存放入老年代中。要分配4M,Eden区8M,刚好一半,而且老年代10M,够分配,所以4M就直接进入老年代去了

2、大对象直接进入老年代

需要大量连续内存空间的Java对象称为大对象,大对象的出现会导致提前触发垃圾收集以获取更大的连续的空间来进行大对象的分配。虚拟机提供了-XX:PretenureSizeThreadshold参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。

-XX:PretenureSizeThreshold 这个参数对 Serial+Serial Old垃圾收集器组合有效而对 Parallel+Serial Old垃圾收集器组合无效。**

3、长期存活的对象进入老年代

每个对象有一个对象年龄计数器,与前面的对象的存储布局中的GC分代年龄对应。对象出生在Eden区、经过一次Minor GC后仍然存活,并能够被Survivor容纳,设置年龄为1,对象在Survivor区每次经过一次Minor GC,年龄就加1,当年龄达到一定程度(默认15),就晋升到老年代,虚拟机提供了-XX:MaxTenuringThreshold来进行设置。

4、动态对象年龄判断

对象的年龄到达了MaxTenuringThreshold可以进入老年代,同时,如果在survivor区中相同年龄所有对象大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就可以直接进入老年代。无需等 MaxTenuringThreshold 中要求的年龄。

5、分配空间担保

在发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,若成立,则说明Minor GC是安全的,否则,虚拟机需要查看 HandlePromotionFailure 的值,看是否运行担保失败,若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次Minor GC;若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改成一次Full GC,以上是JDK Update 24之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

冒险是指经过一次Minor GC后有大量对象存活,而新生代的survivor区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把survivor区无法容纳的对象直接进入老年代。

垃圾收集器与内存分配策略_第7张图片

垃圾收集器与内存分配策略_第8张图片

你可能感兴趣的:(JVM)