java虚拟机_03_GC算法及垃圾回收

一、GC概念

1.1 GC的概念

  • Garbage Collection 垃圾收集(垃圾回收)
    回收java无用的对象
    不回收会导致内存泄露

  • 1960年 List 使用了GC

  • Java中,GC的对象是堆空间和永久区

  • GC的基本原理:
    将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

    • (1)对新生代的对象的收集称为minor GC;
    • (2)对旧生代的对象的收集称为Full GC;
    • (3)程序中主动调用System.gc()强制执行的GC为Full GC

1.2 JVM垃圾回收对象

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

  • (1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
  • (2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
  • (3)弱引用:在GC时一定会被GC回收
  • (4)虚引用:由于虚引用只是用来得知对象是否被GC

二、GC算法

2.1 引用计数法

指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况

  • 老牌垃圾回收算法
  • 通过引用计算来回收垃圾

使用者

  • COM
  • ActionScript3
  • Python

引用计数法实现

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用,该对象才会被回收。

引用计数法的问题

  • 引用和去引用伴随加法和减法,影响性能
  • 很难处理循环引用

引用计数法引起的内存泄露

  • 一是采用这种方法后,每次在增加变量引用和减少引用时都要进行加法或减法操作,如果频繁操作对象的话,在一定程度上增加的系统的消耗。
  • 二是这种方法无法处理循环引用的情况。再解释下什么是循环引用,假设有两个对象 A和B,A中引用了B对象,并且B中也引用了A对象, 那么这时两个对象的引用计数器都不为0,但是由于存在相互引用导致无法垃圾回收A和 B,导致内存泄漏。

2.2 根搜索算法(Tracing)

  • 复制 (Coping)

  • 标记-清除 (Mark-Sweep)

  • 标记-压缩(Mark-Compact)

  • 分代收集算法(Generational Collection)

java采用。通过一系列名为“GC root”的对象作为起点,从这些点开始向下搜索,搜索走过的路劲叫做引用链。当一个对象到GC root没有任何引用链时,则证明此对象是不可用的。这种算法也叫也叫引用链法/可达性算法。

核心流程

  • 如果有一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收;(第一步)
  • 从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象(不可达,年老代回收,第二步)

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

  • 虚拟机栈中的引用的对象。
  • 方向区中的类静态属性引用的对象。
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象
2.2.1 复制算法(Java中新生代采用)

核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象,然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。新生代的内存空间通常都是所有代里最大的,适用复制算法。

很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片

  • 与标记-清除算法相比,复制算法是一种相对高效的回收方法
  • 不适用于存活对象较多的场合 如老年代
  • 将原有的内存空间分为两块(空间浪费),每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收



复制算法优点
没有标记和清除的过程,效率高,没有内存碎片,可以利用 Bump-the-pointer(指针碰撞)技术实现快速内存分配,因为已用和未用的内存各自一边,内存分布规整有序,当新对象分配时就可以通过修改指针偏移量将其分配在第一个空闲的内存位置上,从而快速分配内存,否则只能使用空闲列表(Free List)方式分配内存

复制算法缺点
开辟专门的空间存放存活对象,占用更多的内存。

2.2.2 标记清除
  • 标记-清除算法是现代垃圾回收算法的思想基础。
  • 标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
    一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。
    因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

  • 这个方法是将垃圾回收分成了两个阶段:标记阶段和清除阶段。
    在标记阶段,通过跟对象,标记所有从跟节点开始的可达的对象,那么未标记的对象就是未被引用的垃圾对象。
    在清除阶段,清除掉所以的未被标记的对象。

标志清除算法优点

  • 不需要额外的空间

标志清除算法缺点

  • 垃圾回收后可能存在大量的磁盘碎片(内存碎片),重复扫描,性能低,而且产生。
2.2.3 标记压缩(Java中老年代采用)
  • 用GC时的时间消耗换来的是更多更高效使用的可用空间;

核心流程

一样是从从根集合开始扫描,对存活动对象进行标记,然后重新扫描整个内存空间,并往一个方向移动存活对象,虽然移动对象的消耗时间,但不产生内存碎片,可以通过 Bump-the-pointer(指针碰撞)快速分配内存。

  • 和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。
  • 但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。
  • 之后,清理边界外所有的空间。

应用场景

标记压缩算法的应用场景是老年代,说老年代执行GC效率低,可用对象重排整理是主要原因。

2.2.4 分代思想(分代法:Java堆采用)
  • 主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率。
  • 比如Java虚拟机中的堆就采用了这种方法分成了新生代和老年代。然后对于不同的代采用不同的垃圾回收算法。 新生代使用了复制算法,老年代使用了标记压缩清除算法。

  • 虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

年轻代:

  • 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

年老代:

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

持久代:

  • 用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
2.2.5 分区算法
  • 这种方法将整个空间划分成连续的不同的小区间,每个区间都独立使用,独立回收,好处是可以控制一次回收多少个小区间

三、GC收集与工作机制

3.1 gc回收过程

SUN的jvm内存池被划分为以下几个部分:

  • 1、Eden Space (heap) 内存最初从这个线程池分配给大部分对象(对象产生区)
  • 2、Survivor Space (heap) 用于保存在eden space内存池中经过垃圾回收后没有被回收的对象。(复制算法工作区)
  • 3、Tenured Generation (heap) 用于保持已经在survivor space内存池中存在了一段时间的对象。
  • 4、Permanent Generation (non-heap) 保存虚拟机自己的静态(reflective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的。
  • 5、Code Cache (non-heap) HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)。

综上 jvm的内存回收过程是这样的

对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。

当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。

若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时jvm gc停止所有在堆中运行的线程并执行清除动作。

3.2 HotSpot 垃圾回收器

3.2.1 新生代可用的垃圾回收器

Serial Coping(串行复制),Parallel Scavenge(并行复制),ParNew(并发复制)这三种回收器都是基于复制算法,复制 young eden 和 young from 中还存活的对象到 young to,或者根据设定对象的大小和 GC 次数直接晋升到 old,清空 young eden 和 young from 中的垃圾,下一次 GC 发生交换 young from 和 young to,只可使用于新生代,是在 young eden 内存空间不足以分配给对象时触发 Minor GC(新生代垃圾回收)。

3.2.2 老年代可用垃圾回收器
  • Serial Old (串行标记-清理-压缩)

  • Parallel Old(并行标记-压缩)

  • CMS Concurrent Mark-Sweep(并发标记清除)

3.2.3 垃圾收集器的组合

Serial Coping(串行复制)

适合客户端工作,不适合在服务器运行,针对单 CPU,小新生代,不太在乎暂停时间的应用,可通过- XX:+UseSerialGC手动指定新生代使用 Serial Coping(串行复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收。

ParNew(并发复制)

是 Serial Coping(串行复制)的多线程版本,在多 CPU 核情况下可以提高收集能力,但如果是单 CPU 条件下,还要来回切换任务,不一定比 Serial Coping(串行复制)收集能力强,通过- XX:+UseParNewGC手动指定新生代使用 ParNew(并发复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收。

Parallel Scavenge(并行复制)

跟 ParNew(并发复制)相比更注重于吞吐量而不是低延迟,如果吞吐量优先,必然会降低 GC 的频次,也就造成 GC 回收垃圾量更多、时间更长。如果低延迟优先,为了降低每次的暂停时间,就得高频的回收,这频繁的回收又会导致吞吐量的下降,所以吐吞量和低延迟是对矛盾体,适合多 CPU、高 IO 密集操作、高计算消耗的应用,通过XX:+UseParallelGC手动指定新生代使用 Parallel Scavenge(并行复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收。

Serial Old (串行标记 - 清理 - 压缩)

单线程串行回收,停顿时间长,可以使用

  • XX:+PrintGCApplicationStoppedTime

查看暂停时间,适合客户端使用,不会产生内存碎片

Parallel Old(并行标记 - 压缩)

根据 GC 线程数划分若干区域(Region),并行做标记,重新扫描,定位到需要压缩的 Region,统计 Region 里所有存活对象的下次要移动的目的地地址,然后并行的往一端压缩,不产生内存碎片,整理后的空闲区域是连续的,通过- XX:+UseParallelOldGC手动指定新生代使用 Parallel Scavenge(并行复制)收集器,老年代使用 Parallel Old(并行标记 - 压缩)收集器执行内存回收。

CMS Concurrent Mark-Sweep(并发标记清除)

第一阶段是初始标记,需要 Stop-the-world,这阶段标记出那些与根对象集合所连接的不可达的对象,标记完就会被暂停的应用线程;

第二阶段是并发标记,这阶段是应用线程和回收线程交替执行,把第一步标记为不可达的对象标记为垃圾对象,由于是交替进行,一开始被标记为垃圾的对象,后面应用线程可能更改对象的引用关系导致标记错误;

所以第三阶段重新标记,需要 Stop-the-world,修正上个阶段由于对象引用或者新对象创建导致的标记错误,这阶段只有回收线程执行,确保修正的正确性。

经过三个阶段的标记,第四个阶段会并发的清除无有的对象释放内存,这阶段是应用线程和回收线程交替执行,如果用户应用线程产生了新的垃圾(浮动垃圾),只能留到下次 GC 进行回收,极端情况如果产生的新的垃圾,而老年代的预留空间又不够,就会产生 Concurrent Mode Failure,这个时候只能通过后备的 Serial Old (串行标记 - 清理 - 压缩)来进行垃圾回收。

又因为 CMS 并没有用到压缩算法,回收后会产生内存碎片,为新对象分配内存无法使用 Bump-the-pointer(指针碰撞)技术实现快速内存分配,只能使用空闲列表(Free List :JVM 会维护一张可用内存地址的列表,当需要分配空间,就从列表搜索一段和对象大小一样的连续内存块用于存放要生成的对象实例)方式分配内存。

但也可以通过- XX:CMSFullGCsBeforeCompaction,用于指定经过多少次 Full GC 后对内存碎片整理压缩,由于内存碎片不是并发执行,会带来更长的停顿时间,通过- XX:+UseConcMarkSweepGC设定新生代使用 ParNew(并发复制)收集器,老年代使用 CMS Concurrent Mark-Sweep(并发标记清除)收集器执行内存回收,当出现浮动垃圾导致 Concurrent Mode Failure 或者新对象分配内存失败时,通过备用组合新生代使用 ParNew(并发复制)收集器,老年代使用 Serial Old (串行标记 - 清理 - 压缩)收集器执行内存回收,适用于要求暂停时间短,追求快速响应的应用,如互联网应用。

JVM回收需要注意的点:

在执行 Minor GC 的时候,JVM 会检查老年代中最大连续可用空间是否大于了当前新生代所有对象的总大小,如果大于,则直接执行 Minor GC;

如果小于了,JVM 会检查是否开启了空间分配担保机制;如果开启了,则 JVM 会检查老年代中最大连续可用空间是否大于了历次晋升到老年代中的平均大小;

如果大于则会执行 Minor GC,如果小于则执行改为执行 Full GC,如果没有开启则直接改为执行 Full GC。

当老年代(Major GC)和永久代发生 GC 时,除了 CMS 外都会触发 Full GC,Full GC 就是先按新生代 GC 方式进行 Minor GC,再按照老年代的配置进行 Major GC,包含对老年代和永久代进行 GC,若 JVM 估计 Minor GC 会产生晋升失败,则会采用 Major GC 的配置进行 Full GC。

如果 Minor GC 执行失败则会执行 Full GC。

吞吐量:应用运行时间/总时间,暂停时间:每次 GC 造成的暂停

分区分代增量式式收集器:G1(Garbage-First)收集器

传统的分代收集也提供了并发收集,但最致命的是分代收集把整个堆空间划分成固定间隔的内存块,每次收集都很容易触发 Full GC 从而扫描整个堆空间,这会拖慢应用,而且要对整个堆空间都做内存碎片整理会很麻烦。

而增量式的收集方式是一种新的收集思想,增量收集把堆空间划分成一系列的小内存块(内存块大小可配置),使用时只使用部分内存块,等这部分内存块空间不足时,再把存活对象移动到未被使用过的内存块,避免整个堆用完了再 Full GC,可以一边使用内存一边收集垃圾。

G1 收集器将整个 Java 堆区分成约 2048 大小相同的 Region 块(分新生 Region 块、幸存 Region 块、老年 Region 块),Region 块大小在 1MB 到 32MB 之间,每个对象会被分配到 Region 块里,既可以被块内对象引用也可以被块外对象引用,在判断对象是否存活时,为了避免全堆扫描或者遗漏,是通过 Remembered Set 来检查 Reference 引用的对象是否存在不同的 Region 块中的。G1 在收集垃圾时,会对各个 Region 块的回收价值和成本做排序,根据用户配置的期望停顿时间来进行回收。

G1 收集器与 CMS 收集器执行过程类似。初始标记阶段,Stop-the-World,标记 GC Roots 可直接访问到的对象,为下一个阶段并发标记时,和应用线程交替执行时,有正确可有的 Region 来分配新建对象,并发标记阶段识别上个阶段标记对象的下层对象的活跃状态,找出存活的对象,也就是标记 GC Roots 可达对象;

最终标记阶段,Stop-the-World,修正上次线程交替执行产生的变动;

清除复制阶段,Stop-the-World,这阶段并不是最终标记执行完了就一定执行,毕竟是要 Stop-the-World,为了达到准实时(可配置在 M 毫秒内最多只占用 N 毫秒的时间进行垃圾回收)会根据用户配置的 GC 时间去决定是否做清除。

还有,因为清除复制阶段使用的是复制算法,每次清理都必须保证”to space” 空间是足够的(将存活的对象复制到未使用的 Region 块),所以只有已用空间达到了(1-h)*堆大小(h 是 G1 定义的一个堆空间的百分比阈值,是个固定值)才执行清除,把存活的对象往一个方向移动到”to space” 并整理内存,不会产生内存碎片。

接着把”Eden space” “from space” 的垃圾对象清理,根据维护的优先列表,优先回收价值最大的 Region,通过五个阶段完成垃圾收集,可以通过设定 - XX:UseG1GC 在整个 Java 堆使用 G1 进行垃圾回收,G1 适合高吞吐、低延时、大堆空间的应用。

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

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

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。full GC会影响系统短暂性停歇。

有如下原因可能导致Full GC:

  • 1、System.gc()方法的调用
    系统建议执行Full GC,但是不必然执行
    此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

  • 2、老年代代空间不足
    老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

  • 3、永生区空间不足
    JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

  • 4 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

  • 5由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

  • 6、堆中分配很大的对象
    所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有恒则成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值