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)

    扫描二维码关注公众号,回复: 2237230 查看本文章
  • 标记-清除 (Mark-Sweep)

  • 标记-压缩(Mark-Compact)

  • 分代收集算法(Generational Collection)

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

核心流程

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

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 gc收集-按系统线程分

3.2.1 串行收集

串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。

3.2.2 并行收集

并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。

3.2.3 并发收集

相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。

什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。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后,跟着来一次带压缩的。

猜你喜欢

转载自blog.csdn.net/hardworking0323/article/details/81071401