JVM垃圾回收机制小结

作者: 南山羊 | 来源:发表于2018-05-14 14:37 被阅读3次

    本文主要浅谈JAVA回收机制,让初学者对这一块大概有个简单的认识,同时也记录下自己学习的成果,温故而知新。

    疑问

    • 什么是垃圾回收
    • 为什么要进行垃圾回收
    • 如何进行垃圾回收
    • 常用优化

    1、什么是垃圾回收

    对于垃圾JVM的垃圾回收机制这里我们称为 GC ,众所周知,Java 语言不需要像 C++ 那样需要自己申请内存,自己释放内存,这些都是JVM帮我们做好了的,那么JVM对于内存的回收过程就成为垃圾回收(GC)。

    2、为什么要进行垃圾回收

    程序是运行在物理机之上,而一台物理机的内存数是有上限的,那么程序对于内存的使用也是有上限的,例如Java中大量的 new Object() 新建对象,会占用内存,往往我们在程序中只管新建,没有去管它的回收,那么这些对象在新建之后就会保存在内存当中,随着时间增加大量的垃圾对象就会撑爆内存,导致内存溢出。

    那么我们日常开发中为什么很少出现这种情况,这就是JVM的垃圾回收机制,自动帮我们进行了回收,由此可见,为什么要进行垃圾回收,是为了更合理使用内存,避免大量垃圾对象占用内存。

    3、如何进行垃圾回收

    3.1、判断机制

    上面谈到垃圾回收机制,实质是将垃圾对象进行回收,那么问题来了,如何判断一个对象是垃圾对象。

    • 引用计数法
      每个对象创建的时候,会分配一个引用计数器,当这个对象被引用的时候计数器就加1,当不被引用或者引用失效的时候计数器就会减1。任何时候,对象的引用计数器值为0就说明这个对象不被使用了,就认为是“垃圾”,可以被GC处理掉。
      优点:算法实现简单。
      缺点:无法解决循环引用(致命问题)
      public class A(){
          public A a;
      }
    
      public class B(){
        public B b;
      }
    
      public class demo(){
        public static void main(String[] args){
          A a = new A();
          B b = new B();
          a.b = b;
          b.a = a;
          // 这种情况 这两个对象永远被引用数为1,无法被垃圾回收机制回收。
        }
      }
    
    • 根搜索算法
      以一些特定的对象作为基础原始对象,或者称作“根”,不断往下搜索,到达某一个对象的路径称为引用链。如果一个对象和根对象之间有引用链,即根对象到这个对象是可到达的,则这个对象是活着的,不是垃圾,还不能回收。反之,如果一个对象和根对象之间没有引用链,根对象到这个对象的路径是不可达的,那么这个对象就是可回收的垃圾对象。(红色区域被回收)


      关系图
    3.2、回收机制

    上文谈到引用计数法存在致命问题,所以这里讲到的回收机制主要讲根搜索算法,当JVM根据根搜索算法找到垃圾对象之后,会如何去回收掉它,在此过程中会出现什么问题,以及各个算法的优势劣势,根据优势劣势如果去优化等等...

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

      i.标记Mark:从GC ROOTS开始,遍历堆内存区域的所有根对象,对在引用链上的对象都进行标记。这样下来,如果是存活的对象就会被做了标记,反之如果是垃圾对象,则没做有标记。GC很容易根据有没有被做标记就完成了垃圾对象回收。

      ii.清除Sweep:遍历堆中的所有的对象(标记阶段遍历的是所有根节点),找到未被标记的对象,直接回收所占的内存,释放空间。

      优点:没有产生额外的内存空间消耗,内存利用率高。

      缺点:效率低,清除阶段要遍历所有的对象;回收的垃圾对象是在各个角落的,直接回收垃圾对象,导致存在不连续的内存空间,产生内存碎片。标记-清除算法操作的对象是【垃圾对象】,对于活着的对象(被标记的对象),它则直接不理睬。

    • 复制算法(Copying)

      复制算法把内存区间中年轻代一分为二(Eden与Survivor),有对象存在的一半区间称为“活动区间”,没有对象存在处于空闲状态的空间则为“空闲区间”。当内存空间不足时触发GC,先采用根搜索算法标记对象,然后把活着的对象全部复制到另一半空闲区间上,复制算法的“复制”就来自这一操作。复制到另一半区间的时候,严格按照内存地址依次排列要存放的对象,然后一次性回收垃圾对象。
      这样原来的空闲区间在GC后就变成活动区间,而且内存顺序齐整美观。原来的活动区间在GC后就变成了完全空的空闲区间,等待下一次GC把活的对象被copy进来。

    • 标记-整理算法(Mark-Compact)

      i.标记:这个阶段和标记-清除Mark-Sweep算法一样,遍历GC ROOTS并标记存活的对象。
      ii.整理:移动所有活着的对象到内存区域的一侧(具体在哪一侧则由GC实现),严格按照内存地址次序依次排列活着的对象,然后将最后一个活着的对象地址以后的空间全部回收。

      优点:内存空间利用率高,消除了复制算法内存减半的情况;GC后不会产生内存碎片。

      缺点:需要遍历标记活着的对象,效率较低;复制移动对象后,还要维护这些活着对象的引用地址列表。

    • 分代回收算法(Generational Collecting)
      分代回收算法就是现在JVM使用的GC回收算法。


      内存结构.png

    i.先来看看简单化后的堆的内存结构:

    Java堆 = 年老代 + 年轻代
    (空间大小比例一般是3:1)
    
    年轻代 = Eden区 + Survivor区 + Survivor区
    (空间大小比例一般是8:1:1)
    

    ii.按照对象存活时间长短,我们可以把对象简单分为三类:

    短命对象:存活时间较短的对象,如中间变量对象、临时对象、循环体创建的对象等。这也是产生最多数量的对象,GC回收的关注重点。
    
    长命对象:存活时间较长的对象,如单例模式产生的单例对象、数据库连接对象、缓存对象等。
    
    长生对象:一旦创建则一直存活。
    

    iii.对象分配区域

    短命对象存在于年轻代,长命对象存在于年老代,而长生对象则存在于方法区中。
    由于GC的主要内存区域是堆,所以GC的对象主要就是短命对象和长命对象这类寿命“有限”的对象。
    
    年轻代回收

    当需要在堆中创建一个新的对象,而年轻代内存不足时触发一次GC,在年轻代触发的GC称为普通GC,Minor GC。注意到年轻代中的对象都是存活时间较短的对象,所以适合使用复制算法。这里肯定不会使用两倍的内存来实现复制算法了,牛人们是这样解决的,把年轻代内存组成是80%的Eden、10%的Survivor和10%的Survivor,然后在这些内存区域直接进行复制。

    刚开始创建的对象是在Eden中,此时Eden中有对象,而两个Survivor区没有对象,都是空闲区间。第一次Minor GC后,存活的对象被放到其中一个Survivor,Eden中的内存空间直接被回收。在下一次GC到来时,Eden和一个survivor中又创建满了对象,这个时候GC清除的就是Eden和这个放满对象的survivor组成的大区域(占90%),Minor GC使用复制算法把活的对象复制到另一个空闲的survivor区间,然后直接回收之前90%的内存。周而复始。始终会有一个10%空闲的Survivor区间,作为下一次Minor GC存放对象的准备空间。

    要完成上面的算法,每次Minor GC过程都要满足:
    存活的对象大小都不能超过Survivor那10%的内存空间,不然就没有空间复制剩下的对象了。但是,万一超过了呢?前面我们提到过年老代,对,就是把这些大对象放到年老代。

    例如下图中,Eden中有4个对象块,其中AC对象如果被标记为垃圾对象,在GC的时候就会将BD对象复制到Survivor中,然后直接清除Eden跟另外一个Survivor。当对象被Copy的次数超过一个配置值的时候,就会从年轻代移动到年老代。


    年轻代.png
    年老代回收
    a.在年轻代中,如果一个对象的年龄(GC一次后还存活的对象年岁加1)达到一个阈值(可以配置),就会被移动到年老代。
    b.Survivor中相同年龄的对象大小总和超过survivor空间的一半,则不小于这个年龄的对象都会直接进入年老代。
    c.创建的对象的大小超过设定阈值,这个对象会被直接存进年老代。
    d. 年轻代中大于survivor空间的对象,Minor GC时会被移进年老代。
    

    年老代中的对象特点就是存活时间较长,而且没有备用的空闲空间,所以显然不适合使用复制算法了,这个时候使用标记-清除算法或者标记-整理算法来实现GC。负责年老代中GC操作的是全局GC,Major GC,Full GC。
    什么时候触发Major GC呢?在Minor GC时,先检测JVM的统计数据,查看历史上进入老年代的对象平均大小是否大于目前年老代中的剩余空间,如果大于则触发Full GC。

    3.2、执行机制
    串行GC

    在搜索扫描和复制过程都是采用单线程实现,适用于单CPU、新生代空间较小或者要求GC暂停时间要求不高的地方。是client级别的默认方式。

    并行GC

    在搜索扫描和复制过程都是采用多线程实现,适用于多CPU、或者要求GC暂停时间要求高的地方。是server级别的默认方式。

    同步GC

    同时允许多个GC任务,减少GC暂停时间。主要应用在实时性要求重于总体吞吐量要求的中大型应用,即使如此,降低中断时间的技术还是会导致应用程序性能的降低。

    日常优化

    年老代空间不足
    1)分配足够大空间给old gen。
    2)避免直接创建过大对象或者数组,否则会绕过年轻代直接进入年老代。
    3)应该使对象尽量在年轻代就被回收,或待得时间尽量久,避免过早的把对象移进年老代。

    方法区的永久代空间不足
    1)分配足够大空间给。
    2)避免创建过多的静态对象。

    被显示调用System.gc()
    通常情况下不要显示地触发GC,让JVM根据自己的机制实现。

    1.年轻代过小(年老代过大)
    导致频繁发生GC,增大系统消耗
    容易让普通大文件直接进入年老代,从而更容易诱发Full GC。
    
    2.年轻代过大(年老大过小)
    导致年老代过小,从而更容易诱发Full GC。
    GC耗时增加,降低GC的效率。
    
    3.Eden过大(survivor过小)
    Minor GC时容易让普通大文件直接绕过survivor进入年老代,从而更容易诱发Full GC。
    
    4Eden过小(survivor过大)
    导致GC频率升高,影响系统性能。
    
    5调优策略
    保证系统吞吐量优先
    减少GC暂停时间优先
    
    堆设置
    
    -Xms:初始堆大小
    
    -Xmx:最大堆大小
    
    -XX:NewSize=n:设置年轻代大小
    
    -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
    
    -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
    
    -XX:MaxPermSize=n:设置持久代大小
    
    收集器设置
    
    -XX:+UseSerialGC:设置串行收集器
    
    -XX:+UseParallelGC:设置并行收集器
    
    -XX:+UseParalledlOldGC:设置并行年老代收集器
    
    -XX:+UseConcMarkSweepGC:设置并发收集器
    
    垃圾回收统计信息
    
    -XX:+PrintGC
    
    -XX:+PrintGCDetails
    
    -XX:+PrintGCTimeStamps
    
    -Xloggc:filename
    
    并行收集器设置
    
    -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
    
    -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    
    -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    
    并发收集器设置
    
    -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    
    -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
    

    相关文章

      网友评论

        本文标题:JVM垃圾回收机制小结

        本文链接:https://www.haomeiwen.com/subject/uiyddftx.html