垃圾收集器与内存分配策略

1概述

    在内存动态分配与内存回收技术已经很“自动化”很成熟的情况下,为什么还需要去了解GC(Garbage Collector)和内存分配?

    当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。

    内存回收与分配重点关注的是堆内存方法区内存(程序计数器占用小,虚拟机栈和本地方法栈随线程有相同的生命周期)。

2判断对象存活与否

2.1引用计数算法(非主流)

    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0 的对象就是不可能再被使用的。

优势:实现简单,效率高。
缺陷:无法解决对象相互引用的问题——会导致对象的引用虽然存在,但是已经不可能再被使用,却无法被回收。

2.2可达性分析算法(主流)

    这个算法的基本思 路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的


object5、object6、object7与GC Roots之间没有任何引用链相连,所以它们是可以回收的。

对象到GC Roots没有引用链,则判定为可回收。GC Roots包括:

  • Java虚拟机栈中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中Native方法(JNI)引用的对象。

2.3引用

  • 强引用:Object obj = new Object(),不会被jvm回收。
  • 软引用:在内存溢出异常发生之前才被强制回收。
  • 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用:仅为了对象在被GC回收时收到一个系统通知。

2.4finalize方法

    如果对象在可达性分析后发现没有与GC Roots相连接引用链,那它将会被GC第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法或者finalize方法已经被虚拟机调用过,则被视为不用执行。如果这个对象被视为有必要执行finalize方法,则这个对象先被置到一个F-Queue队列中,并触发一个低优先级的线程去执行finalize方法。finalize方法是对象逃离被回收的最后一次机会。之后GC进行第二次标记,如果对象实现自救则从待回收集合中移除,否则等待回收。

2.5方法区回收

    方法区(或者HotSpot虚拟机中的永久代)垃圾收集效率较低。Java虚拟机规范中也不要求方法区实现垃圾收集。永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

常量回收:当方法区中的一个常量不再被引用就会被回收。

无用类回收(同时满足以下条件):

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3垃圾收集算法

3.1标记-清除算法:首先标记出所有的需要回收的对象,在标记完成后统一回收所有标记的对象。

  • 效率问题:标记和清除两个过程的效率都不高。
  • 空间问题:清除后剩余空间零散不连续,无法为大的对象分配内存。

3.2复制算法:将内存分为两个半区A,B,如果A区需要清理则将A区中的存活对象全部复制到B区的连续空间,然后清理A中所有空间。

  • 缺点:内存实际空间减半。在对象存活率较高时需要进行较多的复制操作(所以老年代不能选用这个算法)。
  • 实际应用:将堆内存分为新生代和老年代。由于新生代中的对象98%都是可回收的,故将新生代又划分为Eden空间和两块较小的Survivor空间,默认Eden:Survivor(单个)=8:1。这样每次垃圾收集时,将Eden和S1中的存活对象复制到S2,然后清空Eden和S1区。
  • 分配担保:为了防止S2中空间不足以存储Eden和S1的所有剩余存活对象, 这些对象将直接通过分配担保机制进入老年代。

3.3标记整理算法:类似于标记清除算法,但是最后不是直接对可回收对象进行清除,而是让这些对象都向一端移动,然后直接清理掉端边界以外的内存。


3.4分代收集算法:当前商业虚拟机都采用分代收集,一般是把Java堆分为新生代和老年代。在新生代 中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

4HotSpot算法实现

4.1枚举根节点
    HotSpot的实现中,是使用一组称为OopMap的数据结构来记录那些地方存在对象的引用,这样就避免花费大量时间来进行GC Roots的可达性分析。使用OopMap枚举根节点的时候需要Stop The World。

4.2安全点与安全区域
    安全点:

  • 程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。在GC发生时候需要让所有线程跑到最近的安全点停下来,让线程跑到安全点停下来有两种方式:抢占式中断(已经不用了)和主动式中断。安全点是用来解决如何进入GC的。

    安全区域:

  • 安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也
  • 在程序不执行的时候,如线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
  • 可以把Safe Region看做是被扩展了的Safepoint。
  • 在线程执行到Safe Region中的代码时,首先标识线程自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离 开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完 成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

5垃圾收集器

垃圾收集中的并行与并发:

  • 并行(Parallel):多条垃圾收集线程。
  • 并发(Concurrent):用户其他线程与垃圾收集线程同时执行。

5.1Serial收集器(下图前半部分)

  • 单线程、Stop the World。
  • Client模式下的默认新生代收集器,使用复制算法。
  • 简单高效,适合在单个CPU工作环境下的新生代垃圾收集,没有线程切换开销。

5.2ParNew收集器

  • Serial的多线程版本(多条垃圾收集线程)。
  • Server模式下首选的新生代收集器。
  • 除Serial外,只有它能与CMS收集器配合工作。

5.3Parallel Scavenge 收集器

  • 新生代收集器,吞吐量优先。
  • 多线程,采用复制算法。

注:

    CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge的目标是达到一个可控制的吞吐量。吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

    停顿时间越短越适合需要与用户交互的程序,良好的响应速度可以提升用户体验,而高吞吐量可以高效率的利用CPU时间,尽快完成运算任务,适合后台运算而不需要太多交互的任务。

5.4Serial Old收集器(图见3-6后半部分)

  • Serial的老年代版本。
  • 单线程,stop the world。
  • 适合client端,使用“标记—整理”算法。

5.5Parallel Old收集器

  • Parallel Old 是Parallel Scavenge收集器的老年代版本,与Parallel Scavenge配合使用。
  • 多线程,标记-整理算法。

5.6CMS收集器(Concurrent Mark Sweep)

  • 以获取最短回收停顿时间为目标。
  • 标记-清除算法(为了获取更快地GC速度,同为老年代的垃圾收集算法“标记-整理”相对较慢)。

    CMS垃圾收集的四个步骤:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

    初始标记和重新标记需要Stop The World。初始标记仅仅标记一下GC Roots能直接关联到的对象。并发标记就是进程GC Roots的追踪过程。重新标记是为了修正并发标记期间用户线程继续运行导致标记发生改变的那一部分标记记录。

    耗时最长的并发标记和并发清理过程收集器线程都可以与用户线程并发执行。

优点:

  • 并发收集、低停顿。

缺点:

  • 既然是并发收集的,对CPU资源敏感,在CPU个数不多( < 4个)的时候表现一般。
  • 使用标记-清除算法,收集后容易产生空间碎片(空间碎片可以合并整理,但是会导致收集变慢)。
  • 可能出现“Concurrent Mode Failure”失败,而导致另一次Full GC的产生。CMS在并发清除的时候用户线程还在继续执行,这样会产生浮动垃圾,浮动垃圾要在下一次GC的时候才清理掉。这需要为浮动垃圾预留内存空间,而不是在老年代几乎满的时候才去收集。

注:

  • Minor GC:清理新生代(Eden、Survivor(包括from,to))。
  • Major GC:清理老年代(Tenured)。
  • FULL GC:清理整个堆空间。

    堆内存划分学习链接

5.7G1收集器

  • 最前沿成果。
  • 削弱新生代与老年代概念,将整个堆划分为独立的不同Region。
  • 根据各Region的回收价值,确定优先列表。

优点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者 CPU核心)来缩短Stop-The-World停顿的时间。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。不需要和其他垃圾收集器搭配使用。
  • 空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实 现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型。G1选择回收具有“回收价值大(有限时间内回收后获得的空间大小尽量大)”的Region。

     

G1垃圾收集的步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

前3个阶段与CMS大同小异,筛选回收阶段是指首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

6内存分配与回收策略

     Java的自动内存管理可以归结为两个问题:给对象分配内存以及回收分配给对象的内存。此前已经讲述了回收,下面来看看如何给对象分配内存。

    对象的内存分配,就是在堆上分配(但也可能经过JIT编译后被拆散为标 量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟 机中与内存相关的参数的设置。

    主要存在一下几条内存分配原则:

  • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
  • 大对象直接进入老年代。虚拟机提供了一个-XX:PretenureSizeThreshold参数(Serial和ParNew支持这个参数),令大于这个设置值的对象直接在老年代分配,避免在新生代发生大量的复制。
  • 长期存活的对象将进入老年代。虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被 Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中 每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就 将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。
  • 动态对象年龄判定。新生代对象晋升到老年代不一定要达到MaxTenuringThreshold设置的年龄。在新生代中相同年龄的对象大小超过Survivor空间一半时,年龄大于等于该年龄的对象可以直接进入老年代,无需等待年龄增大到MaxTenuringThreshold。
  • 空间分配担保。最新的规则是老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
     

参考《深入理解Java虚拟机》

GC与内存分配学习链接

GC与内存分配学习链接2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值