关于jvm:JVM-垃圾收集器与内存分配策略

42次阅读

共计 6032 个字符,预计需要花费 16 分钟才能阅读完成。

1. 判断对象是否被回收

1. 援用计数算法

在对象中增加一个援用计数器,每当有一个中央援用它时,计数器值加 1;当援用生效时,计数器值减 1;任何时刻计数器为零的对象就是不可能再被应用的。(很难解决对象互相循环援用的问题。)

两个对象互相援用时,虚拟机也会进行回收,因而 Java 虚拟机并不是通过计数算法来判断对象存活的。

2. 可达性剖析算法

以 GC Roots 为起始点依据援用关系进行搜寻,可达的对象都是存活的,不可达的对象可被回收。

  • 虚拟机栈中局部变量表中援用的对象
  • 本地办法栈中 JNI(Native 办法) 中援用的对象
  • 办法区中类动态属性援用的对象
  • 办法区中的常量援用的对象

3. 援用

无论是通过援用计数算法判断对象的援用数量,还是通过可达性剖析算法判断对象是否援用可达,判断对象是否存活都和“援用相干”。

1. 强援用

被强援用的对象不会被回收。

应用 new 一个新对象的形式来创立强援用。

Object obj = new Object()

2. 软援用

被软援用关联的对象只有在内存不够的状况下才会被回收。
应用 SoftReference 类来创立软援用。

3. 弱援用

被弱援用关联的对象只能存活到下一次垃圾收集产生为止。所以肯定会被回收。

应用 WeakReference 类来实现弱援用。

4. 虚援用

又称为幽灵援用或者幻影援用,一个对象是否有虚援用的存在,不会对其生存工夫造成影响,也无奈通过虚援用失去一个对象。

为一个对象设置虚援用的惟一目标是能在这个对象被回收时收到一个零碎告诉。

应用 PhantomReference 来创立虚援用。

4. finalize()

当一个对象可被回收时,如果须要执行该对象的 finalize() 办法,那么就有可能在该办法中让对象从新被援用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 办法自救,前面回收时不会再调用该办法。

finalize() 办法运行代价很高,不确定性大,无奈保障各个对象的调用程序,因而最好不要应用。应用 try-finally 或者其余形式能够做得更好。

5. 回收办法区

因为办法区次要寄存永恒代对象,而永恒代对象的回收率比新生代低很多,所以在办法区上进行回收性价比不高。

次要是对 常量池 的回收和对 的卸载。

为了防止内存溢出,在大量应用反射和动静代理的场景都须要虚拟机具备类卸载性能。

类的卸载条件很多,须要同时满足以下三个条件,并且满足了条件也不肯定会被卸载:

  • 该类所有的实例都曾经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 曾经被回收。
  • 该类对应的 Class 对象没有在任何中央被援用,也就无奈在任何中央通过反射拜访该类办法。

2. 垃圾收集算法

1. 分代收集实践

将回收对象根据其年龄(对象熬过垃圾收集过程的次数)将 Java 堆划分出不同的区域。
个别将堆分为新生代和老年代。

  • 新生代应用:标记 – 复制算法
  • 老年代应用:标记 – 革除 或者 标记 – 整顿 算法

长处:

  • 较低的代价回收大量空间;应用较低的频率回收某个区域。兼顾了垃圾收集的工夫开销和内存的空间无效利用。
  • 不同区域采纳适当的收集算法。

2. 标记 - 革除算法

分为“标记和革除”两个阶段:首先标记出所有须要回收的对象,而后对立回收掉所有被标记的对象。
另外,还会判断回收后的分块与前一个闲暇分块是否间断,若间断,会合并这两个分块。

毛病:

  • 执行效率不稳固(可能有大量标记和革除动作)
  • 内存空间的碎片化问题:标记、革除后产生大量不间断的内存碎片,当有大对象须要进行内存调配时,会因为找不到足够内存进行调配对象而造成垃圾回收,频繁的垃圾回收影响效率和性能。

3. 标记 - 复制算法

将内存划分为大小相等的两块,每次只应用其中一块,当这一块内存用完了就将还存活的对象复制到另一块下面,而后再把应用过的内存空间进行一次清理。
毛病: 可用内存放大了一半。

当初的商业虚拟机都采纳这种收集算法回收新生代,然而并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次应用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全副复制到另一块 Survivor 上,最初清理 Eden 和应用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保障了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,这些对象将会用老年代的内存空间,即通过调配担保机制间接进入老年代。

4. 标记 - 整顿算法

让所有存活的对象都向内存空间一端挪动,而后间接清理掉边界以外的内存。

长处: 不会产生内存碎片,空间利用率高。
毛病:须要挪动大量对象并更新援用是一种极为累赘的操作。

3. HotSpot 的算法细节实现

先跳过。

4. 经典垃圾收集器


1. Serial 收集器

  1. 是单线程收集器,新生代收集器。

    • 只会应用一个处理器或者一条收集线程。
    • 必须暂停其余所有工作线程,直到它收集完结。
  2. 长处:简略高效,在单个 CPU 环境下,因为没有线程交互的开销,因而领有最高的单线程收集效率。
  3. 在客户端模式下的默认新生代收集器。因为在该场景下内存一般来说不会很大。它收集一两百兆新生代的进展工夫最多能够管制在一百多毫秒以内,只有不是太频繁产生收集,这点进展工夫是能够承受的。

2. ParNew 收集器

  1. 是 Serial 收集器的多线程并行版本,新生代收集器。
  2. 在服务端模式下的默认新生代收集器。除了性能起因外,次要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合应用。

3. Parallel Scavenge 收集器

  1. 多线程并行的新生代收集器。
  2. 次要指标是达到一个可管制的 吞吐量,这里的吞吐量指 CPU 用于运行用户程序的工夫占总工夫的比值。
  3. 进展工夫越短就越适宜须要与用户交互的程序,而高吞吐量则能够高效率地利用 CPU 工夫,适宜用在后盾运算而不须要太多交互的剖析工作。
  4. 缩短进展工夫是以就义吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量降落。
  5. 垃圾收集的自适应的调节策略 (GC Ergonomics):
    激活开关参数 -XX:+UseApativeSizePolicy, 不须要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、降职老年代对象年龄等细节参数了。虚构机会依据以后零碎的运行状况收集性能监控信息,动静调整这些参数以提供最合适的进展工夫或者最大的吞吐量。

4. Serial Old 收集器

  1. 是 Serial 收集器的老年代版本,是一个单线程收集器。
  2. 在客户端模式下应用。
  3. 如果用在 Server 场景下,它有两种用处:

    • 在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配应用。
    • 作为 CMS 收集器的后备预案,在并发收集产生 Concurrent Mode Failure 时应用。

5. Parallel Old 收集器

  1. Parallel Scavenge 收集器的老年代版本,反对多线程并发收集。
  2. 在重视吞吐量以及 CPU 资源缺稀的场合,都能够优先思考 Parallel Scavenge 加 Parallel Old 收集器。

6. CMS (Concurrent Mark Sweep) 收集器

  1. 以获取最短回收工夫为指标的收集器,基于 标记 – 革除算法。
  2. 运作过程

    • 初始标记: 仅仅只是标记一下 GC Roots 能间接关联到的对象,速度很快,须要进展。
    • 并发标记: 从 GC Roots 的间接关联对象开始遍历整个对象图的过程,它在整个回收过程中耗时最长,不须要进展。
    • 从新标记: 为了修改并发标记期间因用户程序持续运作而导致标记产生变动的那一部分对象的标记记录,须要进展。
    • 并发革除: 删除掉标记阶段判断的曾经死亡的对象,不须要进展。
  3. 毛病:

    • 吞吐量低:在并发阶段会因为占用了一部分线程而导致应用程序变慢,升高总吞吐量。
    • 无奈解决浮动垃圾,可能呈现 Concurrent Mode Failure。

      • 浮动垃圾是指并发革除阶段因为用户线程持续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时能力进行回收。
      • 因为浮动垃圾的存在,因而须要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样期待老年代快满的时候再回收。如果预留的内存不够寄存浮动垃圾,就会呈现 Concurrent Mode Failure,这时虚拟机将长期启用 Serial Old 来代替 CMS。
    • 标记 – 革除算法导致的空间碎片,往往呈现老年代空间残余,但无奈找到足够大间断空间来调配以后对象,不得不提前触发一次 Full GC。

6. G1 (Garbage First) 收集器

  1. 次要面向服务端利用。
  2. Mixed GC 模式:面向堆内任何局部来组成回收集进行回收,不再辨别它是哪个分代,而是哪块内存中寄存的垃圾数量最多,回收收益最大。
  3. 基于 Region 的堆内存散布:

    • G1 把堆划分成多个大小相等的独立区域(Region),将 Region 作为单次回收的最小单位,即每次收集到的内存空间都是 Region 大小的整数倍。每个 Region 依据须要能够表演新生代的 Eden 空间、Survior 空间,或者老年代空间,全局来看,G1 采纳了“标记 - 整顿”算法。
    • 通过记录每个 Region 垃圾回收工夫以及回收所取得的空间(这两个值是通过过来回收的教训取得),并保护一个优先列表,每次依据容许的收集工夫,优先回收价值最大的 Region。
    • 每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的援用对象所在的 Region。通过应用 Remembered Set,在做可达性剖析的时候就能够防止全堆扫描。
  4. 如果不计算保护 Remembered Set 的操作,G1 收集器的运作大抵可划分为以下几个步骤:

    • 初始标记
    • 并发标记
    • 最终标记:为了修改在并发标记期间因用户程序持续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变动记录在线程的 Remembered Set Logs 外面,最终标记阶段须要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段须要进展线程,然而可并行执行。
    • 筛选回收:首先对各个 Region 中的回收价值和老本进行排序,依据用户所冀望的 GC 进展工夫来制订回收打算。把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧的 Region 的全副空间。这里的操作波及存活对象的挪动,必须暂停用户程序。
  5. 具备如下特点:

    • 空间整合:整体来看是基于“标记 – 整顿”算法实现的收集器,从部分(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
    • 可预测的进展:能让使用者明确指定在一个长度为 M 毫秒的工夫片段内,耗费在 GC 上的工夫不得超过 N 毫秒。

4. 内存调配与回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,因为新生代对象存活工夫很短,因而 Minor GC 会频繁执行,执行的速度个别也会比拟快。
  • Full GC:回收老年代和新生代,老年代对象其存活工夫长,因而 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

1. 内存调配策略

1. 对象优先在 Eden 调配

大多数状况下,对象在新生代 Eden 上调配,当 Eden 空间不够时,发动 Minor GC。

2. 大对象间接进入老年代

大对象是指须要间断内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的间断空间调配给大对象。当复制对象时,大对象的复制也意味着高额的内存复制开销。

-XX:PretenureSizeThreshold: 大于此值的对象间接在老年代调配,防止在 Eden 和 Survivor 之间来回复制,产生大量的内存复制操作。

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

为对象定义年龄计数器,对象在 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。

2. Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则绝对简单,有以下条件:

1. 调用 System.gc()

只是倡议虚拟机执行 Full GC,然而虚拟机不肯定真正去执行。不倡议应用这种形式,而是让虚拟机治理内存。

2. 老年代空间有余

老年代空间有余的常见场景为前文所讲的大对象间接进入老年代、长期存活的对象进入老年代等。

为了防止以上起因引起的 Full GC,该当尽量不要创立过大的对象以及数组。除此之外,能够通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还能够通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3. 空间调配担保失败

应用 复制算法 的 Minor GC 须要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。。

4. JDK 1.7 及以前的永恒代空间有余

在 JDK 1.7 及以前,HotSpot 虚拟机中的办法区是用永恒代实现的,永恒代中寄存的为一些 Class 的信息、常量、动态变量等数据。

当零碎中要加载的类、反射的类和调用的办法较多时,永恒代可能会被占满,在未配置为采纳 CMS GC 的状况下也会执行 Full GC。如果通过 Full GC 依然回收不了,那么虚构机会抛出 java.lang.OutOfMemoryError。

为防止以上起因引起的 Full GC,可采纳的办法为增大永恒代空间或转为应用 CMS GC。

5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间有余(可能是 GC 过程中浮动垃圾过多导致暂时性的空间有余),便会报 Concurrent Mode Failure 谬误,并触发 Full GC。

参考资料

https://github.com/CyC2018/CS-Notes

正文完
 0