垃圾收集

垃圾回收

  1. 什么是垃圾?
    在Java程序运行过程中没有任何引用指向的对象,就是垃圾对象
  2. 为什么需要GC?
    对于高级语言来说,没有GC内存迟早会被消耗完
    除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片,以便JVM将整理出的内存分配给新的对象
    应用程序所应付的业务越来越庞大、复杂,没有GC不能保证应用程序的正常进行。(经常造成STW的GC跟不上实际的需求,要不断对GC进行优化)
  3. 引用计数法
    • 为每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有其他对象引用了A,那么对象A的引用计数器就加1;当引用失效时,引用计数器就减1, 只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可以对他进行回收
    • 优点:实现简单,判断是否可回收只需要看对象的引用计数器是否为0,垃圾对象便于辨识;判定效率高,不用找GC Roots,回收没有延迟性
    • 缺点:需要单独的字段存储计数器,有空间开销;更新计数器,有时间开销;引用计数法无法处理循环引用问题
  4. 可达性分析算法
    1. 维护一个根对象集合GC Roots,并以它为起始点,探测目标对象是否可达。其中探测过程所走过的路径我们称为引用链,如果目标对象没有任何引用链相连,则是不可达的,可以对他进行回收。所以在可达性分析算法中,只有能够被GC Roots集合直接或间接相连的对象才是存活对象。
    2. GC Roots包括了哪几类元素
      • 虚拟机栈引用的对象(各个线程调用的方法中使用到的参数、局部变量等)
      • 本地方法栈内JNI(本地方法接口)引用的对象
      • 方法区中类静态属性引用的对象(引用类型静态变量)
      • 方法区中常量引用的对象(字符串常量池里的引用)
      • 所有被同步锁synchronized持有的对象
      • Java虚拟机内部的引用(基本数据类型对象的Class对象,一些常驻的异常对象、系统类加载器)
      • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
    3. 缺点:分析工作必须在一个能保证一致性的快照中进行,STW时间无论如何都省不了

对象的finalization机制

  • 当垃圾收集器发现没有引用指向某对象时,并不是马上就去回收它,而是先调用该对象的finalize方法
  • 不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,理由如下
    1. 在调用finalize()时可能会导致对象复活
    2. finalize()方法的执行是没有保障的,完全由GC线程决定,极端情况下不发生GC,也就不会调用finalize()
    3. 一个糟糕的finalize()可能会严重影响GC的性能
  • 在垃圾收集之前,一个对象可能存在三种状态
    1. 可触及的:能够从根节点出发,访问到该对象
    2. 可复活的:没有任何对象引用它,但是它还没有调用finalize(),此时他是有可能在finalize()中复活的
    3. 不可触及的:对象的finalize()被调用,并且没有复活,就会进入不可触及状态。不可触及对象不可能被复活,因为finalize()只会被调用一次
  • 加入finalize()的垃圾收集–两次标记过程
    1. 如果对象obj到GC Roots没有引用链,则进行第一次标记
    2. 进行筛选,判断该对象是否有必要执行finalize()
      • 如果对象obj没有重写finalize(),或者finalize()已经被虚拟机调用过,则虚拟机视为“没有必要执行”,obj被判定为不可触及的
      • 如果对象obj重写了finalize(),且还未执行过,那么obj会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()执行
      • 如果obj在finalize()中与引用链上的任何一个对象建立了联系,那么在第二次标记时,obj会被移出“即将回收”集合,即对象复活了。这时,finalize()方法不会被再次调用,对象变成不可触及状态
  • MAT和JProfiler查看GC Roots
    1. 获取dump文件
      • 命令行使用jmap 如:jmap -dump:format=b,live,file=test1.bin 14036
      • JVisual VM导出,选中dump文件另存

    2. 使用MAT打开dump文件
      图片[1]-垃圾收集-一一网
    3. 独立使用JProfiler监控

垃圾收集算法及垃圾收集相关理论

  • 垃圾清除阶段算法
    1. 标记清除算法
      】】

      1. 流程:
        • 标记环节:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象(注意:标记的是非垃圾对象)
        • 清除环节:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

        清除并不是置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否足够,够的话就放置到那里

      2. 优缺点:
        • 优点: 简单易实现
        • 缺点:效率不高,标记和清除环节都是全空间;在进行GC时候要停止整个应用程序;清除垃圾后,空闲内存不连续,有内存碎片
    2. 复制算法

      1. 流程:
        • 将空闲的内存空间分为两块,每次只用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,不断交换两个内存的角色,最后完成垃圾回收
      2. 优缺点:
        • 优点:相较于标记-清除,运行高效,找到引用的对象后不再标记直接复制到另一块空间中;保证了空间的连续性,不会出现碎片问题
        • 缺点:需要两倍的内存空间;由于要将对象从一块区域复制到另一块区域,那维护对象引用关系也是不小的开销

        注意:当存在的垃圾对象不多,或者没有垃圾对象时,那采用这种算法效率就会很低了,因为他白白复制了一遍还维护了一遍引用关系

      3. 应用场景:针对新生代垃圾对象多的特点,所在在新生代多采用复制算法
    3. 标记压缩算法

      1. 流程
        • 标记环节:和标记清除相同
        • 压缩环节:将所有的存活对象压缩到内存的一端,留下一段连续的内存空间,之后清理边界外所有的空间
      2. 优缺点
        • 优点:没有内存碎片,JVM只需要持有一个内存的起始地址即可(可以使用指针碰撞法来分配内存)
        • 缺点:压缩算法本身带来的开销;在进行GC时候要停止整个应用程序;移动对象时,要维护引用地址;移动过程中,需要STW
      3. 应用场景:由于老年代对象存活率高且没有额外空间进行分配担保,采用压缩法
  • 收集算法扩展
    1. 分代收集
      1. 新生代:结合新生代对象存活时间较短、垃圾对象较多的特点,使用复制算法(hotspot通过利用两个survivor区的设计提高了复制算法内存利用率不高的问题–浪费的空间占新生代的1/10)
      2. 老年代:结合老年代对象存活率高的特点,使用标记-清除或者标记-清除和标记-压缩的混合实现方法
    2. 增量收集算法
      1. 流程:
        • 针对标记清除和标记整理算法STW时间过长提出的,让垃圾收集线程和应用程序线程交替执行。每次垃圾收集,垃圾收集线程只收集一小片区域内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成
      2. 优缺点:
        • 优点:STW时间变短,用户体验较好
        • 缺点:线程切换和上下文切换的消耗,会使得垃圾回收的总体成本上升,系统吞吐量下降
    3. 分区算法(G1)
      1. 流程
        • 为更好控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,这样就减少了一次GC所产生的停顿(根据时间来,时间短的话就少回收几个,时间长的话多回收几个)
  • 内存溢出和内存泄露
    • 内存溢出
      1. 系统没有空闲内存可供程序使用,并且垃圾收集器也无法提供更多的内存,就可以理解为内存溢出了
      2. 可能引起内存溢出的原因如下:
        • JVM堆内存空间设置的过小,不能够满足系统的使用
        • 系统中创建了大量大对象,并且长时间不能被垃圾收集器收集(如方法区加载过多的类、堆中死循环创建对象、方法递归调用导致栈溢出)
    • 内存泄露
      1. 对象不会再被程序中引用到了,但是GC又不能回收它,就是内存泄露了(从另一个层面来看,在程序中有的变量没有必要声明到方法外,却声明到了方法外,这样变量的生命周期变长,也可以理解为内存泄露)
      2. 可能引起内存泄露的举例:
        • 单例模式:单例模式生命周期很长,如果在单例程序中引用了外部对象,那这个外部对象是不能被回收的,这就导致了内存的泄露
        • 提供close的资源未关闭:数据库连接、网络连接、IO连接等必须手动close,否则就造成了内存的泄露
  • 垃圾回收的并发与并行
    • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

      如ParNew、Parallel Scavenge、Parallel Old

    • 串发(Serial):单线程执行,如果内存不够则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程
    • 并发(Concurrent):指用户线程与垃圾收集线程同时执行,垃圾回收线程在执行时不会停顿用户程序的运行

      用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS、G1

  • 安全点(Safepoint)
    • 安全点指的是在代码执行过程中的一些特殊位置,当代码处于这个位置时,虚拟机当前状态是稳定且安全的,可以进行GC等操作
    • 如何在GC发生时,检查所有线程都在最近的安全点停顿下来呢?
      1. 抢先式中断:首先中断所有线程,如果有的线程不在安全点位置,就恢复该线程,让线程跑到安全点
      2. 主动式中断:设置一个中断标志,各个线程运行到安全点时主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起
  • 安全区域(Safe Region):
    • 产生原因:安全点机制保证的是程序执行时,在不太长的时间内就会遇到可进入GC的SafePoint,但是程序不执行时,如当前线程处于Sleep状态或Blocked状态,线程就无法响应JVM的中断请求,这时就需要安全区域来解决
    • 安全区域是指在这一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的
    • 工作流程
      1. 当线程运行到安全区域代码时,首先标志该线程进入了安全区域,如果这段时间内发生GC,JVM会忽略标识为安全区域状态的线程
      2. 当线程即将离开安全区域时,会检查JVM是否已经完成GC,如果完成了,则继续运行;否则线程必须等待可以离开安全区域的信号
  • Java中的引用类型
    1. 强引用如Object obj = new Object()只要强引用关系存在,垃圾回收器就永远不会回收强引用引用的对象
    2. 软引用如SoftReference<String> str = new SoftReference<>(new String("abc"));描述一些还有用但并非必需的对象,在系统发生内存不足之前会将软引用对象进行第二次回收,如果此次回收完之后还没有足够内存,才会抛OOM(适合做缓存,在内存足够时,直接通过软引用取值,内存不足时,删除缓存,从真实来源查询数据)
    3. 弱引用如WeakReference<String> str = new WeakReference<>(new String("abc"));描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论内存是否足够,垃圾收集器见到直接回收(可以在回调函数时防止内存泄漏)
    4. 虚引用,如PhantomReference<String> str = new PhantomReference<>("abc",new referenceQueue());当垃圾回收器准备回收它时,发现它还有虚引用,就会在回收对象内存前,把这个虚引用加入到与之关联的引用队列中。对象是否有虚引用的存在完全不会对其生存空间构成影响,也无法通过虚引用来获取一个对象实例,为一个对象设置虚引用的唯一目的就是该对象在被回收时能收到一个系统通知

垃圾收集器概述

  • 评价GC的性能指标
    1. 吞吐量:运行用户代码的时间占总运行时间(用户代码运行时间+内存回收时间)的比例
    2. 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
    3. 内存占用:Java堆区所占的内存大小
    4. 垃圾收集开销:吞吐量的补数(1-吞吐量),垃圾收集所用时间与总运行时间的比例
    5. 收集频率:相对于应用程序的执行,收集操作发生的频率
    6. 快速:一个对象从诞生到被回收所经历的时间
  • 吞吐量和暂停时间的对比

    • 高吞吐量会让用户感觉到只有应用程序在工作,直觉上,吞吐量越高程序运行越快
    • 低暂停时间会凸显出低延迟特性,对交互式应用程序时很重要的
  • 不同的垃圾收集器分类
    • 串行、并行和并发角度
      1. 串行回收器:Serial、Serial Old
      2. 并行回收器:ParNew、Parallel Scavenge、Parallel Old
      3. 并发回收器:CMS、G1
    • 垃圾收集器作用位置
      1. 新生代收集器:Serial、ParNew、Parallel Scavenge
      2. 老年代收集器:Serial Old、Parallel Old、CMS
      3. 整堆收集器:G1
  • 垃圾收集器组合关系

    1. CMS收集器是并发的(垃圾回收和用户线程是同时进行的)
    2. 在JDK8中,Serial+CMS和ParNew+Serial Old两个组合被废弃掉,在JDK9中被移除。同时JDK8默认的垃圾收集器是Parallel Scavenge+Parallel Old组合
    3. 在JDK9中,默认垃圾收集器是G1
    4. 在新发布的JDK14中,Parallel Scavenge和SerialOld GC组合被废弃掉,同时移除CMS
  • 查看默认的垃圾收集器
    • -XX:+PrintCommandLineFlags:查看命令行参数,会包含使用的垃圾收集器
      -XX:InitialHeapSize=132500864 -XX:MaxHeapSize=2120013824 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
      复制代码
    • jinfo -flag 垃圾收集器参数 进程id
      >>	jinfo -flag UseParallelGC 22848
      -XX:+UseParallelGC			-- +号说明使用了,-号说明没有使用
      复制代码

Serial回收器:串行回收

  1. 是Hotspot运行在Client模式下的默认的新生代收集器,它采用复制算法、串行回收和STW机制执行内存的回收。
  2. 新生代之外,Serial收集器还提供针对于老年代垃圾收集的Serial Old收集器,和新生代不用的是,它采用标记压缩算法,Serial Old是运行在Client模式下默认的老年代垃圾回收器

    Serial Old在Server模式下主要有两个用途:

     1.与新生代的Parallel Scavenge配合使用   
     2.作为老年代CMS收集器的后备垃圾收集方案
    复制代码
  3. 单线程收集器,说明他只使用一个CPU或一个垃圾收集线程去完成垃圾收集工作,而且在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束
  4. 优势:简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
  5. 设置垃圾收集器
    -XX:+UseSerialGC 此命令将新生代设置为SerialGC,同时会将老年代设置为SerialOldGC

ParNew回收器:并行回收

  1. ParNew收集器是Serial收集器的多线程版本(Par指Parallel并行,New指新生代)。ParNew和Serial同样采用复制算法、STW机制,不同的是它采用并行回收方式(多条垃圾收集线程同时工作)
  2. ParNew是很多JVM产品在Server模式下新生代的默认垃圾收集器

    • 对于新生代,回收次数频繁,使用并行方式会更高效
    • 对于老年代,回收次数少,使用串行方式可以节省资源

      注意:使用ParNew收集器收集新生代垃圾,可以搭配CMS、Serial Old两款老年代收集器。但是在8中就对ParNew+Serial Old组合废弃了,在14中将CMS移除,所以ParNew在新生代垃圾收集中已经没有什么地位了

  3. 设置垃圾收集器
    1. 使用-XX:+UseParNewGC选项手动指定使用ParNew收集器收集新生代垃圾,不影响老年代
    2. 使用-XX:ParallelGCThreads限制线程数量,默认开启和CPU数量相同的线程数

      默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数;当CPU数大于8个,设置为3+[5*CPU_COUNT/8]

Parallel回收器:吞吐量优先

  1. Parallel Scavenge收集器和ParNew收集器一样采用了复制算法、并行回收和STW机制
  2. Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,它以吞吐量为优先,同时提供了针对于老年代的Parallel Old垃圾收集器,它采用并行回收、标记压缩算法
  3. 高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务(批量处理、订单处理、工资支付、科学计算…)
  4. 设置垃圾收集器
    • -XX:+UseParallelGC 手动指定新生代使用Parallel并行收集器
    • -XX:+UseParallelOldGC 手动指定老年代,以上这两个参数开启一个,另一个会自动开启
    • -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(为了将停顿时间尽可能的控制在该时间范围内,收集器会调整Java堆大小或者其他参数)
    • -XX:GCTimeRatio 垃圾收集时间占总时间的比例(1/(n+1)),用于衡量吞吐量的大小(默认值99,即垃圾回收时间不超过1%)
    • -XX:+UseAdaptiveSizePolicy 指定收集器自适应调节策略,默认开启(自动调整新生代大小、Eden和Survivor区比例、晋升老年代对象年龄等参数,以达到堆大小、吞吐量和停顿时间的平衡点)

CMS回收器:低延迟

  • 它采用标记清除算法、STW机制,是Hotspot虚拟机中第一款真正意义上的并发收集器,实现了垃圾收集线程与用户线程同时工作
  • 四阶段清除:
    1. 初始标记:初始标记仅标记那些GC Roots能直接关联到的对象,由于直接关联对象较少,速度很快,STW时间很短
    2. 并发标记:从GC Roots的直接关联对象开始遍历整个对象图过程,耗时较长但是不需要停顿用户线程
    3. 重新标记:重新标记是修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分,这阶段STW时间比初始标记阶段稍长,但远比并发标记阶段时间短
    4. 并发清除:并发清理掉标记阶段判断的已经死亡的对象,释放内存空间。由于采用的是标记-清除算法,不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
  • CMS垃圾回收的时机,如何确定?
    1. 原因:由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,应该确保用户线程有足够的内存可用(其他收集器在面临用户线程对象占满内存时,会让用户线程停顿,并开始清理垃圾对象,而CMS由于是并发的,他不能等到内存不够了再去回收,那时候很容易造成内存溢出)
    2. 解决:当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。如果CMS运行期间预留的内存无法满足程序的需要,会出现Concurrent Mode Failure失败,此时虚拟机会启用后备方案,临时使用Serial Old收集器进行老年代的垃圾回收(这样垃圾回收变成了串行进行,更慢了)
  • CMS的弊端:
    1. 会产生内存碎片问题。在无法分配到大对象的情况下,不得不提前触发Full GC
    2. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总的吞吐量会降低(比如在并发标记阶段,可能存在四个用户线程,但是现在分出去一个去做垃圾收集)
    3. CMS收集器无法处理浮动垃圾。并发标记阶段、并发清理阶段用户线程可能还在生产垃圾,这一部分垃圾出现在标记过程之后,CMS无法在当次收集处理,只能下一次GC处理
  • CMS采用标记清除会产生内存碎片,为什么不用标记压缩算法?
    • 用压缩整理内存方式会改变对象的引用地址,那么用户线程就无法工作了。但是CMS作为并行回收的一个垃圾收集器,必须保证用户线程和垃圾收集线程都能够正常工作,所以不能使用标记压缩算法
  • 设置垃圾收集器
    • -XX:+UseConcMarkSweepGC 手动指定使用CMS收集器(开启该参数会自动将ParNew作为新生代垃圾收集器作为组合)
    • -XX:CMSInitiatingOccupanyFraction设置堆内存使用率的阈值,达到该阈值,开始进行垃圾回收(JDK5及以前默认68,JDK6及以后默认92,当老年代空间使用率达到该比例时就会执行CMS回收。 如果内存增长缓慢,可以设置一个稍大的值,大的阈值可以有效降低CMS的触发效率,减少老年代回收次数;如果应用程序内存使用率增长很快,应该降低该阈值,可以避免频繁触发Serial Old收集器)
    • -XX:+UseCMSCompactAtFullCollection指定在执行完Full GC后对内存空间进行压缩处理,避免内存碎片的产生(压缩无法并发进行,导致停顿时间变长)
    • -XX:CMSFullGCsBeforeCompaction 设置执行多少次Full GC后对内存空间进行压缩
    • -XX:ParallelCMSThreads设置CMS的线程数量(默认启动线程数为(n+3)/4,n为新生代并行收集器的线程数)

G1收集器:区域化分代式

  • 分区Regin的设计

    1. G1收集器将Java堆划分为约2048个大小相同的Region,每个Region块大小根据堆空间的实际大小而定,整体大小在1-32MB之间 且应为2的N次幂,可以通过参数修改
    2. 一个region可以属于Eden、Survivor、Old内存区域,但是一个region只能属于一种角色
    3. Humongous内存区域,它用于存储大对象,如果超过1.5个region大小就会放置到H区(如果一个H区仍放不下对象,G1会寻找连续的H区来存储;如果找不到连续的,full gc)
  • G1收集器的特点(优势)
    1. 兼具并发与并行
      • 并行性: G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程需STW
      • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此一般情况下不会再整个回收阶段发生完全阻塞应用程序的情况
    2. 分代收集
      • 从分代上看,G1依然属于分代型垃圾回收器,他区分新生代和老年代,新生代再分Eden和Survivor;但是从堆结构来看,它不要求整个Eden、年轻代或者老年代是连续的,也不再坚持固定大小和固定数量
      • 将整个堆空间分为若干个区域Regin,这些区域中包含了逻辑上的新生代和老年代
    3. 空间整合
      • G1将内存划分为一个个region,内存的回收是以region作为基本单位的,Region之间(将存活对象复制到空白Regin中)是复制算法,但整体上可看做是标记-压缩算法,两种算法都能够避免内存碎片的产生
    4. 可预测的停顿时间模型
      • G1除追求低停顿外,还能建立可预测的停顿时间模型,能让使用者指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒((M – N) / M为吞吐量)
      • 由于G1是针对Regin回收的,可以只选取部分区域进行内存回收,缩短了回收范围,同时避免了全局停顿
      • G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的的空间大小以及所需时间的经验值)并在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
  • G1收集器的缺点
    • G1内存占用和额外执行负载都比CMS收集器要高;
    • 从经验上来说,在小内存应用上CMS的表现大概率优于G1,而G1在大内存应用上则发挥其优势(平衡点在6-8G)
  • 设置垃圾收集器
    1. -XX:+UseG1GC 使用G1收集器
    2. -XX:G1HeapRegionSize 设置Regin的大小,置为2的幂,范围1-32MB之间
    3. -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标,默认200ms
  • G1收集器的适用场景
    1. 面向服务端应用,针对具有大内存、多处理器的机器,最主要的应用是需要低GC延迟
    2. 用来替换掉JDK1.5中的CMS收集器
      1. 超过50%的Java堆被活动数据占用
      2. 对象分配频率或年代提升频率变化很大
      3. GC停顿时间过长
  • Remembered Set解决全局扫描问题
    • 问题:一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否应该扫描下整个堆区来精确判断?但是扫描整堆,会导致Minor GC效率降低,STW时间边长
    • 解决:
      1. 首先为每一个Region分配一个对应的Remembered Set
      2. 每次引用类型数据在执行写操作时,都会产生一个写屏障暂时中断操作,检查将要写入的引用指向的对象是否和该引用类型数据在相同的Region,如果相同就不处理了,如果不同,通过卡表CardTable将相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中

      图片[2]-垃圾收集-一一网
      3. 在进行垃圾收集时,只要在GC根节点的枚举范围内加入R Set,就可以保证不进行全局扫描

  • G1收集器垃圾回收过程

    1. 新生代GC

      1. 扫描根: GC Roots+RSet记录的外部引用 作为扫描存活对象的入口
      2. 更新RSet: 处理脏卡表中的card,更新RSet
      3. 处理RSet:识别被老年代指向的Eden中的对象,这些对象仍为存活对象
      4. 复制对象:Eden区和From Survivor区存活对象 会被复制到 To Survivor区中(这里指的是Region)
      5. 处理引用:其他引用
    2. 老年代并发标记过程(堆内存使用达到阈值(默认45%)后)
      1. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC
      2. 根区域扫描 : G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成(young gc会调整Survivor区内对象的引用)
      3. 并发标记: 在整个堆中进行并发标记,此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
      4. 再次标记: 由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的原始快照算法SATB
      5. 独占清理:计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为清理阶段做铺垫。是STW的。
      6. 并发清理阶段:识别并清理完全空闲的区域。
    3. 混合回收
      • 标记完成即开始混合回收过程,对于混合回收期,G1 GC从老年代区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和新生代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了
      1. 并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次被回收。
      2. 混合回收的回收集包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。
      3. 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,默认为65%(意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间)
  • 经典垃圾收集器对照关系

GC日志分析

  • -XX:+PrintGC 输出GC日志,很简短一般就是垃圾回收的行为。类似: -verbose:gc

  • -XX:+PrintGCDetails 输出GC的详细日志,包含了垃圾回收行为和回收完毕后的堆内存信息

  • -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)

  • -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800 )

  • -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

  • -Xloggc:../logs/gc.log 日志文件的输出路径

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享